pudongping / hyperf-wise-locksmith
A mutex library provider for the Hyperf framework, designed to enable serialized execution of PHP code in high-concurrency scenarios.
Requires
- php: >=8.1
- hyperf/logger: ~3.1.0
- hyperf/redis: ~3.1.0
- pudongping/wise-locksmith: ^1.0
README
Requirements | Installation | Branches or tags | Quickstart | Note | Documentation | Contributing | License
hyperf-wise-locksmith
English | 中文
🔒 A mutex library provider for the Hyperf framework, designed to enable serialized execution of PHP code in high-concurrency scenarios. This library is based on pudongping/wise-locksmith.
Requirements
- PHP >= 8.1
- hyperf ~3.1.0
Installation
composer require pudongping/hyperf-wise-locksmith:^3.0 -vvv
Branches or tags
Branch
- 2.2: For hyperf 2.2
- 3.0: For hyperf ~3.0
- 3.1: For hyperf ~3.1
Tag
- 1.0.x: For hyperf 2.2
- 2.0.x: For hyperf 3.0
- 3.0.x: For hyperf 3.1
Quickstart
Below, an example of deducting user balances in a high-concurrency scenario will be provided to demonstrate the functionality and usage of this library.
Create the app\Controller\BalanceController.php
file and write the following code:
<?php declare(strict_types=1); namespace App\Controller; use Hyperf\HttpServer\Annotation\AutoController; use App\Services\AccountBalanceService; use Hyperf\Coroutine\Parallel; use function \Hyperf\Support\make; #[AutoController] class BalanceController extends AbstractController { // curl '127.0.0.1:9511/balance/consumer?type=noMutex' public function consumer() { $type = $this->request->input('type', 'noMutex'); $amount = (float)$this->request->input('amount', 1); $parallel = new Parallel(); $balance = make(AccountBalanceService::class); // 模拟 20 个并发 for ($i = 1; $i <= 20; $i++) { $parallel->add(function () use ($balance, $i, $type, $amount) { return $balance->runLock($i, $type, $amount); }, $i); } $result = $parallel->wait(); return $this->response->json($result); } }
Next, create the app\Services\AccountBalanceService.php
file and write the following code:
<?php declare(strict_types=1); namespace App\Services; use Hyperf\Contract\StdoutLoggerInterface; use Pudongping\HyperfWiseLocksmith\Locker; use Pudongping\WiseLocksmith\Exception\WiseLocksmithException; use Pudongping\WiseLocksmith\Support\Swoole\SwooleEngine; use Throwable; class AccountBalanceService { /** * 用户账户初始余额 * * @var float|int */ private float|int $balance = 10; public function __construct( private StdoutLoggerInterface $logger, private Locker $locker ) { $this->locker->setLogger($logger); } private function deductBalance(float|int $amount) { if ($this->balance >= $amount) { // 模拟业务处理耗时 usleep(500 * 1000); $this->balance -= $amount; } return $this->balance; } /** * @return float */ private function getBalance(): float { return $this->balance; } public function runLock(int $i, string $type, float $amount) { try { $start = microtime(true); switch ($type) { case 'flock': $this->flock($amount); break; case 'redisLock': $this->redisLock($amount); break; case 'redLock': $this->redLock($amount); break; case 'channelLock': $this->channelLock($amount); break; case 'noMutex': default: $this->deductBalance($amount); break; } $balance = $this->getBalance(); $id = SwooleEngine::id(); $cost = microtime(true) - $start; $this->logger->notice('[{type} {cost}] ==> [{i}<=>{id}] ==> 当前用户的余额为:{balance}', compact('type', 'i', 'balance', 'id', 'cost')); return $balance; } catch (WiseLocksmithException|Throwable $e) { return sprintf('Err Msg: %s ====> %s', $e, $e->getPrevious()); } } private function flock(float $amount) { $path = BASE_PATH . '/runtime/alex.lock.cache'; $fileHandler = fopen($path, 'a+'); // fwrite($fileHandler, sprintf("%s - %s \r\n", 'Locked', microtime())); $res = $this->locker->flock($fileHandler, function () use ($amount) { return $this->deductBalance($amount); }); return $res; } private function redisLock(float $amount) { $res = $this->locker->redisLock('redisLock', function () use ($amount) { return $this->deductBalance($amount); }, 10); return $res; } private function redLock(float $amount) { $res = $this->locker->redLock('redLock', function () use ($amount) { return $this->deductBalance($amount); }, 10); return $res; } private function channelLock(float $amount) { $res = $this->locker->channelLock('channelLock', function () use ($amount) { return $this->deductBalance($amount); }); return $res; } }
When we access the /balance/consumer?type=noMutex
URL, we can observe that the user's balance goes negative, which is clearly illogical. However, when we visit the following URLs, we can see that the user's balance is not going negative, demonstrating effective protection of the accuracy of shared resources in a race condition.
/balance/consumer?type=flock
: File lock/balance/consumer?type=redisLock
: Distributed lock/balance/consumer?type=redLock
: Redlock/balance/consumer?type=channelLock
: Coroutine-level mutex
Note
Regarding the use of redisLock
and redLock
:
- When using
redisLock
, it defaults to using the firstkey
configuration in theconfig/autoload/redis.php
configuration file, which corresponds to the default Redis instance. You can optionally pass the fourth parameterstring|null $redisPoolName
to re-specify a different Redis instance as needed. - When using
redLock
, it defaults to using all thekey
configurations in theconfig/autoload/redis.php
configuration file. You can optionally pass the fourth parameter?array $redisPoolNames = null
to re-specify different Redis instances as needed.
Documentation
You can find detailed documentation for pudongping/wise-locksmith.
Contributing
Bug reports (and small patches) can be submitted via the issue tracker. Forking the repository and submitting a Pull Request is preferred for substantial patches.
License
MIT, see LICENSE file.