浦东平/hyperf-wise-locksmith

Hyperf 框架的互斥库提供者,旨在实现高并发场景下 PHP 代码的序列化执行。

2.0.0 2023-08-24 05:56 UTC

This package is auto-updated.

Last update: 2024-09-24 08:17:51 UTC


README

需求 | 安装 | 分支或标签 | 快速入门 | 注意 | 文档 | 贡献 | 许可

hyperf-wise-locksmith

Latest Stable Version Total Downloads Latest Unstable Version Minimum PHP Version Packagist License

英文 | 中文

🔒 Hyperf 框架的互斥库提供者,旨在实现高并发场景下 PHP 代码的序列化执行。此库基于 pudongping/wise-locksmith

需求

  • PHP >= 8.0
  • hyperf ~3.0.0

安装

composer require pudongping/hyperf-wise-locksmith:^2.0 -vvv

分支或标签

分支

  • 2.2: 适用于 hyperf 2.2
  • 3.0: 适用于 hyperf 3.0

标签

  • 1.0.x: 适用于 hyperf 2.2
  • 2.0.x: 适用于 hyperf 3.0

快速入门

以下将提供一个高并发场景中扣除用户余额的示例,以演示此库的功能和用法。

创建 app\Controller\BalanceController.php 文件并写入以下代码

<?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);
    }

}

接下来,创建 app\Services\AccountBalanceService.php 文件并写入以下代码

<?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;
    }

}

当我们访问 /balance/consumer?type=noMutex URL 时,我们可以看到用户的余额变为负数,这显然是不合理的。然而,当我们访问以下 URL 时,我们可以看到用户的余额没有变为负数,这证明了在竞争条件下有效地保护共享资源准确性的有效性。

  • /balance/consumer?type=flock : 文件锁
  • /balance/consumer?type=redisLock : 分布式锁
  • /balance/consumer?type=redLock : Redlock
  • /balance/consumer?type=channelLock : 协程级别的互斥锁

注意

关于使用 redisLockredLock

  • 使用 redisLock 时,默认使用 config/autoload/redis.php 配置文件中的第一个 key 配置,这对应于 默认 Redis 实例。您可以选择传递第四个参数 string|null $redisPoolName 以根据需要重新指定不同的 Redis 实例。
  • 使用 redLock 时,默认使用 config/autoload/redis.php 配置文件中的所有 key 配置。您可以选择传递第四个参数 ?array $redisPoolNames = null 以根据需要重新指定不同的 Redis 实例。

文档

您可以在 pudongping/wise-locksmith 找到详细文档。

贡献

可以通过 问题跟踪器提交错误报告(以及小补丁)。对于重大补丁,建议通过分叉存储库并提交拉取请求。

许可

麻省理工学院,见许可文件