├── .github └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Cache.php ├── Commands │ ├── AbstractCommand.php │ ├── WorkbunnyWebmanSharedCacheClean.php │ ├── WorkbunnyWebmanSharedCacheEnable.php │ └── WorkbunnyWebmanSharedCacheHRecycle.php ├── Future.php ├── Install.php ├── RateLimiter.php ├── Traits │ ├── BasicMethods.php │ ├── ChannelMethods.php │ └── HashMethods.php └── config │ └── plugin │ └── workbunny │ └── webman-shared-cache │ ├── app.php │ ├── command.php │ ├── rate-limit.php │ └── shared-cache-enable.sh └── tests ├── BaseTestCase.php ├── CacheTest.php ├── ChannelTest.php ├── GenericTest.php ├── HashTest.php └── simple-benchmark.php /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | pull_request: 6 | branches: [ "main" ] 7 | 8 | jobs: 9 | PHPUnit: 10 | name: PHPUnit (PHP ${{ matrix.php }}) 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: 15 | - "8.4" 16 | - "8.3" 17 | - "8.2" 18 | - "8.1" 19 | - "8.0" 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | extensions: apcu 26 | tools: phpunit:9, composer:v2 27 | coverage: none 28 | ini-values: apc.enabled=1, apc.enable_cli=1, apc.shm_segments=1, apc.shm_size=256M 29 | - run: composer install 30 | - run: vendor/bin/phpunit 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.git 3 | /.vscode 4 | /vendor 5 | composer.lock 6 | .phpunit.result.cache 7 | test*.php -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 workbunny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

workbunny

2 | 3 | **

workbunny/webman-shared-cache

** 4 | 5 | **

🐇 A lightweight shared cache for webman plugin. 🐇

