美文网首页PHP经验分享
PHP 并发扣款,保证数据一致性(悲观锁)

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

作者: 9c1fd88cfd08 | 来源:发表于2020-03-26 19:33 被阅读0次

    业务场景分析

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

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

    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

    解决方案

    悲观锁

    使用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 并发扣款,保证数据一致性(悲观锁)

        本文链接:https://www.haomeiwen.com/subject/czchuhtx.html