hyperf-wise-locksmith,一個高效的PHP分散式鎖方案

左诗右码發表於2024-11-28

在分散式系統中,如何確保多臺機器之間不會產生競爭條件,是一個常見且重要的問題。hyperf-wise-locksmith 庫作為 Hyperf 框架中的一員,提供了一個高效、簡潔的互斥鎖解決方案。

本文將帶你瞭解這個庫的安裝、特性、基本與高階功能,並結合實際應用場景,展示其在專案中的應用。

hyperf-wise-locksmith 庫簡介

hyperf-wise-locksmith 是一個適配 Hyperf 框架的互斥鎖庫,它基於 pudongping/wise-locksmith 庫構建。它可以幫助我們在分散式環境下進行鎖的管理,確保同一時刻只有一個程序能夠操作某些共享資源,從而避免資料的競爭和不一致問題。

安裝

要在你的 Hyperf 專案中使用 hyperf-wise-locksmith,你需要透過 Composer 進行安裝:

composer require pudongping/hyperf-wise-locksmith -vvv

確保你的環境滿足以下要求:

  • PHP >= 8.0
  • hyperf ~3.0.0。

特性

hyperf-wise-locksmith 提供了多種鎖機制,包括檔案鎖分散式鎖紅鎖協程級別的互斥鎖。這些鎖機制可以幫助開發者在不同的場景下保護共享資源,避免競態條件。

基本功能

檔案鎖(flock)

檔案鎖是一種簡單的鎖機制,它依賴於檔案系統。以下是一個使用檔案鎖的示例:

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

分散式鎖(redisLock)

分散式鎖適用於分散式系統,它依賴於 Redis。以下是一個使用分散式鎖的示例:

    private function redisLock(float $amount)
    {
        $res = $this->locker->redisLock('redisLock', function () use ($amount) {
            return $this->deductBalance($amount);
        }, 10);
        return $res;
    }

高階功能

紅鎖(redLock)

紅鎖是一種更安全的分散式鎖實現,它需要多個 Redis 例項。以下是一個使用紅鎖的示例:

    private function redLock(float $amount)
    {
        $res = $this->locker->redLock('redLock', function () use ($amount) {
            return $this->deductBalance($amount);
        }, 10);
        return $res;
    }

協程級別的互斥鎖(channelLock)

協程級別的互斥鎖適用於協程環境,它提供了一種輕量級的鎖機制。以下是一個使用協程鎖的示例:

    private function channelLock(float $amount)
    {
        $res = $this->locker->channelLock('channelLock', function () use ($amount) {
            return $this->deductBalance($amount);
        });
        return $res;
    }

實際應用場景

假設我們有一個線上支付系統,需要在多個請求中扣減使用者的餘額。如果不使用互斥鎖,可能會導致超扣或扣減失敗。使用 hyperf-wise-locksmith 庫,我們可以確保每次扣減操作都是原子性的。

程式碼示例

以下是一個扣減使用者餘額的示例,使用了 hyperf-wise-locksmith 庫:

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

}

然後我們再寫一個控制器進行呼叫

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

}

當我們訪問 /balance/consumer?type=noMutex 地址時,我們可以看到使用者的餘額會被扣成負數,這明顯不符合邏輯。
然而當我們訪問下面幾個地址時,我們可以看到使用者餘額不會被扣成負數,則說明很好的保護了競態下的共享資源的準確性。

  • /balance/consumer?type=flock :檔案鎖
  • /balance/consumer?type=redisLock :分散式鎖
  • /balance/consumer?type=redLock :紅鎖
  • /balance/consumer?type=channelLock :協程級別的互斥鎖

注意

關於使用到 redisLockredLock 時:

  • 使用 redisLock 預設採用的 config/autoload/redis.php 配置檔案中的第一個 key 配置 redis 例項(即 default)。可按需傳入第 4 個引數 string|null $redisPoolName 進行重新指定。
  • 使用 redLock 預設採用的 config/autoload/redis.php 配置檔案中的所有 key 對應的配置 redis 例項。可按需傳入第 4 個引數 ?array $redisPoolNames = null 進行重新指定。

文件

詳細文件可見 pudongping/wise-locksmith

結語

hyperf-wise-locksmith 庫為 Hyperf 框架的開發者提供了強大的互斥鎖功能,可以幫助我們在高併發場景下保護共享資源。

透過本文的介紹,希望你能對 hyperf-wise-locksmith 有一個全面的瞭解,並在你的專案中靈活運用。如果你覺得這個庫對你有幫助,希望你可以幫忙點個 Star 喲~

相關文章