mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
add LightCache support
This commit is contained in:
parent
3734f5d476
commit
fd8b3721ae
@ -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;
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
47
src/ZM/Store/KV/KVInterface.php
Normal file
47
src/ZM/Store/KV/KVInterface.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
interface KVInterface
|
||||
{
|
||||
/**
|
||||
* 打开一个 KV 库
|
||||
*
|
||||
* @param string $name KV 的库名称
|
||||
*/
|
||||
public static function open(string $name = ''): KVInterface;
|
||||
|
||||
/**
|
||||
* 返回一个 KV 键值对的数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param null|mixed $default 如果不存在时返回的默认值
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* 设置一个 KV 键值对的数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 键值
|
||||
* @param int $ttl 超时秒数(如果等于 0 代表永不超时)
|
||||
*/
|
||||
public function set(string $key, mixed $value, int $ttl = 0): bool;
|
||||
|
||||
/**
|
||||
* 强制删除一个 KV 键值对数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @return bool 当键存在并被删除时返回 true
|
||||
*/
|
||||
public function unset(string $key): bool;
|
||||
|
||||
/**
|
||||
* 键值对数据是否存在
|
||||
*
|
||||
* @param string $key 键名
|
||||
*/
|
||||
public function isset(string $key): bool;
|
||||
}
|
||||
164
src/ZM/Store/KV/LightCache.php
Normal file
164
src/ZM/Store/KV/LightCache.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
/**
|
||||
* 轻量、基于本地 JSON 文件的 KV 键值对缓存
|
||||
*/
|
||||
class LightCache implements KVInterface
|
||||
{
|
||||
/** @var array 存放库对象的列表 */
|
||||
private static array $objs = [];
|
||||
|
||||
/** @var array 存放缓存数据的列表 */
|
||||
private static array $caches = [];
|
||||
|
||||
/** @var array 存放超时数据的列表 */
|
||||
private static array $ttys = [];
|
||||
|
||||
/** @var string 查找库的目录地址 */
|
||||
private string $find_dir;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(private string $name = '', string $find_str = '')
|
||||
{
|
||||
if ((ProcessStateManager::$process_mode['worker'] ?? 0) > 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 键名只能包含数字、大小写字母、下划线、短横线、点、中文!');
|
||||
}
|
||||
}
|
||||
}
|
||||
47
tests/ZM/Store/KV/LightCacheTest.php
Normal file
47
tests/ZM/Store/KV/LightCacheTest.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\ZM\Store\KV;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ZM\Store\KV\LightCache;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LightCacheTest extends TestCase
|
||||
{
|
||||
public function testRemoveSelf()
|
||||
{
|
||||
$a = LightCache::open('asd');
|
||||
$this->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'));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user