Skip to content

Instantly share code, notes, and snippets.

@linkerlin
Last active September 4, 2025 03:08
Show Gist options
  • Save linkerlin/94215239bf3323a97cfd425c82f21c0e to your computer and use it in GitHub Desktop.
Save linkerlin/94215239bf3323a97cfd425c82f21c0e to your computer and use it in GitHub Desktop.
MmapCache: 基于 mmap 的多进程共享缓存库
<?php
/**
* ShmCache: 基于 shmop 的多进程共享缓存库
* 支持 key-value 存储,兼容 PHP 原生数据结构,优化读写数据量,保持 ACID
* 需要启用 shmop 扩展
*/
class ShmCache {
private $shmId;
private $shmKey;
private $shmSize;
private $lockFile;
private $lockHandle;
private $index; // key => [offset, length]
private $dataOffset; // 数据区起始偏移量
private const INDEX_SIZE = 4096; // 索引区固定大小(字节)
/**
* 构造函数,初始化共享内存和锁
* @param string $lockFilePath 锁文件路径
* @param int $size 共享内存大小(字节)
*/
public function __construct(string $lockFilePath, int $size = 1048576) {
$this->shmSize = $size;
$this->lockFile = $lockFilePath;
$this->dataOffset = self::INDEX_SIZE;
// 生成共享内存键
$this->shmKey = ftok(__FILE__, 't');
if ($this->shmKey === -1) {
throw new RuntimeException("无法生成共享内存键");
}
// 初始化锁文件
$this->lockHandle = fopen($this->lockFile, 'c+');
if ($this->lockHandle === false) {
throw new RuntimeException("无法创建锁文件: {$this->lockFile}");
}
// 初始化共享内存
$this->shmId = @shmop_open($this->shmKey, 'w', 0, 0);
if ($this->shmId === false) {
$this->shmId = shmop_open($this->shmKey, 'c', 0644, $this->shmSize);
if ($this->shmId === false) {
fclose($this->lockHandle);
throw new RuntimeException("无法创建共享内存");
}
$this->initializeShm();
}
// 加载索引
$this->loadIndex();
}
/**
* 初始化共享内存
*/
private function initializeShm(): void {
$this->index = [];
$this->saveIndex();
}
/**
* 加载索引
*/
private function loadIndex(): void {
try {
if (!flock($this->lockHandle, LOCK_SH)) {
throw new RuntimeException("无法获取共享锁");
}
$data = shmop_read($this->shmId, 0, self::INDEX_SIZE);
if ($data === false || empty(trim($data, "\0"))) {
$this->index = [];
} else {
$decoded = @unserialize($data);
$this->index = $decoded !== false ? $decoded : [];
}
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 保存索引
*/
private function saveIndex(): void {
$serialized = serialize($this->index);
if (strlen($serialized) > self::INDEX_SIZE) {
throw new RuntimeException("索引大小超出限制: " . self::INDEX_SIZE . " 字节");
}
try {
if (!flock($this->lockHandle, LOCK_EX)) {
throw new RuntimeException("无法获取独占锁");
}
$result = shmop_write($this->shmId, str_pad($serialized, self::INDEX_SIZE, "\0"), 0);
if ($result === false) {
throw new RuntimeException("写入索引失败");
}
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 设置缓存值
* @param string $key 键
* @param mixed $value 值(支持 PHP 原生数据结构)
* @return bool
*/
public function set(string $key, $value): bool {
try {
if (!flock($this->lockHandle, LOCK_EX)) {
throw new RuntimeException("无法获取独占锁");
}
$serialized = serialize($value);
$length = strlen($serialized);
// 检查剩余空间
$newOffset = $this->getNextOffset();
if ($newOffset + $length > $this->shmSize) {
throw new RuntimeException("数据超出共享内存容量");
}
// 写入数据
$result = shmop_write($this->shmId, $serialized, $newOffset);
if ($result === false) {
throw new RuntimeException("写入数据失败");
}
// 更新索引
$this->index[$key] = [$newOffset, $length];
$this->saveIndex();
return true;
} catch (RuntimeException $e) {
error_log("ShmCache set error: " . $e->getMessage());
return false;
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 获取缓存值
* @param string $key 键
* @param mixed $default 默认值
* @return mixed
*/
public function get(string $key, $default = null) {
try {
if (!flock($this->lockHandle, LOCK_SH)) {
throw new RuntimeException("无法获取共享锁");
}
if (!isset($this->index[$key])) {
return $default;
}
[$offset, $length] = $this->index[$key];
$data = shmop_read($this->shmId, $offset, $length);
if ($data === false) {
throw new RuntimeException("读取数据失败");
}
$value = @unserialize($data);
return $value !== false ? $value : $default;
} catch (RuntimeException $e) {
error_log("ShmCache get error: " . $e->getMessage());
return $default;
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 删除缓存键
* @param string $key 键
* @return bool
*/
public function delete(string $key): bool {
try {
if (!flock($this->lockHandle, LOCK_EX)) {
throw new RuntimeException("无法获取独占锁");
}
if (!isset($this->index[$key])) {
return false;
}
unset($this->index[$key]);
$this->saveIndex();
return true;
} catch (RuntimeException $e) {
error_log("ShmCache delete error: " . $e->getMessage());
return false;
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 清空缓存
* @return bool
*/
public function clear(): bool {
try {
if (!flock($this->lockHandle, LOCK_EX)) {
throw new RuntimeException("无法获取独占锁");
}
$this->index = [];
$this->saveIndex();
return true;
} catch (RuntimeException $e) {
error_log("ShmCache clear error: " . $e->getMessage());
return false;
} finally {
flock($this->lockHandle, LOCK_UN);
}
}
/**
* 检查键是否存在
* @param string $key 键
* @return bool
*/
public function has(string $key): bool {
$this->loadIndex();
return isset($this->index[$key]);
}
/**
* 获取下一个可用数据偏移量
* @return int
*/
private function getNextOffset(): int {
if (empty($this->index)) {
return $this->dataOffset;
}
$last = max(array_map(fn($entry) => $entry[0] + $entry[1], $this->index));
return max($this->dataOffset, $last);
}
/**
* 析构函数,清理资源
*/
public function __destruct() {
if ($this->shmId !== false) {
shmop_close($this->shmId);
}
if ($this->lockHandle !== false) {
fclose($this->lockHandle);
}
}
}
/**
* 示例用法
*/
if (defined('STDIN')) {
try {
// 初始化缓存,指定锁文件路径和共享内存大小(1MB)
$cache = new ShmCache('/tmp/shm_cache.lock', 1048576);
// 设置简单值
$cache->set('user1', ['id' => 1, 'name' => 'Alice']);
// 设置复杂数据结构
$cache->set('config', [
'settings' => ['theme' => 'dark', 'lang' => 'en'],
'data' => [1, 2, 3]
]);
// 获取值
var_dump($cache->get('user1')); // ['id' => 1, 'name' => 'Alice']
var_dump($cache->get('config')); // 复杂数组
var_dump($cache->get('nonexistent', 'default')); // 'default'
// 检查键
var_dump($cache->has('user1')); // true
var_dump($cache->has('unknown')); // false
// 删除键
$cache->delete('user1');
var_dump($cache->has('user1')); // false
// 清空缓存
$cache->clear();
var_dump($cache->get('config')); // null
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . PHP_EOL;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment