關於分散式鎖在程式設計中的一些應用場景

fireqong發表於2022-10-13

分散式鎖在WEB程式設計中的應用相當廣泛

以介面冪等性為例,未實現介面冪等性的註冊介面,是這樣的:

<?php

public function register(Request $request) 
{
    //一些驗證邏輯

    $user = new User();
    $user->username = $request->post('phone');
    $user->password = Hash::make($request->post('password'));
    $user->save();
}

這樣寫有什麼問題?明顯,如果同一時間以相同的請求資料請求該介面,可能會導致資料庫有兩條同樣的記錄。誠然,我們可以為Users表的username欄位設定唯一索引,再catch異常來丟擲友好錯誤提示。但,這裡我想介紹另一種方法,用分散式鎖,以username作為分散式鎖key的構建。

public function register(Request $request) 
{
    //一些驗證邏輯
    $phone = $request->post('phone');

    $lock = Cache::lock('register-user-' . $phone, 10);

    if ($lock->get()) {
        $user = new User();
        $user->username = $phone;
        $user->password = Hash::make($request->post('password'));
        $user->save();
        $lock->release();
    } else {
        //返回友好的錯誤資訊
    }

}

同樣的,也可以編寫一個限制訪問頻次的中介軟體,以客戶端ip、路由和引數生成唯一指紋。

class AccessLimit
{
    public  function  handle($request, Closure  $next)
    {
        $fingerprint = md5($request->ip() . $request->route()->getName() . var_export($request->all(), true));

        $lock = Cache::lock($fingerprint, 5);
        if ($lock->get()) {
            return  $next($request);
        }

        throw new \Exception('訪問頻繁');

    }
}

還有一些非電商場景下的秒殺交易,比如說虛擬幣,掛單交易只允許被一個使用者購買。
如果不處理併發問題,就會發生超賣的情況。

<?php

public function deal(Request $request)
{
    $dealId = $request->post('id');
    $deal = Deal::find($dealId);

    //一些驗證操作

    if (is_null($deal->sell_time)) {    //狀態判斷邏輯
        DB::beginTransaction();

        try {
            //處理業務邏輯

            $deal->sell_time = date('Y-m-d H:i:s');
            $deal->save();

            DB::commit();
        } catch (\Exception $e) {
            DB::rollback();
        }


        return; //返回交易成功響應
    }

    throw new \Exception('交易失敗響應');


}

這樣寫在高併發場景下有明顯的問題,兩個使用者同時搶單時,狀態判斷邏輯同時成功,並執行到接下來的業務邏輯。怎麼改進?用分散式鎖。

<?php

public function deal(Request $request)
{
    $dealId = $request->post('id');

    $lock = Cache::lock('deal-' . $dealId, 10);

    if (!$lock->get()) {
        throw new \Exception('該掛單已交易完成');
    }
    $deal = Deal::find($dealId);

    //一些驗證操作

    DB::beginTransaction();

    try {
        //處理業務邏輯

        if (!is_null($deal->sell_time)) {    //狀態判斷邏輯
            throw new \Exception('該掛單已被交易');
        }

        $deal->sell_time = date('Y-m-d H:i:s');
        $deal->save();

        DB::commit();
        //返回成功響應
    } catch (\Exception $e) {
        DB::rollback();
        //返回失敗響應
    } finally {
        $lock->release();
    }


}

這樣處理,即可以避免超賣,也可以避免請求頻繁落到DB層,提升整體的系統吞吐量。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章