PHP 併發扣款,保證資料一致性(悲觀鎖)

AR414發表於2020-03-26

:elephant:業務場景分析

使用者購買商品的邏輯中,需要對使用者錢包的餘額進行查詢和扣款

異常:如果同一使用者併發執行多個業務進行”查詢+扣款”的業務中有一定概率出現資料不一致

Tips:如果沒有做限制單一介面請求頻率,使用者使用併發請求的手段也有概率出現資料不一致

扣款場景

Step1: 從資料庫查詢使用者錢包餘額

SELECT balance FROM user_wallet WHERE uid = $uid;
+---------+
| balance |
+---------+
| 100     |
+---------+
1 row in set (0.02 sec)

Step2: 業務邏輯

Tips: 文章分享處理同一使用者併發扣款一致性,檢查庫存啥的邏輯略過

1. 查詢商品價格,比如70元
2. 商品價格對比餘額是否足夠,足夠時進行扣款提交訂單邏輯

if(goodsPrice <= userBalance) {
    $newUserBalance = userBalance - goodsPrice;  
}else {
    throw new UserWalletException(['msg' => '使用者餘額不足']);
}

Step3: 將資料庫的餘額進行修改

UPDATE user_wallet SET balance=$newUserBalance WHERE uid = $uid

在沒有併發的情況下,這個流程沒有任何問題,原有餘額100,購買70元的商品,剩餘30元

異常場景

Step1: 使用者併發購買業務A和業務B(不同例項/服務),一定概率並行查詢餘額是100

step1

Step2: 業務A和業務B分別扣款邏輯處理,業務A商品70結果餘額30,業務B商品80結果餘額20

step1

Step3:

1 業務A先進行修改,修改餘額為30

step1

2 業務A後進行修改,修改餘額為20

step1

此時異常出現了,原餘額100元,業務A和業務B的商品價格總和150元(70+80)都購買成功且餘額還剩20元。

異常點:業務A和業務B並行查詢餘額為100

解決方案

悲觀鎖:lock:

使用Redis悲觀鎖,例如搶到一個KEY才能繼續操作,否則禁止操作

封裝了一個開箱即用的RedisLock

<?php

use Ar414\RedisLock;

$redis = new \Redis();
$redis->connect('127.0.0.1','6379');

$lockTimeOut = 5;
$redisLock = new RedisLock($redis,$lockTimeOut);

$lockKey    = 'lock:user:wallet:uid:1001';
$lockExpire = $redisLock->getLock($lockKey);

if($lockExpire) {
    try {
        //select user wallet balance for uid
        $userBalance = 100;
        //select goods price for goods_id
        $goodsPrice = 80;

        if($userBalance >= $goodsPrice) {
            $newUserBalance = $userBalance - $goodsPrice;
            //TODO set user balance in db
        }else {
            throw new Exception('user balance insufficient');
        }
        $redisLock->releaseLock($lockKey,$lockExpire);
    } catch (\Throwable $throwable) {
        $redisLock->releaseLock($lockKey,$lockExpire);
        throw new Exception('Busy network');
    }
}

樂觀鎖

待續…

結語

  • 解決方案有很多,這只是其中一種解決方案
  • 使用Redis悲觀鎖的方案會降低吞吐量
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章