業務場景分析
使用者購買商品的邏輯中,需要對使用者錢包的餘額進行查詢和扣款
異常:如果同一使用者併發執行多個業務進行”查詢+扣款”的業務中有一定概率出現資料不一致
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
Step2: 業務A和業務B分別扣款邏輯處理,業務A商品70結果餘額30,業務B商品80結果餘額20
Step3:
1 業務A先進行修改,修改餘額為30
2 業務A後進行修改,修改餘額為20
此時異常出現了,原餘額100元,業務A和業務B的商品價格總和150元(70+80)都購買成功且餘額還剩20元。
異常點:業務A和業務B並行
查詢餘額為100
解決方案
悲觀鎖
使用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 協議》,轉載必須註明作者和本文連結