├── .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 |

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 |
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('-----------------------------------');
--------------------------------------------------------------------------------