diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 77d9a8a..10c3161 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,6 +15,7 @@ jobs: - "8.3" - "8.2" - "8.1" + - "8.0" steps: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 diff --git a/README.md b/README.md index 3f4ae2c..b65d495 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,17 @@
- - - Latest Stable Version Latest Stable Version - PHP Version Require + PHP Version Require - GitHub license + GitHub license
@@ -121,6 +118,7 @@ - 支持 HSet/HGet/HDel/HKeys/HExists - 支持 HIncr/HDecr,支持浮点运算 - 支持 储存对象数据 + - 支持 HashKey的秒级过期时间【版本 ≥ 0.5】 - **通配符/正则匹配Search** ```php @@ -138,6 +136,8 @@ } ); ``` + **Tips:Cache::Search()本质上是个扫表匹配的过程,是O(N)的操作,如果需要对特定族群的数据进行监听,推荐使用Channel相关函数实现监听。** + - **原子性执行** ```php diff --git a/composer.json b/composer.json index 2dee76a..b159ced 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "source": "https://github.com/workbunny/webman-shared-cache" }, "require": { - "php": "^8.1", + "php": "^8.0", "ext-apcu": "*" }, "require-dev": { diff --git a/src/Cache.php b/src/Cache.php index 9a2d489..4d8ce9c 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -40,7 +40,7 @@ public static function __callStatic(string $name, array $arguments) throw new Error('PHP-ext apcu not enable. '); } if (!apcu_enabled()) { - throw new Error('You need run shared-cache-enable.sh. '); + throw new Error('You need run workbunny:shared-cache-enable/shared-cache-enable.sh command to enable APCu. '); } return call_user_func([self::class, "_$name"], ...$arguments); } diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index c9e2133..9da0291 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -7,18 +7,52 @@ abstract class AbstractCommand extends Command { + /** + * 兼容webman console,需重写 + * + * @var string + */ + protected static string $defaultName = ''; + /** + * 兼容webman console,需重写 + * + * @var string + */ + protected static string $defaultDescription = ''; + + /** + * 输出info + * + * @param OutputInterface $output + * @param string $message + * @return void + */ protected function info(OutputInterface $output, string $message): void { $output->writeln("ℹ️ $message"); } + /** + * 输出error + * + * @param OutputInterface $output + * @param string $message + * @return int + */ protected function error(OutputInterface $output, string $message): int { $output->writeln("❌ $message"); return self::FAILURE; } + /** + * 输出success + * + * @param OutputInterface $output + * @param string $message + * @return int + */ protected function success(OutputInterface $output, string $message): int { $output->writeln("✅ $message"); diff --git a/src/Commands/WorkbunnyWebmanSharedCacheClean.php b/src/Commands/WorkbunnyWebmanSharedCacheClean.php index ea5f1e3..a521b95 100644 --- a/src/Commands/WorkbunnyWebmanSharedCacheClean.php +++ b/src/Commands/WorkbunnyWebmanSharedCacheClean.php @@ -10,13 +10,15 @@ class WorkbunnyWebmanSharedCacheClean extends AbstractCommand { + protected static string $defaultName = 'workbunny:shared-cache-clean'; + protected static string $defaultDescription = 'Remove all workbunny/webman-shared-cache caches. '; + /** * @return void */ protected function configure(): void { - $this->setName('workbunny:shared-cache-clean') - ->setDescription('Remove all workbunny/webman-shared-cache caches. '); + $this->setName(static::$defaultName)->setDescription(static::$defaultDescription); } /** diff --git a/src/Commands/WorkbunnyWebmanSharedCacheEnable.php b/src/Commands/WorkbunnyWebmanSharedCacheEnable.php new file mode 100644 index 0000000..1c81871 --- /dev/null +++ b/src/Commands/WorkbunnyWebmanSharedCacheEnable.php @@ -0,0 +1,72 @@ +setDescription('Enable APCu cache with specified settings.') + ->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'Specify configuration name', 'apcu-cache.ini') + ->addOption('target', 't', InputOption::VALUE_REQUIRED, 'Specify target location', '/usr/local/etc/php/conf.d') + ->addOption('size', 'si', InputOption::VALUE_REQUIRED, 'Configure apcu.shm_size', '1024M') + ->addOption('segments', 'se', InputOption::VALUE_REQUIRED, 'Configure apcu.shm_segments', 1) + ->addOption('mmap', 'm', InputOption::VALUE_REQUIRED, 'Configure apcu.mmap_file_mask', '') + ->addOption('gc_ttl', 'gc', InputOption::VALUE_REQUIRED, 'Configure apcu.gc_ttl', 3600); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $fileName = $input->getOption('file'); + $target = $input->getOption('target'); + $shmSize = $input->getOption('size'); + $shmSegments = $input->getOption('segments'); + $mmapFileMask = $input->getOption('mmap'); + $gcTtl = $input->getOption('gc_ttl'); + + if (!is_dir($target)) { + return $this->error($output, "Target directory does not exist: $target. "); + } + $configContent = <<getHelper('question'); + $question = new ConfirmationQuestion("Configuration file already exists at $filePath. Overwrite? (y/N) ", false); + + if (!$helper->ask($input, $output, $question)) { + return $this->success($output, "Operation aborted. "); + } + } + + file_put_contents($filePath, $configContent); + return $this->success($output, "Configuration file created at: $filePath. "); + } + +} diff --git a/src/Commands/WorkbunnyWebmanSharedCacheHRecycle.php b/src/Commands/WorkbunnyWebmanSharedCacheHRecycle.php new file mode 100644 index 0000000..11c72cd --- /dev/null +++ b/src/Commands/WorkbunnyWebmanSharedCacheHRecycle.php @@ -0,0 +1,48 @@ +setName(static::$defaultName)->setDescription(static::$defaultDescription); + $this->addOption('key', 'k', InputOption::VALUE_OPTIONAL, 'Cache Key. '); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $key = $input->getOption('key'); + if ($key) { + Cache::HRecycle($key); + } else { + $progressBar = new ProgressBar($output); + $progressBar->start(); + $keys = Cache::Keys(); + $progressBar->setMaxSteps(count($keys)); + foreach ($keys as $key) { + Cache::HRecycle($key); + $progressBar->advance(); + } + $progressBar->finish(); + } + return $this->success($output, 'HRecycle Success. '); + } +} diff --git a/src/Commands/WorkbunnyWebmanSharedCacheList.php b/src/Commands/WorkbunnyWebmanSharedCacheList.php deleted file mode 100644 index 413e918..0000000 --- a/src/Commands/WorkbunnyWebmanSharedCacheList.php +++ /dev/null @@ -1,44 +0,0 @@ -setName('workbunny:shared-cache-list') - ->setDescription('Show workbunny/webman-shared-cache caches list. '); - - $this->addOption('page', 'p', InputOption::VALUE_OPTIONAL, 'Page. ', 1); - $this->addOption('size', 's', InputOption::VALUE_OPTIONAL, 'Page size. ', 20); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * @return int - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $page = $input->getOption('page'); - $size = $input->getOption('size'); - $headers = ['name', 'value']; - $rows = []; - // todo - - $table = new Table($output); - $table->setHeaders($headers); - $table->setRows($rows); - $table->render(); - - return self::SUCCESS; - } -} diff --git a/src/Future.php b/src/Future.php index eadfcef..8303883 100644 --- a/src/Future.php +++ b/src/Future.php @@ -22,9 +22,10 @@ class Future /** * @param Closure $func * @param array $args + * @param float|int|null $interval * @return int|false */ - public static function add(Closure $func, array $args = []): int|false + public static function add(Closure $func, array $args = [], float|int|null $interval = null): int|false { if (self::$debug) { self::$debugFunc = $func; diff --git a/src/Traits/ChannelMethods.php b/src/Traits/ChannelMethods.php index 2e27c19..677e734 100644 --- a/src/Traits/ChannelMethods.php +++ b/src/Traits/ChannelMethods.php @@ -21,10 +21,24 @@ trait ChannelMethods protected static string $_CHANNEL = '#Channel#'; /** - * @var array = [channelKey => listeners] + * @var array = [channelKey => futureId] */ protected static array $_listeners = []; + /** + * @var float|int|null + */ + protected static float|int|null $interval = null; + + /** + * @param float|int|null $interval + * @return void + */ + public static function SetChannelListenerInterval(float|int|null $interval): void + { + self::$interval = $interval; + } + /** * @param string $key * @return string @@ -66,7 +80,7 @@ protected static function _ChPublish(string $key, mixed $message, bool $store = $func = __FUNCTION__; $params = func_get_args(); self::_Atomic($key, function () use ( - $key, $message, $func, $params, $store + $key, $message, $func, $params, $store, $workerId ) { /** * [ @@ -80,13 +94,30 @@ protected static function _ChPublish(string $key, mixed $message, bool $store = // 如果还没有监听器,将数据投入默认 if (!$channel) { if ($store) { - $channel['--default--']['value'][] = $message; + // 非指定workerId + if ($workerId === null) { + $channel['--default--']['value'][] = $message; + } + // 指定workerId + else { + $channel[$workerId]['value'][] = $message; + } + } } // 否则将消息投入到每个worker的监听器数据中 else { - foreach ($channel as $workerId => $item) { - if ($store or isset($item['futureId'])) { + // 非指定workerId + if ($workerId === null) { + foreach ($channel as $workerId => $item) { + if ($store or isset($item['futureId'])) { + $channel[$workerId]['value'][] = $message; + } + } + } + // 指定workerId + else { + if ($store or isset($channel[$workerId]['futureId'])) { $channel[$workerId]['value'][] = $message; } } @@ -123,7 +154,7 @@ protected static function _ChCreateListener(string $key, string|int $workerId, C throw new Error("Channel $key listener already exist. "); } self::_Atomic($key, function () use ( - $key, $workerId, $func, $params, &$result + $key, $workerId, $func, $params, $listener, &$result ) { /** * [ @@ -138,20 +169,20 @@ protected static function _ChCreateListener(string $key, string|int $workerId, C // 设置回调 $channel[$workerId]['futureId'] = self::$_listeners[$key] = - $result = Future::add(function () use ($key, $workerId) { + $result = Future::add(function () use ($key, $workerId, $listener) { // 原子性执行 - self::_Atomic($key, function () use ($key, $workerId) { + self::_Atomic($key, function () use ($key, $workerId, $listener) { $channel = self::_Get($channelName = self::GetChannelKey($key), []); if ((!empty($value = $channel[$workerId]['value'] ?? []))) { // 先进先出 $msg = array_shift($value); $channel[$workerId]['value'] = $value; - call_user_func(self::$_listeners[$key], $key, $workerId, $msg); + call_user_func($listener, $key, $workerId, $msg); self::_Set($channelName, $channel); } }); - }); + }, interval: self::$interval); $channel[$workerId]['value'] = []; // 如果存在默认数据 if ($default = $channel['--default--']['value'] ?? []) { @@ -202,7 +233,6 @@ protected static function _ChRemoveListener(string $key, string|int $workerId, b self::_Set($channelName, $channel); } unset(self::$_listeners[$key]); - } return [ diff --git a/src/Traits/HashMethods.php b/src/Traits/HashMethods.php index ebed670..aba6970 100644 --- a/src/Traits/HashMethods.php +++ b/src/Traits/HashMethods.php @@ -10,6 +10,7 @@ * @method static bool|int|float HIncr(string $key, string|int $hashKey, int|float $value = 1) Hash 自增 * @method static bool|int|float HDecr(string $key, string|int $hashKey, int|float $value = 1) Hash 自减 * @method static array HExists(string $key, string|int ...$hashKey) Hash key 判断 + * @method static void HRecycle(string $key) Hash key 过期回收 */ trait HashMethods { @@ -23,17 +24,22 @@ trait HashMethods * @param string $key * @param string|int $hashKey * @param mixed $hashValue + * @param int $ttl * @return bool */ - protected static function _HSet(string $key, string|int $hashKey, mixed $hashValue): bool + protected static function _HSet(string $key, string|int $hashKey, mixed $hashValue, int $ttl = 0): bool { $func = __FUNCTION__; $params = func_get_args(); self::_Atomic($key, function () use ( - $key, $hashKey, $hashValue, $func, $params + $key, $hashKey, $hashValue, $ttl, $func, $params ) { $hash = self::_Get($key, []); - $hash[$hashKey] = $hashValue; + $hash[$hashKey] = [ + '_value' => $hashValue, + '_ttl' => $ttl, + '_timestamp' => time() + ]; self::_Set($key, $hash); return [ 'timestamp' => microtime(true), @@ -51,19 +57,29 @@ protected static function _HSet(string $key, string|int $hashKey, mixed $hashVal * @param string $key * @param string|int $hashKey * @param int|float $hashValue + * @param int $ttl * @return bool|int|float */ - protected static function _HIncr(string $key, string|int $hashKey, int|float $hashValue = 1): bool|int|float + protected static function _HIncr(string $key, string|int $hashKey, int|float $hashValue = 1, int $ttl = 0): bool|int|float { $func = __FUNCTION__; $result = false; $params = func_get_args(); self::_Atomic($key, function () use ( - $key, $hashKey, $hashValue, $func, $params, &$result + $key, $hashKey, $hashValue, $ttl, $func, $params, &$result ) { $hash = self::_Get($key, []); - if (is_numeric($v = ($hash[$hashKey] ?? 0))) { - $hash[$hashKey] = $result = $v + $hashValue; + $value = $hash[$hashKey]['_value'] ?? 0; + $oldTtl = $hash[$hashKey]['_ttl'] ?? 0; + $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; + if (is_numeric($value)) { + $now = time(); + $value = ($oldTtl <= 0 or (($timestamp + $oldTtl) >= $now)) ? $value : 0; + $hash[$hashKey] = [ + '_value' => $result = $value + $hashValue, + '_ttl' => ($ttl > 0) ? $ttl : ($timestamp > 0 ? $now - $timestamp : 0), + '_timestamp' => $now, + ]; self::_Set($key, $hash); } return [ @@ -82,19 +98,29 @@ protected static function _HIncr(string $key, string|int $hashKey, int|float $ha * @param string $key * @param string|int $hashKey * @param int|float $hashValue + * @param int $ttl * @return bool|int|float */ - protected static function _HDecr(string $key, string|int $hashKey, int|float $hashValue = 1): bool|int|float + protected static function _HDecr(string $key, string|int $hashKey, int|float $hashValue = 1, int $ttl = 0): bool|int|float { $func = __FUNCTION__; $result = false; $params = func_get_args(); self::_Atomic($key, function () use ( - $key, $hashKey, $hashValue, $func, $params, &$result + $key, $hashKey, $hashValue, $ttl, $func, $params, &$result ) { $hash = self::_Get($key, []); - if (is_numeric($v = ($hash[$hashKey] ?? 0))) { - $hash[$hashKey] = $result = $v - $hashValue; + $value = $hash[$hashKey]['_value'] ?? 0; + $oldTtl = $hash[$hashKey]['_ttl'] ?? 0; + $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; + if (is_numeric($value)) { + $now = time(); + $value = ($oldTtl <= 0 or (($timestamp + $oldTtl) >= $now)) ? $value : 0; + $hash[$hashKey] = [ + '_value' => $result = $value - $hashValue, + '_ttl' => ($ttl > 0) ? $ttl : ($timestamp > 0 ? $now - $timestamp : 0), + '_timestamp' => $now, + ]; self::_Set($key, $hash); } return [ @@ -148,8 +174,50 @@ protected static function _HDel(string $key, string|int ...$hashKey): bool */ protected static function _HGet(string $key, string|int $hashKey, mixed $default = null): mixed { + $now = time(); $hash = self::_Get($key, []); - return $hash[$hashKey] ?? $default; + $value = $hash[$hashKey]['_value'] ?? $default; + $ttl = $hash[$hashKey]['_ttl'] ?? 0; + $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; + return ($ttl <= 0 or (($timestamp + $ttl) >= $now)) ? $value : $default; + } + + /** + * 回收过期 hashKey + * + * @param string $key + * @return void + */ + protected static function _HRecycle(string $key): void + { + $func = __FUNCTION__; + $params = func_get_args(); + self::_Atomic($key, function () use ( + $key, $func, $params + ) { + $hash = self::_Get($key, []); + if (isset($hash['_ttl']) and isset($hash['_timestamp'])) { + $now = time(); + $set = false; + foreach ($hash as $hashKey => $hashValue) { + $ttl = $hashValue['_ttl'] ?? 0; + $timestamp = $hashValue['_timestamp'] ?? 0; + if ($ttl > 0 and $timestamp > 0 and $timestamp + $ttl < $now) { + $set = true; + unset($hash[$hashKey]); + } + } + if ($set) { + self::_Set($key, $hash); + } + } + return [ + 'timestamp' => microtime(true), + 'method' => $func, + 'params' => $params, + 'result' => null + ]; + }, true); } /** @@ -163,8 +231,11 @@ protected static function _HExists(string $key, string|int ...$hashKey): array { $hash = self::_Get($key, []); $result = []; + $now = time(); foreach ($hashKey as $hk) { - if (isset($hash[$hk])) { + $ttl = $hash[$hk]['_ttl'] ?? 0; + $timestamp = $hash[$hk]['_timestamp'] ?? 0; + if (($ttl <= 0 or (($timestamp + $ttl) >= $now)) and isset($hash[$hk]['_value'])) { $result[$hk] = true; } } @@ -181,11 +252,17 @@ protected static function _HExists(string $key, string|int ...$hashKey): array protected static function _HKeys(string $key, null|string $regex = null): array { $hash = self::_Get($key, []); - $keys = array_keys($hash); - if ($regex !== null) { - $keys = array_values(array_filter($keys, function($key) use ($regex) { - return preg_match($regex, $key); - })); + $keys = []; + $now = time(); + foreach ($hash as $hashKey => $hashValue) { + $ttl = $hashValue['_ttl'] ?? 0; + $timestamp = $hashValue['_timestamp'] ?? 0; + if (($ttl <= 0 or (($timestamp + $ttl) >= $now)) and isset($hashValue['_value'])) { + if ($regex !== null and preg_match($regex, $key)) { + continue; + } + $keys[] = $hashKey; + } } return $keys; } diff --git a/src/config/plugin/workbunny/webman-shared-cache/command.php b/src/config/plugin/workbunny/webman-shared-cache/command.php new file mode 100644 index 0000000..aff331a --- /dev/null +++ b/src/config/plugin/workbunny/webman-shared-cache/command.php @@ -0,0 +1,18 @@ + + * @copyright chaz6chez + * @link https://github.com/workbunny/webman-push-server + * @license https://github.com/workbunny/webman-push-server/blob/main/LICENSE + */ +declare(strict_types=1); + +return [ + Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheEnable::class, + Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheClean::class, + Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheHRecycle::class +]; diff --git a/tests/HashTest.php b/tests/HashTest.php index d04126a..639fda8 100644 --- a/tests/HashTest.php +++ b/tests/HashTest.php @@ -13,7 +13,11 @@ public function testHashGet(): void // 单进程执行 $this->assertEquals(null, Cache::HGet($key, $hash)); apcu_add($key, [ - $hash => $hash + $hash => [ + '_value' => $hash, + '_ttl' => 0, + '_timestamp' => time() + ] ]); $this->assertEquals([], Cache::LockInfo()); $this->assertEquals($hash, Cache::HGet($key, $hash)); @@ -24,7 +28,11 @@ public function testHashGet(): void $this->assertEquals(null, Cache::HGet($key, $hash)); $this->childExec(static function (string $key, string $hash) { apcu_add($key, [ - $hash => $hash + $hash => [ + '_value' => $hash, + '_ttl' => 0, + '_timestamp' => time() + ] ]); }, $key, $hash); $this->assertEquals([], Cache::LockInfo()); @@ -42,7 +50,11 @@ public function testHashSet(): void $this->assertTrue(Cache::HSet($key, $hash, $hash)); $this->assertEquals([], Cache::LockInfo()); $this->assertEquals([ - $hash => $hash + $hash => [ + '_value' => $hash, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key); @@ -54,7 +66,11 @@ public function testHashSet(): void }, $key, $hash); $this->assertEquals([], Cache::LockInfo()); $this->assertEquals([ - $hash => $hash + $hash => [ + '_value' => $hash, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key); @@ -98,8 +114,12 @@ public function testHashExists(): void $this->assertEquals([], Cache::HExists($key, 'a')); apcu_add($key, [ - 'a' => 1, - 'b' => 2 + 'a' => [ + '_value' => 1 + ], + 'b' => [ + '_value' => 2 + ] ]); $this->assertEquals([ 'a' => true, 'b' => true @@ -116,15 +136,27 @@ public function testHashIncr(): void $this->assertFalse(apcu_fetch($key)); $this->assertEquals(1, Cache::HIncr($key, 'a')); $this->assertEquals([ - 'a' => 1 + 'a' => [ + '_value' => 1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->assertEquals(3, Cache::HIncr($key, 'a', 2)); $this->assertEquals([ - 'a' => 3 + 'a' => [ + '_value' => 3, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->assertEquals(4.1, Cache::HIncr($key, 'a', 1.1)); $this->assertEquals([ - 'a' => 4.1 + 'a' => [ + '_value' => 4.1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key); @@ -135,19 +167,31 @@ public function testHashIncr(): void Cache::HIncr($key, 'a'); }, $key); $this->assertEquals([ - 'a' => 1 + 'a' => [ + '_value' => 1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->childExec(static function (string $key) { Cache::HIncr($key, 'a',2); }, $key); $this->assertEquals([ - 'a' => 3 + 'a' => [ + '_value' => 3, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->childExec(static function (string $key) { Cache::HIncr($key, 'a',1.1); }, $key); $this->assertEquals([ - 'a' => 4.1 + 'a' => [ + '_value' => 4.1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key); @@ -160,15 +204,27 @@ public function testHashDecr(): void $this->assertFalse(apcu_fetch($key)); $this->assertEquals(-1, Cache::HDecr($key, 'a')); $this->assertEquals([ - 'a' => -1 + 'a' => [ + '_value' => -1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->assertEquals(-3, Cache::HDecr($key, 'a', 2)); $this->assertEquals([ - 'a' => -3 + 'a' => [ + '_value' => -3, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->assertEquals(-4.1, Cache::HDecr($key, 'a', 1.1)); $this->assertEquals([ - 'a' => -4.1 + 'a' => [ + '_value' => -4.1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key); @@ -179,19 +235,31 @@ public function testHashDecr(): void Cache::HDecr($key, 'a'); }, $key); $this->assertEquals([ - 'a' => -1 + 'a' => [ + '_value' => -1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->childExec(static function (string $key) { Cache::HDecr($key, 'a', 2); }, $key); $this->assertEquals([ - 'a' => -3 + 'a' => [ + '_value' => -3, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); $this->childExec(static function (string $key) { Cache::HDecr($key, 'a', 1.1); }, $key); $this->assertEquals([ - 'a' => -4.1 + 'a' => [ + '_value' => -4.1, + '_ttl' => 0, + '_timestamp' => time() + ] ], apcu_fetch($key)); // 清理 apcu_delete($key);