From fd8b3721aee8fb02ddce14e2181adc7d584c5a43 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 31 Dec 2022 15:27:09 +0800 Subject: [PATCH] add LightCache support --- config/global.php | 9 +- src/Globals/global_class_alias.php | 1 + src/Globals/global_functions.php | 11 ++ src/ZM/Event/Listener/WorkerEventListener.php | 10 ++ src/ZM/Store/KV/KVInterface.php | 47 +++++ src/ZM/Store/KV/LightCache.php | 164 ++++++++++++++++++ tests/ZM/Store/KV/LightCacheTest.php | 47 +++++ 7 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/ZM/Store/KV/KVInterface.php create mode 100644 src/ZM/Store/KV/LightCache.php create mode 100644 tests/ZM/Store/KV/LightCacheTest.php diff --git a/config/global.php b/config/global.php index b8700668..63e7268c 100644 --- a/config/global.php +++ b/config/global.php @@ -40,7 +40,7 @@ $config['swoole_options'] = [ ]; /* 默认存取炸毛数据的目录(相对目录时,代表WORKING_DIR下的目录,绝对目录按照绝对目录来) */ -$config['data_dir'] = 'zm_data'; +$config['data_dir'] = WORKING_DIR . '/zm_data'; /* 框架本体运行时的一些可调配置 */ $config['runtime'] = [ @@ -102,4 +102,11 @@ $config['database'] = [ ], ]; +/* KV 数据库的配置 */ +$config['kv'] = [ + 'use' => \LightCache::class, // 默认在单进程模式下使用 LightCache,多进程需要使用 ZMRedis + 'light_cache_dir' => $config['data_dir'] . '/lc', // 默认的 LightCache 保存持久化数据的位置 + 'light_cache_autosave_time' => 600, // LightCache 自动保存时间(秒) +]; + return $config; diff --git a/src/Globals/global_class_alias.php b/src/Globals/global_class_alias.php index 0d9c677b..749e28ce 100644 --- a/src/Globals/global_class_alias.php +++ b/src/Globals/global_class_alias.php @@ -15,6 +15,7 @@ class_alias(\ZM\Annotation\Closed::class, 'Closed'); class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin'); class_alias(\OneBot\V12\Object\OneBotEvent::class, 'OneBotEvent'); class_alias(\ZM\Context\BotContext::class, 'BotContext'); +class_alias(\ZM\Store\KV\LightCache::class, 'LightCache'); // 下面是 OneBot 相关类的全局别称 class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent'); diff --git a/src/Globals/global_functions.php b/src/Globals/global_functions.php index a179240d..8f459449 100644 --- a/src/Globals/global_functions.php +++ b/src/Globals/global_functions.php @@ -14,6 +14,8 @@ use ZM\Middleware\MiddlewareHandler; use ZM\Store\Database\DBException; use ZM\Store\Database\DBQueryBuilder; use ZM\Store\Database\DBWrapper; +use ZM\Store\KV\KVInterface; +use ZM\Utils\ZMRequest; // 防止重复引用引发报错 if (function_exists('zm_internal_errcode')) { @@ -246,3 +248,12 @@ function bot(): ZM\Context\BotContext } return new \ZM\Context\BotContext('', ''); } + +function kv(string $name = ''): KVInterface +{ + global $kv_class; + if (!$kv_class) { + $kv_class = config('global.kv.use', \LightCache::class); + } + return $kv_class::open($name); +} diff --git a/src/ZM/Event/Listener/WorkerEventListener.php b/src/ZM/Event/Listener/WorkerEventListener.php index 6d051e87..42b0c809 100644 --- a/src/ZM/Event/Listener/WorkerEventListener.php +++ b/src/ZM/Event/Listener/WorkerEventListener.php @@ -20,6 +20,7 @@ use ZM\Process\ProcessStateManager; use ZM\Store\Database\DBException; use ZM\Store\Database\DBPool; use ZM\Store\FileSystem; +use ZM\Store\KV\LightCache; use ZM\Utils\ZMUtil; class WorkerEventListener @@ -70,6 +71,11 @@ class WorkerEventListener logger()->info('WORKER#' . $i . ":\t" . ProcessStateManager::getProcessState(ZM_PROCESS_WORKER, $i)); } + // 如果使用的是 LightCache,注册下自动保存的监听器 + if (is_a(config('global.kv.use', \LightCache::class), LightCache::class, true)) { + Framework::getInstance()->getDriver()->getEventLoop()->addTimer(config('global.kv.light_cache_autosave_time', 600) * 1000, [LightCache::class, 'saveAll'], 0); + } + // 注册 Worker 进程遇到退出时的回调,安全退出 register_shutdown_function(function () { $error = error_get_last(); @@ -104,9 +110,13 @@ class WorkerEventListener /** * @throws ZMKnownException + * @throws \JsonException */ public function onWorkerStop999(): void { + if (is_a(config('global.kv.use', \LightCache::class), LightCache::class, true)) { + LightCache::saveAll(); + } logger()->debug('Worker #' . ProcessManager::getProcessId() . ' stopping'); if (DIRECTORY_SEPARATOR !== '\\') { ProcessStateManager::removeProcessState(ZM_PROCESS_WORKER, ProcessManager::getProcessId()); diff --git a/src/ZM/Store/KV/KVInterface.php b/src/ZM/Store/KV/KVInterface.php new file mode 100644 index 00000000..038e1d15 --- /dev/null +++ b/src/ZM/Store/KV/KVInterface.php @@ -0,0 +1,47 @@ + 1) { + logger()->error('LightCache 不支持多进程模式,如需在多进程下使用,请使用 ZMRedis 作为 KV 引擎!'); + return; + } + $this->find_dir = empty($find_str) ? config('global.kv.light_cache_dir', '/tmp/zm_light_cache') : $find_str; + FileSystem::createDir($this->find_dir); + $this->validateKey($name); + if (file_exists($this->find_dir . '/' . $name . '.json')) { + $data = json_decode(file_get_contents($this->find_dir . '/' . $name . '.json'), true); + if (is_array($data)) { + self::$caches[$name] = $data['data']; + self::$ttys[$name] = $data['expire']; + } + } + } + + /** + * @throws InvalidArgumentException + */ + public static function open(string $name = ''): KVInterface + { + if (!isset(self::$objs[$name])) { + self::$objs[$name] = new LightCache($name); + } + return self::$objs[$name]; + } + + /** + * @throws \JsonException + */ + public static function saveAll() + { + /** @var LightCache $obj */ + foreach (self::$objs as $obj) { + $obj->save(); + } + logger()->debug('Saved all light caches'); + } + + /** + * 保存 KV 库的数据到文件 + * + * @throws \JsonException + */ + public function save(): void + { + file_put_contents(zm_dir($this->find_dir . '/' . $this->name . '.json'), json_encode([ + 'data' => self::$caches[$this->name] ?? [], + 'expire' => self::$ttys[$this->name] ?? [], + ], JSON_THROW_ON_ERROR)); + } + + /** + * 删除该 KV 库的所有数据,并且永远无法恢复 + */ + public function removeSelf(): bool + { + if (file_exists($this->find_dir . '/' . $this->name . '.json')) { + unlink($this->find_dir . '/' . $this->name . '.json'); + } + unset(self::$caches[$this->name], self::$ttys[$this->name], self::$objs[$this->name]); + return true; + } + + public function get(string $key, mixed $default = null): mixed + { + // 首先判断在不在缓存变量里 + if (!isset(self::$caches[$this->name][$key])) { + return $default; + } + // 然后判断是否有延迟 + if (isset(self::$ttys[$this->name][$key])) { + if (self::$ttys[$this->name][$key] > time()) { + return self::$caches[$this->name][$key]; + } + unset(self::$ttys[$this->name][$key], self::$caches[$this->name][$key]); + + return $default; + } + return self::$caches[$this->name][$key]; + } + + /** + * @throws InvalidArgumentException + */ + public function set(string $key, mixed $value, int $ttl = 0): bool + { + $this->validateKey($key); + self::$caches[$this->name][$key] = $value; + if ($ttl > 0) { + self::$ttys[$this->name][$key] = time() + $ttl; + } + return true; + } + + public function unset(string $key): bool + { + unset(self::$caches[$this->name][$key], self::$ttys[$this->name][$key]); + + return true; + } + + public function isset(string $key): bool + { + if (!isset(self::$caches[$this->name][$key])) { + return false; + } + if (isset(self::$ttys[$this->name][$key])) { + if (self::$ttys[$this->name][$key] > time()) { + return true; + } + unset(self::$ttys[$this->name][$key], self::$caches[$this->name][$key]); + + return false; + } + return true; + } + + private function validateKey(string $key): void + { + if ($key === '') { + return; + } + if (strlen($key) >= 128) { + throw new InvalidArgumentException('LightCache 键名长度不能超过 128 字节!'); + } + // 只能包含数字、大小写字母、下划线、短横线、点、中文 + if (!preg_match('/^[\w\-.\x{4e00}-\x{9fa5}]+$/u', $key)) { + throw new InvalidArgumentException('LightCache 键名只能包含数字、大小写字母、下划线、短横线、点、中文!'); + } + } +} diff --git a/tests/ZM/Store/KV/LightCacheTest.php b/tests/ZM/Store/KV/LightCacheTest.php new file mode 100644 index 00000000..98ee38a3 --- /dev/null +++ b/tests/ZM/Store/KV/LightCacheTest.php @@ -0,0 +1,47 @@ +assertInstanceOf(LightCache::class, $a); + /* @phpstan-ignore-next-line */ + $this->assertTrue($a->removeSelf()); + } + + public function testSet() + { + $this->assertTrue(LightCache::open()->set('test123', 'help')); + } + + public function testIsset() + { + $this->assertFalse(LightCache::open()->isset('test111')); + } + + public function testGet() + { + LightCache::open('ppp')->set('hello', 'world'); + $this->assertSame(LightCache::open('ppp')->get('hello', 'ffff'), 'world'); + } + + public function testUnset() + { + $kv = LightCache::open('sss'); + $kv->set('test', 'test'); + $this->assertSame($kv->get('test'), 'test'); + $kv->unset('test'); + $this->assertNull($kv->get('test')); + } +}