** 6 | 7 | # A lightweight shared cache for webman plugin 8 | 9 | 10 |
11 | 12 | Build Status 13 | 14 | 15 | Latest Stable Version 16 | 17 | 18 | PHP Version Require 19 | 20 | 21 | GitHub license 22 | 23 | 24 |
25 | 26 | ## 常见问题 27 | 28 | ### 1. 它与 Redis/Memcache 的区别 29 | 30 | - shared-cache是基于APCu的本地缓存,它的底层是带有锁的MMAP共享内存; 31 | - Redis和Memcache本质上是“分布式”缓存系统/K-V数据库,存在网络IO; 32 | - shared-cache没有持久化,同时也无法实现“分布式”,仅可用于本地的多进程环境(进程需要有亲缘关系); 33 | - shared-cache是μs级别的缓存,redis是ms级别的缓存; 34 | - 网络IO存在内核态和用户态的多次拷贝,存在较大的延迟,共享内存不存在这样的问题; 35 | 36 | ### 2. 它的使用场景 37 | 38 | - 可以用作一些服务器的本地缓存,如页面缓存、L2-cache; 39 | - 可以跨进程做一些计算工作,也可以跨进程通讯; 40 | - 用在一些延迟敏感的服务下,如游戏服务器; 41 | - 简单的限流插件; 42 | 43 | ### 3. 与redis简单的比较 44 | - 运行/tests/simple-benchmark.php 45 | - redis使用host.docker.internal 46 | - 在循环中增加不同的间隔,模拟真实使用场景 47 | - 结果如下: 48 | ```shell 49 | 1^ "count: 100000" 50 | 2^ "interval: 0 μs" 51 | ^ "redis: 73.606367111206" 52 | ^ "cache: 0.081215143203735" 53 | ^ "-----------------------------------" 54 | 1^ "count: 100000" 55 | 2^ "interval: 1 μs" 56 | ^ "redis: 78.833391904831" 57 | ^ "cache: 6.4423549175262" 58 | ^ "-----------------------------------" 59 | 1^ "count: 100000" 60 | 2^ "interval: 10 μs" 61 | ^ "redis: 79.543494939804" 62 | ^ "cache: 7.2690420150757" 63 | ^ "-----------------------------------" 64 | 1^ "count: 100000" 65 | 2^ "interval: 100 μs" 66 | ^ "redis: 88.58958697319" 67 | ^ "cache: 17.31387090683" 68 | ^ "-----------------------------------" 69 | 1^ "count: 100000" 70 | 2^ "interval: 1000 μs" 71 | ^ "redis: 183.2620780468" 72 | ^ "cache: 112.18278503418" 73 | ^ "-----------------------------------" 74 | ``` 75 | 76 | ## 简介 77 | 78 | - 基于APCu拓展的轻量级高速缓存,读写微秒级; 79 | - 支持具备亲缘关系的多进程内存共享; 80 | - 支持具备亲缘关系的多进程限流; 81 | 82 | ## 安装 83 | 84 | 1. **自行安装APCu拓展** 85 | ```shell 86 | # 1. pecl安装 87 | pecl instanll apcu 88 | # 2. docker中请使用安装器安装 89 | curl -sSL https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions -o - | sh -s apcu 90 | ``` 91 | 2. 安装composer包 92 | ```shell 93 | composer require workbunny/webman-shared-cache 94 | ``` 95 | 3. 使用命令进行php.ini的配置 96 | - 进入 **/config/plugin/workbunny/webman-shared-cache** 目录 97 | - 运行 98 | ```shell 99 | # 帮助信息 100 | sh ./shared-cache-enable.sh --help 101 | # or 102 | bash ./shared-cache-enable.sh --help 103 | ``` 104 | 105 | ## 使用 106 | 107 | #### 注:\Workbunny\WebmanSharedCache\Cache::$fuse为全局阻塞保险 108 | 109 | ### 1. Cache基础使用 110 | 111 | - **类似Redis的String【使用方法与Redis基本一致】** 112 | - 支持 Set/Get/Del/Keys/Exists 113 | - 支持 Incr/Decr,支持浮点运算 114 | - 支持 储存对象数据 115 | - 支持 XX/NX模式,支持秒级过期时间 116 | 117 | - **类似Redis的Hash【使用方法与Redis基本一致】** 118 | - 支持 HSet/HGet/HDel/HKeys/HExists 119 | - 支持 HIncr/HDecr,支持浮点运算 120 | - 支持 储存对象数据 121 | - 支持 HashKey的秒级过期时间【版本 ≥ 0.5】 122 | 123 | - **通配符/正则匹配Search** 124 | ```php 125 | $result = []; 126 | # 默认正则匹配 - 以50条为一次分片查询 127 | \Workbunny\WebmanSharedCache\Cache::Search('/^abc.+$/', function ($key, $value) use (&$result) { 128 | $result[$key] = $value; 129 | }, 50); 130 | 131 | # 通配符转正则 132 | \Workbunny\WebmanSharedCache\Cache::Search( 133 | \Workbunny\WebmanSharedCache\Cache::WildcardToRegex('abc*'), 134 | function ($key, $value) use (&$result) { 135 | $result[$key] = $value; 136 | } 137 | ); 138 | ``` 139 | **Tips:Cache::Search()本质上是个扫表匹配的过程,是O(N)的操作,如果需要对特定族群的数据进行监听,推荐使用Channel相关函数实现监听。** 140 | 141 | 142 | - **原子性执行** 143 | ```php 144 | # key-1、key-2、key-3会被当作一次原子性操作 145 | 146 | # 非阻塞执行 - 成功执行则返回true,失败返回false,锁冲突会导致执行失败 147 | $result = \Workbunny\WebmanSharedCache\Cache::Atomic('lock-test', function () { 148 | \Workbunny\WebmanSharedCache\Cache::Set('key-1', 1); 149 | \Workbunny\WebmanSharedCache\Cache::Set('key-2', 2); 150 | \Workbunny\WebmanSharedCache\Cache::Set('key-3', 3); 151 | }); 152 | # 阻塞等待执行 - 默认阻塞受Cache::$fuse阻塞保险影响 153 | $result = \Workbunny\WebmanSharedCache\Cache::Atomic('lock-test', function () { 154 | \Workbunny\WebmanSharedCache\Cache::Set('key-1', 1); 155 | \Workbunny\WebmanSharedCache\Cache::Set('key-2', 2); 156 | \Workbunny\WebmanSharedCache\Cache::Set('key-3', 3); 157 | }, true); 158 | # 自行实现阻塞 159 | $result = false 160 | while (!$result) { 161 | # TODO 可以适当增加保险,以免超长阻塞 162 | $result = \Workbunny\WebmanSharedCache\Cache::Atomic('lock-test', function () { 163 | \Workbunny\WebmanSharedCache\Cache::Set('key-1', 1); 164 | \Workbunny\WebmanSharedCache\Cache::Set('key-2', 2); 165 | \Workbunny\WebmanSharedCache\Cache::Set('key-3', 3); 166 | }); 167 | } 168 | ``` 169 | 170 | - **查看cache信息** 171 | ```php 172 | # 全量数据 173 | var_dump(\Workbunny\WebmanSharedCache\Cache::Info()); 174 | 175 | # 不查询数据 176 | var_dump(\Workbunny\WebmanSharedCache\Cache::Info(true)); 177 | ``` 178 | 179 | - **查看锁信息** 180 | ```php 181 | # Hash数据的处理建立在写锁之上,如需调试,则使用该方法查询锁信息 182 | var_dump(\Workbunny\WebmanSharedCache\Cache::LockInfo()); 183 | ``` 184 | 185 | - **查看键信息** 186 | ```php 187 | # 包括键的一些基础信息 188 | var_dump(\Workbunny\WebmanSharedCache\Cache::KeyInfo('test-key')); 189 | ``` 190 | 191 | - **清空cache** 192 | - 使用Del多参数进行清理 193 | ```php 194 | # 接受多个参数 195 | \Workbunny\WebmanSharedCache\Cache::Del($a, $b, $c, $d); 196 | # 接受一个key的数组 197 | \Workbunny\WebmanSharedCache\Cache::Del(...$keysArray); 198 | ``` 199 | - 使用Clear进行清理 200 | ```php 201 | \Workbunny\WebmanSharedCache\Cache::Clear(); 202 | ``` 203 | 204 | ### 2. RateLimiter插件 205 | 206 | > 高效轻量的亲缘进程限流器 207 | 208 | 1. 在/config/plugin/workbbunny/webman-shared-cache/rate-limit.php中配置 209 | 2. 在使用的位置调用 210 | - 当没有执行限流时,返回空数组 211 | - 当执行但没有到达限流时,返回数组is_limit为false 212 | - 当执行且到达限流时,返回数组is_limit为true 213 | ```php 214 | $rate = \Workbunny\WebmanSharedCache\RateLimiter::traffic('test'); 215 | if ($rate['is_limit'] ?? false) { 216 | // 限流逻辑 如可以抛出异常、返回错误信息等 217 | return new \support\Response(429, [ 218 | 'X-Rate-Reset' => $rate['reset'], 219 | 'X-Rate-Limit' => $rate['limit'], 220 | 'X-Rate-Remaining' => $rate['reset'] 221 | ]) 222 | } 223 | ``` 224 | 225 | ### 3. Cache的Channel功能 226 | 227 | - Channel是一个类似Redis-stream、Redis-list、Redis-Pub/Sub的功能模块 228 | - 一个通道可以被多个进程监听,每个进程只能监听一个相同通道(也就是对相同通道只能创建一个监听器) 229 | 230 | - **向通道发布消息** 231 | - 临时消息 232 | ```php 233 | # 向一个名为test的通道发送临时消息; 234 | # 通道没有监听器时,临时消息会被忽略,只有通道存在监听器时,该消息才会被存入通道 235 | Cache::ChPublish('test', '这是一个测试消息', false); 236 | ``` 237 | - 暂存消息 238 | ```php 239 | # 向一个名为test的通道发送暂存消息; 240 | # 通道存在监听器时,该消息会被存入通道内的所有子通道 241 | Cache::ChPublish('test', '这是一个测试消息', true); 242 | ``` 243 | - 指定workerId 244 | ```php 245 | # 指定发送消息至当前通道内workerId为1的子通道 246 | Cache::ChPublish('test', '这是一个测试消息', true, 1); 247 | ``` 248 | 249 | - **创建通道监听器** 250 | - 一个进程对相同通道仅能创建一个监听器 251 | - 一个进程可以同时监听多个不同的通道 252 | - 建议workerId使用workerman的workerId进行区分 253 | ```php 254 | # 向一个名为test的通道创建一个workerId为1的监听器; 255 | # 通道消息先进先出,当有消息时会触发回调 256 | Cache::ChCreateListener('test', '1', function(string $channelKey, string|int $workerId, mixed $message) { 257 | // TODO 你的业务逻辑 258 | dump($channelKey, $workerId, $message); 259 | }); 260 | ``` 261 | 262 | - **移除通道监听器** 263 | - 移除监听器子通道及子通道内消息 264 | ```php 265 | # 向一个名为test的通道创建一个workerId为1的监听器; 266 | # 通道移除时不会移除其他子通道消息 267 | Cache::ChRemoveListener('test', '1', true); 268 | ``` 269 | - 移除监听器子通道,但保留子通道内消息 270 | ```php 271 | # 向一个名为test的通道创建一个workerId为1的监听器; 272 | # 通道移除时不会移除所有子通道消息 273 | Cache::ChRemoveListener('test', '1', false); 274 | ``` 275 | 276 | - **实验性功能:信号通知** 277 | - 由于共享内存无法使用事件监听,所以底层使用Timer定时器进行轮询,实验性功能可以开启使用系统信号来监听数据的变化 278 | ```php 279 | // 设置信号 280 | // 因为event等事件循环库是对标准信号的监听,所以不能使用自定实时信号SIGRTMIN ~ SIGRTMAX 281 | // 默认暂时使用SIGPOLL,异步IO监听信号,可能影响异步文件IO相关的触发 282 | Future::$signal = \SIGPOLL; 283 | // 开启信号监听,这时候开启的监听会触发之前的回调和通道回调,不会影响之前的回调 284 | Cache::channelUseSignalEnable(true) 285 | ``` 286 | - 当使用的监听信号存在已注册的回调产生回调冲突时,可以手动设置回调事件共享 287 | ```php 288 | // 设置信号 289 | // 因为event等事件循环库是对标准信号的监听,所以不能使用自定实时信号SIGRTMIN ~ SIGRTMAX 290 | // 默认暂时使用SIGPOLL 291 | Future::$signal = \SIGPOLL; 292 | // 假设\SIGPOLL存在一个已注册的回调,YourEventLoop::getCallback(\SIGPOLL)可以获取该事件在当前进程注册的回调响应 293 | // 设置回调 294 | Future::setSignalCallback(YourEventLoop::getCallback(\SIGPOLL)); 295 | // 开启信号监听,这时候开启的监听会触发之前的回调和通道回调,不会影响之前的回调 296 | Cache::channelUseSignalEnable(true) 297 | ``` 298 | > 通道信号监听维系了一个事件队列,多次触发信号时,回调只会根据事件队列是否存在事件消费标记而执行事件回调 299 | ### 其他功能具体可以参看代码注释和测试用例 300 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workbunny/webman-shared-cache", 3 | "type": "library", 4 | "license": "MIT", 5 | "description": "Webman plugin workbunny/webman-shared-cache", 6 | "authors": [ 7 | { 8 | "name": "chaz6chez", 9 | "email": "chaz6chez1993@outlook.com", 10 | "homepage": "https://chaz6chez.cn" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/workbunny/webman-shared-cache/issues", 15 | "source": "https://github.com/workbunny/webman-shared-cache" 16 | }, 17 | "require": { 18 | "php": "^8.0", 19 | "ext-apcu": "*" 20 | }, 21 | "require-dev": { 22 | "workerman/webman-framework": "^1.0 | ^2.0", 23 | "symfony/var-dumper": "^6.0 | ^7.0", 24 | "phpunit/phpunit": "^9.6", 25 | "webman/console": "^1.0 | ^2.0" 26 | }, 27 | "suggest": { 28 | "webman/console": "Webman-CLI support. ", 29 | "ext-posix": "Channel signal support. " 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Workbunny\\WebmanSharedCache\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Workbunny\\WebmanSharedCache\\Tests\\": "tests" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | writeln("ℹ️ $message"); 34 | } 35 | 36 | /** 37 | * 输出error 38 | * 39 | * @param OutputInterface $output 40 | * @param string $message 41 | * @return int 42 | */ 43 | protected function error(OutputInterface $output, string $message): int 44 | { 45 | $output->writeln("❌ $message"); 46 | return self::FAILURE; 47 | } 48 | 49 | /** 50 | * 输出success 51 | * 52 | * @param OutputInterface $output 53 | * @param string $message 54 | * @return int 55 | */ 56 | protected function success(OutputInterface $output, string $message): int 57 | { 58 | $output->writeln("✅ $message"); 59 | return self::SUCCESS; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Commands/WorkbunnyWebmanSharedCacheClean.php: -------------------------------------------------------------------------------- 1 | setName(static::$defaultName)->setDescription(static::$defaultDescription); 22 | } 23 | 24 | /** 25 | * @param InputInterface $input 26 | * @param OutputInterface $output 27 | * @return int 28 | */ 29 | protected function execute(InputInterface $input, OutputInterface $output): int 30 | { 31 | Cache::Clear(); 32 | return $this->success($output, "All caches removed successfully. "); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Commands/WorkbunnyWebmanSharedCacheEnable.php: -------------------------------------------------------------------------------- 1 | setDescription('Enable APCu cache with specified settings.') 23 | ->addOption('file', 'f', InputOption::VALUE_REQUIRED, 'Specify configuration name', 'apcu-cache.ini') 24 | ->addOption('target', 't', InputOption::VALUE_REQUIRED, 'Specify target location', '/usr/local/etc/php/conf.d') 25 | ->addOption('size', 'si', InputOption::VALUE_REQUIRED, 'Configure apcu.shm_size', '1024M') 26 | ->addOption('segments', 'se', InputOption::VALUE_REQUIRED, 'Configure apcu.shm_segments', 1) 27 | ->addOption('mmap', 'm', InputOption::VALUE_REQUIRED, 'Configure apcu.mmap_file_mask', '') 28 | ->addOption('gc_ttl', 'gc', InputOption::VALUE_REQUIRED, 'Configure apcu.gc_ttl', 3600); 29 | } 30 | 31 | /** 32 | * @param InputInterface $input 33 | * @param OutputInterface $output 34 | * @return int 35 | */ 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $fileName = $input->getOption('file'); 39 | $target = $input->getOption('target'); 40 | $shmSize = $input->getOption('size'); 41 | $shmSegments = $input->getOption('segments'); 42 | $mmapFileMask = $input->getOption('mmap'); 43 | $gcTtl = $input->getOption('gc_ttl'); 44 | 45 | if (!is_dir($target)) { 46 | return $this->error($output, "Target directory does not exist: $target. "); 47 | } 48 | $configContent = <<getHelper('question'); 61 | $question = new ConfirmationQuestion("Configuration file already exists at $filePath. Overwrite? (y/N) ", false); 62 | 63 | if (!$helper->ask($input, $output, $question)) { 64 | return $this->success($output, "Operation aborted. "); 65 | } 66 | } 67 | 68 | file_put_contents($filePath, $configContent); 69 | return $this->success($output, "Configuration file created at: $filePath. "); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Commands/WorkbunnyWebmanSharedCacheHRecycle.php: -------------------------------------------------------------------------------- 1 | setName(static::$defaultName)->setDescription(static::$defaultDescription); 22 | $this->addOption('key', 'k', InputOption::VALUE_OPTIONAL, 'Cache Key. '); 23 | } 24 | 25 | /** 26 | * @param InputInterface $input 27 | * @param OutputInterface $output 28 | * @return int 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output): int 31 | { 32 | $key = $input->getOption('key'); 33 | if ($key) { 34 | Cache::HRecycle($key); 35 | } else { 36 | $progressBar = new ProgressBar($output); 37 | $progressBar->start(); 38 | $keys = Cache::Keys(); 39 | $progressBar->setMaxSteps(count($keys)); 40 | foreach ($keys as $key) { 41 | Cache::HRecycle($key); 42 | $progressBar->advance(); 43 | } 44 | $progressBar->finish(); 45 | } 46 | return $this->success($output, 'HRecycle Success. '); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Future.php: -------------------------------------------------------------------------------- 1 | = [id => func|coroutine] 33 | */ 34 | protected static array $_futures = []; 35 | 36 | /** 37 | * @var Closure|null 38 | */ 39 | protected static ?Closure $_signalCallback = null; 40 | 41 | /** 42 | * @param Closure|null $func 43 | * @return void 44 | */ 45 | public static function setSignalCallback(?Closure $func): void 46 | { 47 | self::$_signalCallback = $func; 48 | } 49 | 50 | /** 51 | * @param Closure $func 52 | * @param array $args 53 | * @param float|int $interval 54 | * @return int|false 55 | */ 56 | public static function add(Closure $func, array $args = [], float|int $interval = 0): int|false 57 | { 58 | if (self::$debug) { 59 | self::$debugFunc = $func; 60 | self::$debugArgs = $args; 61 | return 1; 62 | } 63 | 64 | if (!Worker::$globalEvent) { 65 | throw new Error("Event driver error. "); 66 | } 67 | 68 | switch (self::$driver) { 69 | # 协程 70 | case self::DRIVER_COROUTINE: 71 | $coroutine = Coroutine::create(function () use ($func, $args) { 72 | Coroutine::suspend(); 73 | call_user_func($func, $args); 74 | }); 75 | $id = $coroutine->id(); 76 | self::$_futures[$id] = $coroutine; 77 | break; 78 | # 信号 79 | case self::DRIVER_SIGNAL: 80 | $func = function () use ($func, $args) { 81 | // 触发信号原回调 82 | if (self::$_signalCallback) { 83 | call_user_func(self::$_signalCallback); 84 | } 85 | // 触发信号通道回调 86 | call_user_func($func, $args); 87 | }; 88 | if (method_exists(Worker::$globalEvent, 'onSignal')) { 89 | Worker::$globalEvent->onSignal(self::$signal, $func); 90 | } else { 91 | Worker::$globalEvent->add(self::$signal, EventInterface::EV_SIGNAL, $func); 92 | } 93 | self::$_futures[$id = 0] = $func; 94 | break; 95 | # 默认定时器 96 | case self::DRIVER_TIMER: 97 | default: 98 | $interval = $interval > 0 ? $interval : (Worker::$eventLoopClass === Event::class ? 0 : 0.001); 99 | if ( 100 | $id = method_exists(Worker::$globalEvent, 'delay') 101 | ? Worker::$globalEvent->delay($interval, $func, $args) 102 | : Worker::$globalEvent->add($interval, EventInterface::EV_TIMER, $func, $args) 103 | ) { 104 | self::$_futures[$id] = $func; 105 | } 106 | break; 107 | } 108 | 109 | return $id; 110 | } 111 | 112 | /** 113 | * @param int|null $id 114 | * @return void 115 | */ 116 | public static function del(int|null $id = null): void 117 | { 118 | if (self::$debug) { 119 | self::$debugFunc = null; 120 | self::$debugArgs = []; 121 | return; 122 | } 123 | 124 | if (!Worker::$globalEvent) { 125 | throw new Error("Event driver error. "); 126 | } 127 | 128 | $futures = $id === null ? self::$_futures : [$id => (self::$_futures[$id] ?? null)]; 129 | foreach ($futures as $id => $fuc) { 130 | switch (self::$driver) { 131 | # 协程 132 | case self::DRIVER_COROUTINE: 133 | if ( 134 | ($coroutine = self::$_futures[$id] ?? null) and 135 | $coroutine instanceof Coroutine\Coroutine\CoroutineInterface 136 | ) { 137 | $coroutine->resume(); 138 | } 139 | break; 140 | case self::DRIVER_SIGNAL: 141 | if ($id === 0) { 142 | // 如果有信号回调,则恢复信号回调 143 | if (self::$_signalCallback) { 144 | if (method_exists(Worker::$globalEvent, 'onSignal')) { 145 | Worker::$globalEvent->onSignal(self::$signal, self::$_signalCallback); 146 | } else { 147 | Worker::$globalEvent->add(self::$signal, EventInterface::EV_SIGNAL, self::$_signalCallback); 148 | } 149 | } else { 150 | if (method_exists(Worker::$globalEvent, 'offSignal')) { 151 | Worker::$globalEvent->offSignal(self::$signal); 152 | } else { 153 | Worker::$globalEvent->del(self::$signal, EventInterface::EV_SIGNAL); 154 | } 155 | } 156 | } 157 | break; 158 | # 默认定时器 159 | case self::DRIVER_TIMER: 160 | if (method_exists(Worker::$globalEvent, 'offDelay')) { 161 | Worker::$globalEvent->offDelay($id); 162 | } else { 163 | Worker::$globalEvent->del($id, EventInterface::EV_TIMER); 164 | } 165 | break; 166 | default: 167 | break; 168 | } 169 | unset(self::$_futures[$id]); 170 | } 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/Install.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/workbunny/webman-shared-cache', 14 | ]; 15 | 16 | /** 17 | * Install 18 | * @return void 19 | */ 20 | public static function install(): void 21 | { 22 | static::installByRelation(); 23 | static::removeObsoleteCommand(); 24 | } 25 | 26 | /** 27 | * Uninstall 28 | * @return void 29 | */ 30 | public static function uninstall(): void 31 | { 32 | self::uninstallByRelation(); 33 | } 34 | 35 | /** 36 | * installByRelation 37 | * @return void 38 | */ 39 | public static function installByRelation(): void 40 | { 41 | foreach (static::$pathRelation as $source => $dest) { 42 | if ($pos = strrpos($dest, '/')) { 43 | $parent_dir = base_path().'/'.substr($dest, 0, $pos); 44 | if (!is_dir($parent_dir)) { 45 | mkdir($parent_dir, 0777, true); 46 | } 47 | } 48 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 49 | copy_dir(__DIR__ . "/$source", base_path()."/$dest"); 50 | echo "Create $dest 51 | "; 52 | } 53 | } 54 | 55 | /** 56 | * uninstallByRelation 57 | * @return void 58 | */ 59 | public static function uninstallByRelation(): void 60 | { 61 | foreach (static::$pathRelation as $source => $dest) { 62 | $path = base_path()."/$dest"; 63 | if (!is_dir($path) && !is_file($path)) { 64 | continue; 65 | } 66 | echo "Remove $dest 67 | "; 68 | if (is_file($path) || is_link($path)) { 69 | unlink($path); 70 | continue; 71 | } 72 | remove_dir($path); 73 | } 74 | } 75 | 76 | /** 77 | * remove obsolete command 78 | * @return void 79 | */ 80 | public static function removeObsoleteCommand(): void 81 | {} 82 | } -------------------------------------------------------------------------------- /src/RateLimiter.php: -------------------------------------------------------------------------------- 1 | (int)窗口限制数量, 19 | * 'remaining' => (int)当前窗口剩余数量, 20 | * 'reset' => (int)当前窗口剩余时间, 21 | * 'is_limit' => (bool)是否达到限流 22 | * ] 23 | */ 24 | public static function traffic(string $limitKey, string $configKey = 'default'): array 25 | { 26 | $data = []; 27 | if ( 28 | $config = self::$_debug ? 29 | [ 30 | 'limit' => 10, // 请求次数 31 | 'window_time' => 10, // 窗口时间,单位:秒 32 | ] : 33 | config("plugin.workbunny.webman-shared-cache.rate-limit.$configKey", []) 34 | ) { 35 | $blocking = false; 36 | while (!$blocking) { 37 | $blocking = Cache::Atomic($limitKey, function () use ($limitKey, $config, &$data) { 38 | if ( 39 | $cache = Cache::Get($limitKey) and 40 | ($reset = $cache['expired'] - time()) >= 0 41 | ) { 42 | $cache['count'] += 1; 43 | Cache::Set($limitKey, $cache, [ 44 | 'EX' => $config['window_time'] 45 | ]); 46 | return $data = [ 47 | 'limit' => $limit = $config['limit'], 48 | 'remaining' => max($limit - $cache['count'], 0), 49 | 'reset' => $reset, 50 | 'is_limit' => $cache['count'] > $config['limit'], 51 | ]; 52 | } 53 | Cache::Set($limitKey, [ 54 | 'created' => $now = time(), 55 | 'expired' => $now + $config['window_time'], 56 | 'count' => 1 57 | ], [ 58 | 'EX' => $config['window_time'] 59 | ]); 60 | return $data = [ 61 | 'limit' => $limit = $config['limit'], 62 | 'remaining' => max($limit - 1, 0), 63 | 'reset' => $config['window_time'], 64 | 'is_limit' => false, 65 | ]; 66 | }); 67 | } 68 | } 69 | return $data; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Traits/BasicMethods.php: -------------------------------------------------------------------------------- 1 | * 匹配一个或多个字符 44 | * ? 匹配一个字符 45 | * @return string 46 | */ 47 | public static function WildcardToRegex(string $match): string 48 | { 49 | $regex = preg_quote($match, '/'); // 对特殊字符进行转义 50 | $regex = str_replace('\*', '.+', $regex); 51 | $regex = str_replace('\?', '.', $regex); 52 | return '/^' . $regex . '$/'; 53 | } 54 | 55 | /** 56 | * 获取键信息 57 | * 58 | * @param string $key 59 | * @return array 60 | */ 61 | protected static function _KeyInfo(string $key): array 62 | { 63 | return apcu_key_info($key) ?: []; 64 | } 65 | 66 | /** 67 | * 获取缓存信息 68 | * 69 | * @param bool $limited 70 | * @return array 71 | */ 72 | protected static function _Info(bool $limited = false): array 73 | { 74 | return apcu_cache_info($limited); 75 | } 76 | 77 | /** 78 | * 搜索 79 | * 80 | * @param string $regex 正则 81 | * @param Closure $handler = function ($key, $value) {} 82 | * @param int $chunkSize 83 | * @return void 84 | */ 85 | protected static function _Search(string $regex, Closure $handler, int $chunkSize = 100): void 86 | { 87 | $iterator = new APCUIterator($regex, APC_ITER_ALL, $chunkSize); 88 | while ($iterator->valid()) { 89 | $data = $iterator->current(); 90 | call_user_func($handler, $data['key'], $data['value']); 91 | $iterator->next(); 92 | } 93 | } 94 | 95 | /** 96 | * 缓存释放 97 | * 98 | * @return bool 99 | */ 100 | protected static function _Clear(): bool 101 | { 102 | return apcu_clear_cache(); 103 | } 104 | 105 | /** 106 | * 获取锁信息 107 | * 108 | * @return array = [ 109 | * lockKey => [ 110 | * 'timestamp' => 创建时间(float), 111 | * 'method' => 创建的方法(string), 112 | * 'params' => 方法参数(array), 113 | * ] 114 | * ] 115 | */ 116 | protected static function _LockInfo(): array 117 | { 118 | $res = []; 119 | self::_Search(self::WildcardToRegex(self::$_LOCK . '*'), function ($key, $value) use (&$res) { 120 | $res[$key] = $value; 121 | }); 122 | return $res; 123 | } 124 | 125 | /** 126 | * 原子操作 127 | * - 无法对锁本身进行原子性操作 128 | * - 只保证handler是否被原子性触发,对其逻辑是否抛出异常不负责 129 | * - handler尽可能避免超长阻塞 130 | * - lockKey会被自动设置特殊前缀#lock#,可以通过Cache::LockInfo进行查询 131 | * 132 | * @param string $lockKey 133 | * @param Closure $handler 134 | * @param bool $blocking 135 | * @return bool 136 | */ 137 | protected static function _Atomic(string $lockKey, Closure $handler, bool $blocking = false): bool 138 | { 139 | $func = __FUNCTION__; 140 | $result = false; 141 | if ($blocking) { 142 | $startTime = time(); 143 | while ($blocking) { 144 | // 阻塞保险 145 | if (time() >= $startTime + self::$fuse) {return false;} 146 | // 创建锁 147 | apcu_entry($lock = self::GetLockKey($lockKey), function () use ( 148 | $lockKey, $handler, $func, &$result, &$blocking 149 | ) { 150 | $res = call_user_func($handler); 151 | $result = true; 152 | $blocking = false; 153 | return [ 154 | 'timestamp' => microtime(true), 155 | 'method' => $func, 156 | 'params' => [$lockKey, '\Closure'], 157 | 'result' => $res 158 | ]; 159 | }); 160 | } 161 | } else { 162 | // 创建锁 163 | apcu_entry($lock = self::GetLockKey($lockKey), function () use ( 164 | $lockKey, $handler, $func, &$result 165 | ) { 166 | $res = call_user_func($handler); 167 | $result = true; 168 | return [ 169 | 'timestamp' => microtime(true), 170 | 'method' => $func, 171 | 'params' => [$lockKey, '\Closure'], 172 | 'result' => $res 173 | ]; 174 | }); 175 | } 176 | if ($result) { 177 | apcu_delete($lock); 178 | } 179 | return $result; 180 | } 181 | 182 | /** 183 | * 设置缓存 184 | * - NX和XX将会阻塞直至成功 185 | * - 阻塞最大时长受fuse保护,默认60s 186 | * - 抢占式锁 187 | * 188 | * @param string $key 189 | * @param mixed $value 190 | * @param array $optional = [ 191 | * 'NX', 192 | * Only set the key if it doesn't exist. 193 | * 'XX', 194 | * Only set the key if it already exists. 195 | * 'EX' => 60, 196 | * expire 60 seconds. 197 | * 'EXAT' => time() + 10, 198 | * expire in 10 seconds. 199 | * ] 200 | * @return bool 201 | */ 202 | protected static function _Set(string $key, mixed $value, array $optional = []): bool 203 | { 204 | $ttl = intval($optional['EX'] ?? (isset($optional['EXAT']) ? ($optional['EXAT'] - time()) : 0)); 205 | if (in_array('NX', $optional)) { 206 | $startTime = time(); 207 | while (!apcu_add($key, $value, $ttl)) { 208 | // 阻塞保险 209 | if (time() >= $startTime + self::$fuse) {return false;} 210 | } 211 | return true; 212 | } 213 | if (in_array('XX', $optional)) { 214 | $startTime = time(); 215 | $blocking = true; 216 | while ($blocking) { 217 | apcu_entry($key, function () use ($value, &$blocking) { 218 | $blocking = false; 219 | return $value; 220 | }, $ttl); 221 | // 阻塞保险 222 | if (time() >= $startTime + self::$fuse) {return false;} 223 | } 224 | return true; 225 | } 226 | return (bool)apcu_store($key, $value, $ttl); 227 | } 228 | 229 | /** 230 | * 自增 231 | * 232 | * @param string $key 233 | * @param int|float $value 234 | * @param int $ttl 235 | * @return bool|int|float 236 | */ 237 | protected static function _Incr(string $key, int|float $value = 1, int $ttl = 0): bool|int|float 238 | { 239 | $func = __FUNCTION__; 240 | $result = false; 241 | $params = func_get_args(); 242 | self::_Atomic($key, function () use ( 243 | $key, $value, $ttl, $func, $params, &$result 244 | ) { 245 | $v = self::_Get($key, 0); 246 | if (is_numeric($v)) { 247 | self::_Set($key, $value = $v + $value, [ 248 | 'EX' => $ttl 249 | ]); 250 | $result = $value; 251 | } 252 | return [ 253 | 'timestamp' => microtime(true), 254 | 'method' => $func, 255 | 'params' => $params, 256 | 'result' => null 257 | ]; 258 | }, true); 259 | return $result; 260 | } 261 | 262 | /** 263 | * @param string $key 264 | * @param int|float $value 265 | * @param int $ttl 266 | * @return bool|int|float 267 | */ 268 | protected static function _Decr(string $key, int|float $value = 1, int $ttl = 0): bool|int|float 269 | { 270 | $func = __FUNCTION__; 271 | $result = false; 272 | $params = func_get_args(); 273 | self::_Atomic($key, function () use ( 274 | $key, $value, $ttl, $func, $params, &$result 275 | ) { 276 | $v = self::_Get($key, 0); 277 | if (is_numeric($v)) { 278 | self::_Set($key, $value = $v - $value, [ 279 | 'EX' => $ttl 280 | ]); 281 | $result = $value; 282 | } 283 | return [ 284 | 'timestamp' => microtime(true), 285 | 'method' => $func, 286 | 'params' => $params, 287 | 'result' => null 288 | ]; 289 | }, true); 290 | return $result; 291 | } 292 | 293 | /** 294 | * 判断缓存键 295 | * 296 | * @param string ...$key 297 | * @return array returned that contains all existing keys, or an empty array if none exist. 298 | */ 299 | protected static function _Exists(string ...$key): array 300 | { 301 | return apcu_exists($key); 302 | } 303 | 304 | /** 305 | * 获取缓存值 306 | * 307 | * @param string $key 308 | * @param mixed|null $default 309 | * @return mixed 310 | */ 311 | protected static function _Get(string $key, mixed $default = null): mixed 312 | { 313 | $res = apcu_fetch($key, $success); 314 | return $success ? $res : $default; 315 | } 316 | 317 | /** 318 | * 移除缓存 319 | * 320 | * @param string ...$keys 321 | * @return array returns list of failed keys. 322 | */ 323 | protected static function _Del(string ...$keys): array 324 | { 325 | return apcu_delete($keys); 326 | } 327 | 328 | /** 329 | * 获取所有key 330 | * 331 | * @param string|null $regex 332 | * @return array 333 | */ 334 | protected static function _Keys(null|string $regex = null): array 335 | { 336 | $keys = []; 337 | if ($info = apcu_cache_info()) { 338 | $keys = array_column($info['cache_list'] ?? [], 'info'); 339 | if ($regex !== null) { 340 | $keys = array_values(array_filter($keys, function($key) use ($regex) { 341 | return preg_match($regex, $key); 342 | })); 343 | } 344 | } 345 | return $keys; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Traits/ChannelMethods.php: -------------------------------------------------------------------------------- 1 | futureId] 31 | */ 32 | protected static array $_listeners = []; 33 | 34 | /** 35 | * @var float|int 36 | */ 37 | protected static float|int $interval = 0; 38 | 39 | /** 40 | * @param float|int $interval 41 | * @return void 42 | */ 43 | public static function SetChannelListenerInterval(float|int $interval): void 44 | { 45 | self::$interval = $interval; 46 | } 47 | 48 | /** 49 | * @param string $key 50 | * @return string 51 | */ 52 | public static function GetChannelKey(string $key): string 53 | { 54 | return self::$_CHANNEL . $key; 55 | } 56 | 57 | /** 58 | * 通道全局开启使用信号监听 59 | * 60 | * @param bool $enable 61 | * @return void 62 | */ 63 | public static function channelUseSignalEnable(bool $enable = true): void 64 | { 65 | Future::$driver = $enable ? Future::DRIVER_SIGNAL : Future::DRIVER_TIMER; 66 | } 67 | 68 | /** 69 | * 通道是否使用信号监听 70 | * 71 | * @return bool 72 | */ 73 | public static function isChannelUseSignal(): bool 74 | { 75 | return Future::$driver === Future::DRIVER_SIGNAL; 76 | } 77 | 78 | /** 79 | * 通道获取 80 | * 81 | * @param string $key 82 | * @return array = [ 83 | * workerId = [ 84 | * 'futureId' => futureId, 85 | * 'value' => array 86 | * ] 87 | * ] 88 | */ 89 | protected static function _GetChannel(string $key): array 90 | { 91 | return self::_Get(self::GetChannelKey($key), []); 92 | } 93 | 94 | /** 95 | * 通道投递 96 | * - 阻塞最大时长受fuse保护,默认60s 97 | * - 抢占式锁 98 | * 99 | * @param string $key 100 | * @param mixed $message 101 | * @param string|int|null $workerId 指定的workerId 102 | * @param bool $store 在没有监听器时是否进行储存 103 | * @return bool 104 | */ 105 | protected static function _ChPublish(string $key, mixed $message, bool $store = true, null|string|int $workerId = null): bool 106 | { 107 | $func = __FUNCTION__; 108 | $params = func_get_args(); 109 | self::_Atomic($key, function () use ( 110 | $key, $message, $func, $params, $store, $workerId 111 | ) { 112 | /** 113 | * [ 114 | * workerId = [ 115 | * 'futureId' => futureId, 116 | * 'value' => array 117 | * ] 118 | * ] 119 | */ 120 | $channel = self::_Get($channelName = self::GetChannelKey($key), []); 121 | // 如果还没有监听器,将数据投入默认 122 | if (!$channel) { 123 | if ($store) { 124 | // 非指定workerId 125 | if ($workerId === null) { 126 | $channel['--default--']['value'][] = $message; 127 | } 128 | // 指定workerId 129 | else { 130 | $channel[$workerId]['value'][] = $message; 131 | } 132 | 133 | } 134 | } 135 | // 否则将消息投入到每个worker的监听器数据中 136 | else { 137 | // 非指定workerId 138 | if ($workerId === null) { 139 | foreach ($channel as $workerId => $item) { 140 | if ($store or isset($item['futureId'])) { 141 | $channel[$workerId]['value'][] = $message; 142 | } 143 | } 144 | } 145 | // 指定workerId 146 | else { 147 | if ($store or isset($channel[$workerId]['futureId'])) { 148 | $channel[$workerId]['value'][] = $message; 149 | } 150 | } 151 | } 152 | 153 | self::_Set($channelName, $channel); 154 | // 使用信号监听 155 | if (self::isChannelUseSignal()) { 156 | $list = self::_Get(self::$_CHANNEL_PID_LIST, []); 157 | foreach ($list as $pid) { 158 | self::_Atomic(self::$_CHANNEL_EVENT_LIST, function () use ($pid) { 159 | // 设置通道事件标记 160 | $channelEventList = self::_Get(self::$_CHANNEL_EVENT_LIST, []); 161 | $channelEventList[$pid][] = 1; 162 | self::_Set(self::$_CHANNEL_EVENT_LIST, $channelEventList); 163 | // 发送信号通知进程 164 | @posix_kill($pid, Future::$signal); 165 | }); 166 | } 167 | } 168 | return [ 169 | 'timestamp' => microtime(true), 170 | 'method' => $func, 171 | 'params' => $params, 172 | 'result' => null 173 | ]; 174 | }, true); 175 | return true; 176 | } 177 | 178 | /** 179 | * 创建通道监听器 180 | * - 同一个进程只能创建一个监听器来监听相同的通道 181 | * - 同一个进程可以同时监听不同的通道 182 | * 183 | * @param string $key 184 | * @param string|int $workerId 185 | * @param Closure $listener = function(string $channelName, string|int $workerId, mixed $message) {} 186 | * @return bool|int 监听器id 187 | */ 188 | protected static function _ChCreateListener(string $key, string|int $workerId, Closure $listener): bool|int 189 | { 190 | $func = __FUNCTION__; 191 | $result = false; 192 | $params = func_get_args(); 193 | $params[2] = '\Closure'; 194 | if (isset(self::$_listeners[$key])) { 195 | throw new Error("Channel $key listener already exist. "); 196 | } 197 | self::_Atomic($key, function () use ( 198 | $key, $workerId, $func, $params, $listener, &$result 199 | ) { 200 | // 信号监听则注册pid 201 | if (self::isChannelUseSignal()) { 202 | $channelPidList = self::_Get(self::$_CHANNEL_PID_LIST, []); 203 | $channelPidList[$pid = posix_getpid()] = $pid; 204 | self::_Set(self::$_CHANNEL_PID_LIST, $channelPidList); 205 | } 206 | /** 207 | * [ 208 | * workerId = [ 209 | * 'futureId' => futureId, 210 | * 'value' => array 211 | * ] 212 | * ] 213 | */ 214 | $channel = self::_Get($channelName = self::GetChannelKey($key), []); 215 | // 监听器回调函数 216 | $callback = function () use ($key, $workerId, $listener) { 217 | // 原子性执行 218 | self::_Atomic($key, function () use ($key, $workerId, $listener) { 219 | // 信号监听 220 | if (self::isChannelUseSignal()) { 221 | $pid = posix_getpid(); 222 | // 获取通道事件标记列表 223 | $channelEventList = self::_Get(self::$_CHANNEL_EVENT_LIST, []); 224 | $events = $channelEventList[$pid] ?? []; 225 | // 如果没有事件标记则跳过 226 | if (!array_pop($events)) { 227 | return; 228 | } 229 | // 更新通道事件标记 230 | $channelEventList[$pid] = $events; 231 | self::_Set(self::$_CHANNEL_EVENT_LIST, $channelEventList); 232 | } 233 | // 数据回调 234 | $channel = self::_Get($channelName = self::GetChannelKey($key), []); 235 | if ((!empty($value = $channel[$workerId]['value'] ?? []))) { 236 | // 先进先出 237 | $msg = array_shift($value); 238 | $channel[$workerId]['value'] = $value; 239 | call_user_func($listener, $key, $workerId, $msg); 240 | self::_Set($channelName, $channel); 241 | } 242 | 243 | }); 244 | }; 245 | // 设置回调 246 | $channel[$workerId]['futureId'] = self::$_listeners[$key] = $result = Future::add($callback, interval: self::$interval); 247 | $channel[$workerId]['value'] = []; 248 | // 如果存在默认数据 249 | if ($default = $channel['--default--']['value'] ?? []) { 250 | foreach ($channel as &$item) { 251 | array_unshift($item['value'], ...$default); 252 | } 253 | unset($channel['--default--']); 254 | } 255 | self::_Set($channelName, $channel); 256 | return [ 257 | 'timestamp' => microtime(true), 258 | 'method' => $func, 259 | 'params' => $params, 260 | 'result' => null 261 | ]; 262 | }, true); 263 | return $result; 264 | } 265 | 266 | /** 267 | * 移除通道监听器 268 | * 269 | * @param string $key 270 | * @param string|int $workerId 271 | * @param bool $remove 是否移除消息 272 | * @return void 273 | */ 274 | protected static function _ChRemoveListener(string $key, string|int $workerId, bool $remove = false): void 275 | { 276 | $func = __FUNCTION__; 277 | $params = func_get_args(); 278 | self::_Atomic($key, function () use ( 279 | $key, $workerId, $func, $params, $remove 280 | ) { 281 | if ($id = self::$_listeners[$key] ?? null) { 282 | // 移除future 283 | Future::del($id); 284 | // 信号监听则注册pid 285 | if (self::isChannelUseSignal()) { 286 | $pid = posix_getpid(); 287 | // 移除pid 288 | $channelPidList = self::_Get(self::$_CHANNEL_PID_LIST, []); 289 | if ($channelPidList[$pid] ?? null) { 290 | unset($channelPidList[$pid]); 291 | self::_Set(self::$_CHANNEL_PID_LIST, $channelPidList); 292 | } 293 | // 移除事件标记 294 | $channelEventList = self::_Get(self::$_CHANNEL_EVENT_LIST, []); 295 | if ($channelEventList[$pid] ?? null) { 296 | unset($channelEventList[$pid]); 297 | self::_Set(self::$_CHANNEL_EVENT_LIST, $channelEventList); 298 | } 299 | } 300 | if ($remove) { 301 | /** 302 | * [ 303 | * workerId = [ 304 | * 'futureId' => futureId, 305 | * 'value' => array 306 | * ] 307 | * ] 308 | */ 309 | $channel = self::_Get($channelName = self::GetChannelKey($key), []); 310 | unset($channel[$workerId]); 311 | self::_Set($channelName, $channel); 312 | } 313 | unset(self::$_listeners[$key]); 314 | } 315 | return [ 316 | 'timestamp' => microtime(true), 317 | 'method' => $func, 318 | 'params' => $params, 319 | 'result' => null 320 | ]; 321 | }, true); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Traits/HashMethods.php: -------------------------------------------------------------------------------- 1 | $hashValue, 40 | '_ttl' => $ttl, 41 | '_timestamp' => time() 42 | ]; 43 | self::_Set($key, $hash); 44 | return [ 45 | 'timestamp' => microtime(true), 46 | 'method' => $func, 47 | 'params' => $params, 48 | 'result' => null 49 | ]; 50 | }, true); 51 | return true; 52 | } 53 | 54 | /** 55 | * hash 自增 56 | * 57 | * @param string $key 58 | * @param string|int $hashKey 59 | * @param int|float $hashValue 60 | * @param int $ttl 61 | * @return bool|int|float 62 | */ 63 | protected static function _HIncr(string $key, string|int $hashKey, int|float $hashValue = 1, int $ttl = 0): bool|int|float 64 | { 65 | $func = __FUNCTION__; 66 | $result = false; 67 | $params = func_get_args(); 68 | self::_Atomic($key, function () use ( 69 | $key, $hashKey, $hashValue, $ttl, $func, $params, &$result 70 | ) { 71 | $hash = self::_Get($key, []); 72 | $value = $hash[$hashKey]['_value'] ?? 0; 73 | $oldTtl = $hash[$hashKey]['_ttl'] ?? 0; 74 | $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; 75 | if (is_numeric($value)) { 76 | $now = time(); 77 | $value = ($oldTtl <= 0 or (($timestamp + $oldTtl) >= $now)) ? $value : 0; 78 | $hash[$hashKey] = [ 79 | '_value' => $result = $value + $hashValue, 80 | '_ttl' => ($ttl > 0) ? $ttl : ($timestamp > 0 ? $now - $timestamp : 0), 81 | '_timestamp' => $now, 82 | ]; 83 | self::_Set($key, $hash); 84 | } 85 | return [ 86 | 'timestamp' => microtime(true), 87 | 'method' => $func, 88 | 'params' => $params, 89 | 'result' => null 90 | ]; 91 | }, true); 92 | return $result; 93 | } 94 | 95 | /** 96 | * hash 自减 97 | * 98 | * @param string $key 99 | * @param string|int $hashKey 100 | * @param int|float $hashValue 101 | * @param int $ttl 102 | * @return bool|int|float 103 | */ 104 | protected static function _HDecr(string $key, string|int $hashKey, int|float $hashValue = 1, int $ttl = 0): bool|int|float 105 | { 106 | $func = __FUNCTION__; 107 | $result = false; 108 | $params = func_get_args(); 109 | self::_Atomic($key, function () use ( 110 | $key, $hashKey, $hashValue, $ttl, $func, $params, &$result 111 | ) { 112 | $hash = self::_Get($key, []); 113 | $value = $hash[$hashKey]['_value'] ?? 0; 114 | $oldTtl = $hash[$hashKey]['_ttl'] ?? 0; 115 | $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; 116 | if (is_numeric($value)) { 117 | $now = time(); 118 | $value = ($oldTtl <= 0 or (($timestamp + $oldTtl) >= $now)) ? $value : 0; 119 | $hash[$hashKey] = [ 120 | '_value' => $result = $value - $hashValue, 121 | '_ttl' => ($ttl > 0) ? $ttl : ($timestamp > 0 ? $now - $timestamp : 0), 122 | '_timestamp' => $now, 123 | ]; 124 | self::_Set($key, $hash); 125 | } 126 | return [ 127 | 'timestamp' => microtime(true), 128 | 'method' => $func, 129 | 'params' => $params, 130 | 'result' => null 131 | ]; 132 | }, true); 133 | return $result; 134 | } 135 | 136 | /** 137 | * hash 移除 138 | * - 阻塞最大时长受fuse保护,默认60s 139 | * - 抢占式锁 140 | * 141 | * @param string $key 142 | * @param string|int ...$hashKey 143 | * @return bool 144 | */ 145 | protected static function _HDel(string $key, string|int ...$hashKey): bool 146 | { 147 | $func = __FUNCTION__; 148 | $params = func_get_args(); 149 | self::_Atomic($key, function () use ( 150 | $key, $hashKey, $func, $params 151 | ) { 152 | $hash = self::_Get($key, []); 153 | foreach ($hashKey as $hk) { 154 | unset($hash[$hk]); 155 | } 156 | self::_Set($key, $hash); 157 | return [ 158 | 'timestamp' => microtime(true), 159 | 'method' => $func, 160 | 'params' => $params, 161 | 'result' => null 162 | ]; 163 | }, true); 164 | return true; 165 | } 166 | 167 | /** 168 | * hash 获取 169 | * 170 | * @param string $key 171 | * @param string|int $hashKey 172 | * @param mixed|null $default 173 | * @return mixed 174 | */ 175 | protected static function _HGet(string $key, string|int $hashKey, mixed $default = null): mixed 176 | { 177 | $now = time(); 178 | $hash = self::_Get($key, []); 179 | $value = $hash[$hashKey]['_value'] ?? $default; 180 | $ttl = $hash[$hashKey]['_ttl'] ?? 0; 181 | $timestamp = $hash[$hashKey]['_timestamp'] ?? 0; 182 | return ($ttl <= 0 or (($timestamp + $ttl) >= $now)) ? $value : $default; 183 | } 184 | 185 | /** 186 | * 回收过期 hashKey 187 | * 188 | * @param string $key 189 | * @return void 190 | */ 191 | protected static function _HRecycle(string $key): void 192 | { 193 | $func = __FUNCTION__; 194 | $params = func_get_args(); 195 | self::_Atomic($key, function () use ( 196 | $key, $func, $params 197 | ) { 198 | $hash = self::_Get($key, []); 199 | if (isset($hash['_ttl']) and isset($hash['_timestamp'])) { 200 | $now = time(); 201 | $set = false; 202 | foreach ($hash as $hashKey => $hashValue) { 203 | $ttl = $hashValue['_ttl'] ?? 0; 204 | $timestamp = $hashValue['_timestamp'] ?? 0; 205 | if ($ttl > 0 and $timestamp > 0 and $timestamp + $ttl < $now) { 206 | $set = true; 207 | unset($hash[$hashKey]); 208 | } 209 | } 210 | if ($set) { 211 | self::_Set($key, $hash); 212 | } 213 | } 214 | return [ 215 | 'timestamp' => microtime(true), 216 | 'method' => $func, 217 | 'params' => $params, 218 | 'result' => null 219 | ]; 220 | }, true); 221 | } 222 | 223 | /** 224 | * hash key 判断 225 | * 226 | * @param string $key 227 | * @param string|int ...$hashKey 228 | * @return array 229 | */ 230 | protected static function _HExists(string $key, string|int ...$hashKey): array 231 | { 232 | $hash = self::_Get($key, []); 233 | $result = []; 234 | $now = time(); 235 | foreach ($hashKey as $hk) { 236 | $ttl = $hash[$hk]['_ttl'] ?? 0; 237 | $timestamp = $hash[$hk]['_timestamp'] ?? 0; 238 | if (($ttl <= 0 or (($timestamp + $ttl) >= $now)) and isset($hash[$hk]['_value'])) { 239 | $result[$hk] = true; 240 | } 241 | } 242 | return $result; 243 | } 244 | 245 | /** 246 | * hash keys 247 | * 248 | * @param string $key 249 | * @param string|null $regex 250 | * @return array 251 | */ 252 | protected static function _HKeys(string $key, null|string $regex = null): array 253 | { 254 | $hash = self::_Get($key, []); 255 | $keys = []; 256 | $now = time(); 257 | foreach ($hash as $hashKey => $hashValue) { 258 | $ttl = $hashValue['_ttl'] ?? 0; 259 | $timestamp = $hashValue['_timestamp'] ?? 0; 260 | if (($ttl <= 0 or (($timestamp + $ttl) >= $now)) and isset($hashValue['_value'])) { 261 | if ($regex !== null and preg_match($regex, $key)) { 262 | continue; 263 | } 264 | $keys[] = $hashKey; 265 | } 266 | } 267 | return $keys; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/config/plugin/workbunny/webman-shared-cache/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /src/config/plugin/workbunny/webman-shared-cache/command.php: -------------------------------------------------------------------------------- 1 | 8 | * @copyright chaz6chez 9 | * @link https://github.com/workbunny/webman-push-server 10 | * @license https://github.com/workbunny/webman-push-server/blob/main/LICENSE 11 | */ 12 | declare(strict_types=1); 13 | 14 | return [ 15 | Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheEnable::class, 16 | Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheClean::class, 17 | Workbunny\WebmanSharedCache\Commands\WorkbunnyWebmanSharedCacheHRecycle::class 18 | ]; 19 | -------------------------------------------------------------------------------- /src/config/plugin/workbunny/webman-shared-cache/rate-limit.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'limit' => 10, // 请求次数 6 | 'window_time' => 60, // 窗口时间,单位:秒 7 | ] 8 | ]; 9 | -------------------------------------------------------------------------------- /src/config/plugin/workbunny/webman-shared-cache/shared-cache-enable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | target="" 4 | shm_size="1024M" 5 | shm_segments=1 6 | mmap_file_mask="" 7 | gc_ttl=3600 8 | file_name="apcu-cache.ini" 9 | 10 | options=$(getopt -o hf:t:si:se:m:gc: --long help,file:,target:,size:,segments:,mmap:,gc_ttl: -- "$@") 11 | eval set -- "$options" 12 | 13 | while true; do 14 | case $1 in 15 | -h | --help) 16 | echo "使用: cache-enable.sh [选项]..." 17 | echo "选项:" 18 | echo " -h, --help 显示帮助信息" 19 | echo " -f, --file [名称] 指定配置名称" 20 | echo " -t, --target [路径] 指定目标位置" 21 | echo " -m, --mmap-file [取值] 配置 apcu.mmap_file_mask 【例 /tmp/apc.XXXXXX】" 22 | echo " -si, --size [取值] 配置 apcu.shm_size 【例 1024M】" 23 | echo " -se, --segments [取值] 配置 apcu.shm_segments" 24 | echo " -gc, --gc_ttl [取值] 配置 apcu.gc_ttl" 25 | 26 | exit 0 27 | ;; 28 | -t | --target) 29 | target="$2" 30 | shift 2 31 | ;; 32 | -f | --file) 33 | file_name="$2" 34 | shift 2 35 | ;; 36 | -si | --size) 37 | shm_size="$2" 38 | shift 2 39 | ;; 40 | -se | --segments) 41 | shm_segments="$2" 42 | shift 2 43 | ;; 44 | -m | --mmap) 45 | mmap_file_mask="$2" 46 | shift 2 47 | ;; 48 | -gc | --gc_ttl) 49 | gc_ttl="$2" 50 | shift 2 51 | ;; 52 | --) 53 | shift 54 | break 55 | ;; 56 | *) 57 | echo "无效选项: $1" >&2 58 | exit 1 59 | ;; 60 | esac 61 | done 62 | 63 | if [ -z "$target" ]; then 64 | target="/usr/local/etc/php/conf.d" 65 | echo "配置将被创建至 $target,是否继续?(y/N)" 66 | read answer 67 | answer=$(echo $answer | tr [a-z] [A-Z]) 68 | if [ "$answer" != "Y" ]; then 69 | echo "已放弃操作. " 70 | exit 0 71 | fi 72 | fi 73 | 74 | cat >$file_name <assertEquals(null, Cache::Get($key)); 15 | apcu_add($key, $key); 16 | $this->assertEquals($key, Cache::Get($key)); 17 | // 清理 18 | apcu_delete($key); 19 | 20 | // 子进程执行 21 | $this->assertEquals(null, Cache::Get($key)); 22 | $this->childExec(static function (string $key) { 23 | apcu_add($key, $key); 24 | }, $key); 25 | $this->assertEquals($key, Cache::Get($key)); 26 | // 清理 27 | apcu_delete($key); 28 | } 29 | 30 | 31 | public function testCacheSet(): void 32 | { 33 | $key = __METHOD__; 34 | // 单进程执行 35 | $this->assertFalse(apcu_fetch($key)); 36 | $this->assertTrue(Cache::Set($key, $key)); 37 | $this->assertEquals($key, apcu_fetch($key)); 38 | // 清理 39 | apcu_delete($key); 40 | 41 | // 子进程执行 42 | $this->assertFalse(apcu_fetch($key)); 43 | $this->childExec(static function (string $key) { 44 | Cache::Set($key, $key); 45 | }, $key); 46 | $this->assertEquals($key, apcu_fetch($key)); 47 | // 清理 48 | apcu_delete($key); 49 | } 50 | 51 | 52 | public function testCacheDel(): void 53 | { 54 | $key = __METHOD__; 55 | // 在单进程内 56 | apcu_add($key, $key); 57 | $this->assertEquals([], Cache::Del($key)); 58 | $this->assertFalse(apcu_fetch($key)); 59 | // 清理 60 | apcu_delete($key); 61 | 62 | // 在子进程内 63 | apcu_add($key, $key); 64 | $this->childExec(static function (string $key) { 65 | Cache::Del($key); 66 | }, $key); 67 | $this->assertFalse(apcu_fetch($key)); 68 | // 清理 69 | apcu_delete($key); 70 | } 71 | 72 | 73 | public function testCacheExists(): void 74 | { 75 | $key = __METHOD__; 76 | 77 | $this->assertEquals([], Cache::Exists($key)); 78 | apcu_add($key, $key); 79 | $this->assertEquals([ 80 | 'Workbunny\WebmanSharedCache\Tests\CacheTest::testCacheExists' => true 81 | ], Cache::Exists($key)); 82 | // 清理 83 | apcu_delete($key); 84 | } 85 | 86 | 87 | public function testCacheIncr(): void 88 | { 89 | $key = __METHOD__; 90 | // 在单进程内 91 | $this->assertFalse(apcu_fetch($key)); 92 | $this->assertEquals(1, Cache::Incr($key)); 93 | $this->assertEquals(1, apcu_fetch($key)); 94 | $this->assertEquals(3, Cache::Incr($key, 2)); 95 | $this->assertEquals(3, apcu_fetch($key)); 96 | $this->assertEquals(4.1, Cache::Incr($key, 1.1)); 97 | $this->assertEquals(4.1, apcu_fetch($key)); 98 | // 清理 99 | apcu_delete($key); 100 | 101 | // 在子进程内 102 | $this->assertFalse(apcu_fetch($key)); 103 | $this->childExec(static function (string $key) { 104 | Cache::Incr($key); 105 | }, $key); 106 | $this->assertEquals(1, apcu_fetch($key)); 107 | $this->childExec(static function (string $key) { 108 | Cache::Incr($key, 2); 109 | }, $key); 110 | $this->assertEquals(3, apcu_fetch($key)); 111 | $this->childExec(static function (string $key) { 112 | Cache::Incr($key, 1.1); 113 | }, $key); 114 | $this->assertEquals(4.1, apcu_fetch($key)); 115 | 116 | // 清理 117 | apcu_delete($key); 118 | } 119 | 120 | 121 | public function testCacheDecr(): void 122 | { 123 | $key = __METHOD__; 124 | // 在单进程内 125 | $this->assertFalse(apcu_fetch($key)); 126 | $this->assertEquals(-1, Cache::Decr($key)); 127 | $this->assertEquals(-1, apcu_fetch($key)); 128 | $this->assertEquals(-3, Cache::Decr($key, 2)); 129 | $this->assertEquals(-3, apcu_fetch($key)); 130 | $this->assertEquals(-4.1, Cache::Decr($key, 1.1)); 131 | $this->assertEquals(-4.1, apcu_fetch($key)); 132 | // 清理 133 | apcu_delete($key); 134 | 135 | // 在子进程内 136 | $this->assertFalse(apcu_fetch($key)); 137 | $this->childExec(static function (string $key) { 138 | Cache::Decr($key); 139 | }, $key); 140 | $this->assertEquals(-1, apcu_fetch($key)); 141 | $this->childExec(static function (string $key) { 142 | Cache::Decr($key, 2); 143 | }, $key); 144 | $this->assertEquals(-3, apcu_fetch($key)); 145 | $this->childExec(static function (string $key) { 146 | Cache::Decr($key, 1.1); 147 | }, $key); 148 | $this->assertEquals(-4.1, apcu_fetch($key)); 149 | // 清理 150 | apcu_delete($key); 151 | } 152 | } -------------------------------------------------------------------------------- /tests/ChannelTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('#Channel#' . $channel, Cache::GetChannelKey($channel)); 14 | } 15 | 16 | public function testChannelPublish(): void 17 | { 18 | $channel = __FUNCTION__; 19 | $message = 'test'; 20 | // 初始化确认 21 | $this->assertEquals([], Cache::GetChannel($channel)); 22 | $this->assertEquals([], Cache::LockInfo()); 23 | // 当前进程执行 24 | Cache::ChPublish($channel, $message); 25 | // 确认数据 26 | $this->assertEquals([ 27 | '--default--' => [ 28 | 'value' => [$message] 29 | ] 30 | ], apcu_fetch(Cache::GetChannelKey($channel))); 31 | // 确认无锁 32 | $this->assertEquals([], Cache::LockInfo()); 33 | // 清理 34 | apcu_delete(Cache::GetChannelKey($channel)); 35 | } 36 | 37 | public function testChannelPublishByChild(): void 38 | { 39 | $channel = __FUNCTION__; 40 | $message = 'test'; 41 | // 初始化确认 42 | $this->assertEquals([], Cache::GetChannel($channel)); 43 | $this->assertEquals([], Cache::LockInfo()); 44 | // 子进程执行 45 | $this->childExec(static function (string $channel, string $message) { 46 | Cache::ChPublish($channel, $message); 47 | }, $channel, $message); 48 | // 确认数据 49 | $this->assertEquals([ 50 | '--default--' => [ 51 | 'value' => [$message] 52 | ] 53 | ], apcu_fetch(Cache::GetChannelKey($channel))); 54 | // 确认无锁 55 | $this->assertEquals([], Cache::LockInfo()); 56 | // 清理 57 | apcu_delete(Cache::GetChannelKey($channel)); 58 | } 59 | 60 | public function testChannelCreateListener(): void 61 | { 62 | Future::$debug = true; 63 | $channel = __FUNCTION__; 64 | $message = 'test'; 65 | // 初始化确认 66 | $this->assertEquals([], Cache::GetChannel($channel)); 67 | $this->assertEquals([], Cache::LockInfo()); 68 | // 当前进程执行 69 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 70 | dump($key, $workerId, $message); 71 | }); 72 | // 确认数据 73 | $this->assertEquals([ 74 | '1' => [ 75 | 'futureId' => 1, 76 | 'value' => [] 77 | ] 78 | ], apcu_fetch(Cache::GetChannelKey($channel))); 79 | // 确认无锁 80 | $this->assertEquals([], Cache::LockInfo()); 81 | // 清理 82 | apcu_delete(Cache::GetChannelKey($channel)); 83 | } 84 | 85 | public function testChannelCreateListenerByChild(): void 86 | { 87 | Future::$debug = true; 88 | $channel = __FUNCTION__; 89 | $message = 'test'; 90 | 91 | // 初始化确认 92 | $this->assertEquals([], Cache::GetChannel($channel)); 93 | $this->assertEquals([], Cache::LockInfo()); 94 | // 子进程执行 95 | $this->childExec(static function (string $channel, string $message) { 96 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 97 | dump($key, $workerId, $message); 98 | }); 99 | }, $channel, $message); 100 | // 确认数据 101 | $this->assertEquals([ 102 | '1' => [ 103 | 'futureId' => 1, 104 | 'value' => [] 105 | ] 106 | ], apcu_fetch(Cache::GetChannelKey($channel))); 107 | // 确认无锁 108 | $this->assertEquals([], Cache::LockInfo()); 109 | // 清理 110 | apcu_delete(Cache::GetChannelKey($channel)); 111 | } 112 | 113 | public function testChannelPublishAfterCreateListener(): void 114 | { 115 | Future::$debug = true; 116 | $channel = __FUNCTION__; 117 | $message = 'test'; 118 | // 初始化确认 119 | $this->assertEquals([], Cache::GetChannel($channel)); 120 | $this->assertEquals([], Cache::LockInfo()); 121 | // 当前进程执行 122 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 123 | dump($key, $workerId, $message); 124 | }); 125 | Cache::ChPublish($channel, $message); 126 | // 确认数据 127 | $this->assertEquals([ 128 | '1' => [ 129 | 'futureId' => 1, 130 | 'value' => [$message] 131 | ] 132 | ], apcu_fetch(Cache::GetChannelKey($channel))); 133 | // 确认无锁 134 | $this->assertEquals([], Cache::LockInfo()); 135 | // 清理 136 | apcu_delete(Cache::GetChannelKey($channel)); 137 | } 138 | 139 | public function testChannelPublishAfterCreateListenerByChild(): void 140 | { 141 | Future::$debug = true; 142 | $channel = __FUNCTION__; 143 | $message = 'test'; 144 | // 初始化确认 145 | $this->assertEquals([], Cache::GetChannel($channel)); 146 | $this->assertEquals([], Cache::LockInfo()); 147 | // 子进程执行 148 | $this->childExec(static function (string $channel, string $message) { 149 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 150 | dump($key, $workerId, $message); 151 | }); 152 | Cache::ChPublish($channel, $message); 153 | }, $channel, $message); 154 | // 确认数据 155 | $this->assertEquals([ 156 | '1' => [ 157 | 'futureId' => 1, 158 | 'value' => [$message] 159 | ] 160 | ], apcu_fetch(Cache::GetChannelKey($channel))); 161 | // 确认无锁 162 | $this->assertEquals([], Cache::LockInfo()); 163 | // 清理 164 | apcu_delete(Cache::GetChannelKey($channel)); 165 | } 166 | 167 | public function testChannelRemoveListener(): void 168 | { 169 | Future::$debug = true; 170 | $channel = __FUNCTION__; 171 | // 初始化确认 172 | $this->assertEquals([], Cache::GetChannel($channel)); 173 | $this->assertEquals([], Cache::LockInfo()); 174 | // 当前进程执行 175 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 176 | dump($key, $workerId, $message); 177 | }); 178 | // 确认数据 179 | $this->assertEquals([ 180 | '1' => [ 181 | 'futureId' => 1, 182 | 'value' => [] 183 | ] 184 | ], apcu_fetch(Cache::GetChannelKey($channel))); 185 | // 确认无锁 186 | $this->assertEquals([], Cache::LockInfo()); 187 | Cache::ChRemoveListener($channel, '1', false); 188 | // 确认数据 189 | $this->assertEquals([ 190 | '1' => [ 191 | 'futureId' => 1, 192 | 'value' => [] 193 | ] 194 | ], apcu_fetch(Cache::GetChannelKey($channel))); 195 | // 确认无锁 196 | $this->assertEquals([], Cache::LockInfo()); 197 | // 清理 198 | apcu_delete(Cache::GetChannelKey($channel)); 199 | } 200 | 201 | public function testChannelRemoveListenerUseRemove(): void 202 | { 203 | Future::$debug = true; 204 | $channel = __FUNCTION__; 205 | // 初始化确认 206 | $this->assertEquals([], Cache::GetChannel($channel)); 207 | $this->assertEquals([], Cache::LockInfo()); 208 | // 当前进程执行 209 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 210 | dump($key, $workerId, $message); 211 | }); 212 | // 确认数据 213 | $this->assertEquals([ 214 | '1' => [ 215 | 'futureId' => 1, 216 | 'value' => [] 217 | ] 218 | ], apcu_fetch(Cache::GetChannelKey($channel))); 219 | // 确认无锁 220 | $this->assertEquals([], Cache::LockInfo()); 221 | Cache::ChRemoveListener($channel, '1', true); 222 | // 确认数据 223 | $this->assertEquals([], apcu_fetch(Cache::GetChannelKey($channel))); 224 | // 确认无锁 225 | $this->assertEquals([], Cache::LockInfo()); 226 | // 清理 227 | apcu_delete(Cache::GetChannelKey($channel)); 228 | } 229 | 230 | public function testChannelRemoveListenerByChild(): void 231 | { 232 | Future::$debug = true; 233 | $channel = __FUNCTION__; 234 | $message = 'test'; 235 | // 初始化确认 236 | $this->assertEquals([], Cache::GetChannel($channel)); 237 | $this->assertEquals([], Cache::LockInfo()); 238 | // 子进程执行 239 | $this->childExec(static function (string $channel, string $message) { 240 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 241 | dump($key, $workerId, $message); 242 | }); 243 | Cache::ChRemoveListener($channel, '1', false); 244 | }, $channel, $message); 245 | // 确认数据 246 | $this->assertEquals([ 247 | '1' => [ 248 | 'futureId' => 1, 249 | 'value' => [] 250 | ] 251 | ], apcu_fetch(Cache::GetChannelKey($channel))); 252 | // 确认无锁 253 | $this->assertEquals([], Cache::LockInfo()); 254 | // 清理 255 | apcu_delete(Cache::GetChannelKey($channel)); 256 | } 257 | 258 | public function testChannelRemoveListenerUseRemoveByChild(): void 259 | { 260 | Future::$debug = true; 261 | $channel = __FUNCTION__; 262 | $message = 'test'; 263 | // 初始化确认 264 | $this->assertEquals([], Cache::GetChannel($channel)); 265 | $this->assertEquals([], Cache::LockInfo()); 266 | // 子进程执行 267 | $this->childExec(static function (string $channel, string $message) { 268 | Cache::ChCreateListener($channel, '1', function (string $key, string|int $workerId, mixed $message) { 269 | dump($key, $workerId, $message); 270 | }); 271 | Cache::ChRemoveListener($channel, '1', true); 272 | }, $channel, $message); 273 | // 确认数据 274 | $this->assertEquals([], apcu_fetch(Cache::GetChannelKey($channel))); 275 | // 确认无锁 276 | $this->assertEquals([], Cache::LockInfo()); 277 | // 清理 278 | apcu_delete(Cache::GetChannelKey($channel)); 279 | } 280 | } -------------------------------------------------------------------------------- /tests/GenericTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([], Cache::LockInfo()); 17 | apcu_entry($key, function () use ($func, $timestamp, $params) { 18 | return [ 19 | 'timestamp' => $timestamp, 20 | 'method' => $func, 21 | 'params' => $params 22 | ]; 23 | }); 24 | $this->assertContainsEquals([ 25 | 'timestamp' => $timestamp, 26 | 'method' => $func, 27 | 'params' => $params 28 | ], Cache::LockInfo()); 29 | // 清理 30 | apcu_delete($key); 31 | } 32 | 33 | public function testKeyInfo(): void 34 | { 35 | $key = __METHOD__; 36 | $func = __FUNCTION__; 37 | $this->assertEquals([], Cache::KeyInfo($key)); 38 | apcu_store($key, $func); 39 | $info = Cache::KeyInfo($key); 40 | $this->assertArrayHasKey('hits', $info); 41 | $this->assertArrayHasKey('access_time', $info); 42 | $this->assertArrayHasKey('mtime', $info); 43 | $this->assertArrayHasKey('creation_time', $info); 44 | $this->assertArrayHasKey('deletion_time', $info); 45 | $this->assertArrayHasKey('ttl', $info); 46 | $this->assertArrayHasKey('refs', $info); 47 | // 清理 48 | apcu_delete($key); 49 | } 50 | 51 | public function testInfo(): void 52 | { 53 | $info = Cache::Info(); 54 | $this->assertArrayHasKey('ttl', $info); 55 | $this->assertArrayHasKey('num_hits', $info); 56 | $this->assertArrayHasKey('num_misses', $info); 57 | $this->assertArrayHasKey('num_inserts', $info); 58 | $this->assertArrayHasKey('num_entries', $info); 59 | $this->assertArrayHasKey('expunges', $info); 60 | $this->assertArrayHasKey('start_time', $info); 61 | $this->assertArrayHasKey('mem_size', $info); 62 | $this->assertArrayHasKey('memory_type', $info); 63 | $this->assertArrayHasKey('cache_list', $info); 64 | } 65 | 66 | public function testSearch(): void 67 | { 68 | $key = __FUNCTION__; 69 | apcu_store("$key-1", 1); 70 | apcu_store("$key-2", 1); 71 | apcu_store("$key--1", 1); 72 | apcu_store("$key--2", 1); 73 | 74 | $res = []; 75 | Cache::Search("/^$key.+$/", function ($key, $value) use (&$res) { 76 | $res[$key] = $value; 77 | }); 78 | $this->assertEquals([ 79 | "$key-1" => 1, 80 | "$key-2" => 1, 81 | "$key--1" => 1, 82 | "$key--2" => 1, 83 | ], $res); 84 | 85 | $res = []; 86 | Cache::Search(Cache::WildcardToRegex("$key-*"), function ($key, $value) use (&$res) { 87 | $res[$key] = $value; 88 | }); 89 | $this->assertEquals([ 90 | "$key-1" => 1, 91 | "$key-2" => 1, 92 | "$key--1" => 1, 93 | "$key--2" => 1, 94 | ], $res); 95 | 96 | $res = []; 97 | Cache::Search(Cache::WildcardToRegex("$key-?"), function ($key, $value) use (&$res) { 98 | $res[$key] = $value; 99 | }); 100 | $this->assertEquals([ 101 | "$key-1" => 1, 102 | "$key-2" => 1, 103 | ], $res); 104 | 105 | apcu_delete("$key-1"); 106 | apcu_delete("$key-2"); 107 | apcu_delete("$key--1"); 108 | apcu_delete("$key--2"); 109 | } 110 | 111 | public function testAtomic(): void 112 | { 113 | $lockKey = Cache::GetLockKey($key = __METHOD__); 114 | $this->assertTrue(Cache::Atomic($key, function () { 115 | return true; 116 | })); 117 | 118 | $this->assertTrue(Cache::Atomic($key, function () use ($key) { 119 | Cache::Set("$key-1", "$key-1"); 120 | Cache::Set("$key-2", "$key-2"); 121 | })); 122 | $this->assertEquals("$key-1", Cache::Get("$key-1")); 123 | $this->assertEquals("$key-2", Cache::Get("$key-2")); 124 | apcu_delete("$key-1"); 125 | apcu_delete("$key-2"); 126 | 127 | apcu_store($lockKey, 1); 128 | $this->assertFalse(Cache::Atomic($key, function () { 129 | return true; 130 | })); 131 | apcu_delete($lockKey); 132 | 133 | apcu_store($lockKey, 1); 134 | $this->assertFalse(Cache::Atomic($key, function () use ($key) { 135 | Cache::Set("$key-1", "$key-1"); 136 | Cache::Set("$key-2", "$key-2"); 137 | })); 138 | $this->assertNull(Cache::Get("$key-1")); 139 | $this->assertNull(Cache::Get("$key-2")); 140 | apcu_delete($lockKey); 141 | } 142 | } -------------------------------------------------------------------------------- /tests/HashTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(null, Cache::HGet($key, $hash)); 15 | apcu_add($key, [ 16 | $hash => [ 17 | '_value' => $hash, 18 | '_ttl' => 0, 19 | '_timestamp' => time() 20 | ] 21 | ]); 22 | $this->assertEquals([], Cache::LockInfo()); 23 | $this->assertEquals($hash, Cache::HGet($key, $hash)); 24 | // 清理 25 | apcu_delete($key); 26 | 27 | // 子进程执行 28 | $this->assertEquals(null, Cache::HGet($key, $hash)); 29 | $this->childExec(static function (string $key, string $hash) { 30 | apcu_add($key, [ 31 | $hash => [ 32 | '_value' => $hash, 33 | '_ttl' => 0, 34 | '_timestamp' => time() 35 | ] 36 | ]); 37 | }, $key, $hash); 38 | $this->assertEquals([], Cache::LockInfo()); 39 | $this->assertEquals($hash, Cache::HGet($key, $hash)); 40 | // 清理 41 | apcu_delete($key); 42 | } 43 | 44 | public function testHashSet(): void 45 | { 46 | $key = __METHOD__; 47 | $hash = 'test'; 48 | // 单进程执行 49 | $this->assertFalse(apcu_fetch($key)); 50 | $this->assertTrue(Cache::HSet($key, $hash, $hash)); 51 | $this->assertEquals([], Cache::LockInfo()); 52 | $this->assertEquals([ 53 | $hash => [ 54 | '_value' => $hash, 55 | '_ttl' => 0, 56 | '_timestamp' => time() 57 | ] 58 | ], apcu_fetch($key)); 59 | // 清理 60 | apcu_delete($key); 61 | 62 | // 子进程执行 63 | $this->assertFalse(apcu_fetch($key)); 64 | $this->childExec(static function (string $key, string $hash) { 65 | Cache::HSet($key, $hash, $hash); 66 | }, $key, $hash); 67 | $this->assertEquals([], Cache::LockInfo()); 68 | $this->assertEquals([ 69 | $hash => [ 70 | '_value' => $hash, 71 | '_ttl' => 0, 72 | '_timestamp' => time() 73 | ] 74 | ], apcu_fetch($key)); 75 | // 清理 76 | apcu_delete($key); 77 | } 78 | 79 | public function testHashDel(): void 80 | { 81 | $key = __METHOD__; 82 | // 在单进程内 83 | apcu_add($key, [ 84 | 'a' => 1, 85 | 'b' => 2 86 | ]); 87 | $this->assertTrue(Cache::HDel($key, 'a')); 88 | $this->assertEquals([], Cache::LockInfo()); 89 | $this->assertEquals([ 90 | 'b' => 2 91 | ], apcu_fetch($key)); 92 | // 清理 93 | apcu_delete($key); 94 | 95 | // 在子进程内 96 | apcu_add($key, [ 97 | 'a' => 1, 98 | 'b' => 2 99 | ]); 100 | $this->childExec(static function (string $key) { 101 | Cache::HDel($key, 'b'); 102 | }, $key); 103 | $this->assertEquals([], Cache::LockInfo()); 104 | $this->assertEquals([ 105 | 'a' => 1 106 | ],apcu_fetch($key)); 107 | // 清理 108 | apcu_delete($key); 109 | } 110 | 111 | public function testHashExists(): void 112 | { 113 | $key = __METHOD__; 114 | 115 | $this->assertEquals([], Cache::HExists($key, 'a')); 116 | apcu_add($key, [ 117 | 'a' => [ 118 | '_value' => 1 119 | ], 120 | 'b' => [ 121 | '_value' => 2 122 | ] 123 | ]); 124 | $this->assertEquals([ 125 | 'a' => true, 'b' => true 126 | ], Cache::HExists($key, 'a', 'b', 'c')); 127 | $this->assertEquals([], Cache::LockInfo()); 128 | // 清理 129 | apcu_delete($key); 130 | } 131 | 132 | public function testHashIncr(): void 133 | { 134 | $key = __METHOD__; 135 | // 在单进程内 136 | $this->assertFalse(apcu_fetch($key)); 137 | $this->assertEquals(1, Cache::HIncr($key, 'a')); 138 | $this->assertEquals([ 139 | 'a' => [ 140 | '_value' => 1, 141 | '_ttl' => 0, 142 | '_timestamp' => time() 143 | ] 144 | ], apcu_fetch($key)); 145 | $this->assertEquals(3, Cache::HIncr($key, 'a', 2)); 146 | $this->assertEquals([ 147 | 'a' => [ 148 | '_value' => 3, 149 | '_ttl' => 0, 150 | '_timestamp' => time() 151 | ] 152 | ], apcu_fetch($key)); 153 | $this->assertEquals(4.1, Cache::HIncr($key, 'a', 1.1)); 154 | $this->assertEquals([ 155 | 'a' => [ 156 | '_value' => 4.1, 157 | '_ttl' => 0, 158 | '_timestamp' => time() 159 | ] 160 | ], apcu_fetch($key)); 161 | // 清理 162 | apcu_delete($key); 163 | 164 | // 在子进程内 165 | $this->assertFalse(apcu_fetch($key)); 166 | $this->childExec(static function (string $key) { 167 | Cache::HIncr($key, 'a'); 168 | }, $key); 169 | $this->assertEquals([ 170 | 'a' => [ 171 | '_value' => 1, 172 | '_ttl' => 0, 173 | '_timestamp' => time() 174 | ] 175 | ], apcu_fetch($key)); 176 | $this->childExec(static function (string $key) { 177 | Cache::HIncr($key, 'a',2); 178 | }, $key); 179 | $this->assertEquals([ 180 | 'a' => [ 181 | '_value' => 3, 182 | '_ttl' => 0, 183 | '_timestamp' => time() 184 | ] 185 | ], apcu_fetch($key)); 186 | $this->childExec(static function (string $key) { 187 | Cache::HIncr($key, 'a',1.1); 188 | }, $key); 189 | $this->assertEquals([ 190 | 'a' => [ 191 | '_value' => 4.1, 192 | '_ttl' => 0, 193 | '_timestamp' => time() 194 | ] 195 | ], apcu_fetch($key)); 196 | // 清理 197 | apcu_delete($key); 198 | } 199 | 200 | public function testHashDecr(): void 201 | { 202 | $key = __METHOD__; 203 | // 在单进程内 204 | $this->assertFalse(apcu_fetch($key)); 205 | $this->assertEquals(-1, Cache::HDecr($key, 'a')); 206 | $this->assertEquals([ 207 | 'a' => [ 208 | '_value' => -1, 209 | '_ttl' => 0, 210 | '_timestamp' => time() 211 | ] 212 | ], apcu_fetch($key)); 213 | $this->assertEquals(-3, Cache::HDecr($key, 'a', 2)); 214 | $this->assertEquals([ 215 | 'a' => [ 216 | '_value' => -3, 217 | '_ttl' => 0, 218 | '_timestamp' => time() 219 | ] 220 | ], apcu_fetch($key)); 221 | $this->assertEquals(-4.1, Cache::HDecr($key, 'a', 1.1)); 222 | $this->assertEquals([ 223 | 'a' => [ 224 | '_value' => -4.1, 225 | '_ttl' => 0, 226 | '_timestamp' => time() 227 | ] 228 | ], apcu_fetch($key)); 229 | // 清理 230 | apcu_delete($key); 231 | 232 | // 在子进程内 233 | $this->assertFalse(apcu_fetch($key)); 234 | $this->childExec(static function (string $key) { 235 | Cache::HDecr($key, 'a'); 236 | }, $key); 237 | $this->assertEquals([ 238 | 'a' => [ 239 | '_value' => -1, 240 | '_ttl' => 0, 241 | '_timestamp' => time() 242 | ] 243 | ], apcu_fetch($key)); 244 | $this->childExec(static function (string $key) { 245 | Cache::HDecr($key, 'a', 2); 246 | }, $key); 247 | $this->assertEquals([ 248 | 'a' => [ 249 | '_value' => -3, 250 | '_ttl' => 0, 251 | '_timestamp' => time() 252 | ] 253 | ], apcu_fetch($key)); 254 | $this->childExec(static function (string $key) { 255 | Cache::HDecr($key, 'a', 1.1); 256 | }, $key); 257 | $this->assertEquals([ 258 | 'a' => [ 259 | '_value' => -4.1, 260 | '_ttl' => 0, 261 | '_timestamp' => time() 262 | ] 263 | ], apcu_fetch($key)); 264 | // 清理 265 | apcu_delete($key); 266 | } 267 | } -------------------------------------------------------------------------------- /tests/simple-benchmark.php: -------------------------------------------------------------------------------- 1 | pconnect('host.docker.internal'); 7 | 8 | $count = 100000; 9 | 10 | $interval = 0; 11 | dump("count: $count", "interval: $interval μs"); 12 | $start = microtime(true); 13 | for ($i = 0; $i < $count; $i ++) { 14 | $redis->set('test-redis', $i); 15 | } 16 | dump('redis: ' . microtime(true) - $start); 17 | $redis->del('test-redis'); 18 | 19 | $start = microtime(true); 20 | for ($i = 0; $i < $count; $i ++) { 21 | \Workbunny\WebmanSharedCache\Cache::Set('test-cache', $i); 22 | } 23 | dump('cache: ' . microtime(true) - $start); 24 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 25 | dump('-----------------------------------'); 26 | 27 | $interval = 1; 28 | dump("count: $count", "interval: $interval μs"); 29 | $start = microtime(true); 30 | for ($i = 0; $i < $count; $i ++) { 31 | $redis->set('test-redis', $i); 32 | usleep($interval); 33 | } 34 | dump('redis: ' . microtime(true) - $start); 35 | $redis->del('test-redis'); 36 | 37 | $start = microtime(true); 38 | for ($i = 0; $i < $count; $i ++) { 39 | \Workbunny\WebmanSharedCache\Cache::Set('test-cache', $i); 40 | usleep($interval); 41 | } 42 | dump('cache: ' . microtime(true) - $start); 43 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 44 | dump('-----------------------------------'); 45 | 46 | $interval = 10; 47 | dump("count: $count", "interval: $interval μs"); 48 | $start = microtime(true); 49 | for ($i = 0; $i < $count; $i ++) { 50 | $redis->set('test-redis', $i); 51 | usleep($interval); 52 | } 53 | dump('redis: ' . microtime(true) - $start); 54 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 55 | $redis->del('test-redis'); 56 | 57 | $start = microtime(true); 58 | for ($i = 0; $i < $count; $i ++) { 59 | \Workbunny\WebmanSharedCache\Cache::Set('test-cache', $i); 60 | usleep($interval); 61 | } 62 | dump('cache: ' . microtime(true) - $start); 63 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 64 | dump('-----------------------------------'); 65 | 66 | $interval = 100; 67 | dump("count: $count", "interval: $interval μs"); 68 | $start = microtime(true); 69 | for ($i = 0; $i < $count; $i ++) { 70 | $redis->set('test-redis', $i); 71 | usleep($interval); 72 | } 73 | dump('redis: ' . microtime(true) - $start); 74 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 75 | $redis->del('test-redis'); 76 | 77 | $start = microtime(true); 78 | for ($i = 0; $i < $count; $i ++) { 79 | \Workbunny\WebmanSharedCache\Cache::Set('test-cache', $i); 80 | usleep($interval); 81 | } 82 | dump('cache: ' . microtime(true) - $start); 83 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 84 | dump('-----------------------------------'); 85 | 86 | $interval = 1000; 87 | dump("count: $count", "interval: $interval μs"); 88 | $start = microtime(true); 89 | for ($i = 0; $i < $count; $i ++) { 90 | $redis->set('test-redis', $i); 91 | usleep($interval); 92 | } 93 | dump('redis: ' . microtime(true) - $start); 94 | $redis->del('test-redis'); 95 | 96 | $start = microtime(true); 97 | for ($i = 0; $i < $count; $i ++) { 98 | \Workbunny\WebmanSharedCache\Cache::Set('test-cache', $i); 99 | usleep($interval); 100 | } 101 | dump('cache: ' . microtime(true) - $start); 102 | \Workbunny\WebmanSharedCache\Cache::Del('test-cache'); 103 | dump('-----------------------------------'); --------------------------------------------------------------------------------