PHP concurrent deduction to ensure data consistency
#🐘 Business scenario analysis
In the logic of the user's purchase of goods, it is necessary to query and deduct the balance of the user's wallet
Exception: If the same user executes multiple services concurrently, there is a certain probability that there will be data inconsistencies in the business of "query + deduction"
Tips: If there is no restriction on the frequency of requests for a single interface, users may also have data inconsistencies when using concurrent requests
#Deduction scene
#Step1: Query user wallet balance from database
SELECT balance FROM user_wallet WHERE uid = $uid;
+---------+
| balance |
+---------+
| 100 |
+---------+
1 row in set (0.02 sec)
#Step2: Business logic
Tips: Article sharing handles the consistency of concurrent deductions for the same user, skip the logic of checking inventory
- 查询商品价格,比如70元
- 商品价格对比余额是否足够,足够时进行扣款提交订单逻辑
if(goodsPrice <= userBalance) {
$newUserBalance = userBalance - goodsPrice;
}else {
throw new UserWalletException(['msg' => '用户余额不足']);
}
#Step3: Modify the balance of the database
UPDATE user_wallet SET balance=$newUserBalance WHERE uid = $uid
In the absence of concurrency, there is no problem with this process, the original balance is 100, the purchase of 70 yuan of goods, the remaining 30 yuan
#Abnormal scene
#Step1: The user concurrently purchases business A and business B (different instances/services), a certain probability of parallel
query balance is 100
#Step2: Business A and business B are separately deducted logically, business A product 70 results in a balance of 30, and business B product 80 results in a balance of 20
#Step3:
1.Business A is modified first, and the modified balance is 30
2.Modify after business B, and modify the balance to 20
At this time, an exception occurred. The original balance was 100 yuan, the sum of the product prices of business A and business B was 150 yuan (70+80), and the purchase was successful and the balance was 20 yuan.
Abnormal point: business A and business B parallel
query balance is 100
#solution
#Pessimistic lock: lock:
Use Redis pessimistic lock, such as grab a KEY to continue the operation, otherwise the operation is prohibited
Packaged RedisLock out of the box
<?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');
}
}
#Optimistic lock
Use CAS
(Compare And Set)
When the set is written back, the initial state condition compare
is added. Only when the initial state is unchanged, the set write back is allowed to succeed, and the method to ensure data consistency
will:
UPDATE user_wallet SET balance=$newUserBalance WHERE uid = $uid
to:
UPDATE user_wallet SET balance=$newUserBalance WHERE uid = $uid AND balance = $oldUserBalance
In this case, only one of the concurrent operations is executed successfully, and the success is determined according to whether the affect rows is 1
#Conclusion
- There are many solutions, this is just one solution
- Using Redis pessimistic lock scheme will reduce throughput
Did you enjoyed the article ?