欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

程序员文章站 2022-05-04 15:05:38
...

 

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)业务场景分析

用户购买商品的逻辑中,需要对用户钱包的余额进行查询和扣款

异常:如果同一用户并发执行多个业务进行” 查询 + 扣款” 的业务中有一定概率出现数据不一致

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

 

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

Step2: 业务 A 和业务 B 分别扣款逻辑处理,业务 A 商品 70 结果余额 30,业务 B 商品 80 结果余额 20

 

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

Step3:

1 业务 A 先进行修改,修改余额为 30

 

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

2 业务 A 后进行修改,修改余额为 20

 

PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

此时异常出现了,原余额 100 元,业务 A 和业务 B 的商品价格总和 150 元(70+80)都购买成功且余额还剩 20 元。

异常点:业务 A 和业务 B 并行查询余额为 100

解决方案

悲观锁PHP 并发扣款,保证数据一致性(悲观锁和乐观锁)

使用 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');
    }
}

乐观锁

使用 CAS(Compare And Set)

在 set 写回的时候,加上初始状态的条件 compare, 只有初始状态不变的时候才允许 set 写回成功,保证数据一致性的方法

将:

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

改为:

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

这样的话并发操作时只有一个是执行成功的,根据 affect rows 是否为 1 判断是否成功

结语

  • 解决方案有很多,这只是其中一种解决方案
  • 使用 Redis 悲观锁的方案会降低吞吐量

php 高并发

本作品采用《CC 协议》,转载必须注明作者和本文链接
相关标签: 乐观锁 悲观锁