业务场景分析在用户购买商品的逻辑中,需要查询用户钱包余额并扣款异常:如果同一用户并发执行多个业务,进行“查询+扣款”业务,存在一定的数据不一致的概率Tips:如果没有限制单个接口的请求频率,用户并发请求的方法也可能会出现数据不一致的情况。扣款场景Step1:从数据库中查询用户钱包余额SELECTbalanceFROMuser_walletWHEREuid=$uid;+----------+|余额|+--------+|100|+--------+1rowinset(0.02sec)Step2:业务逻辑Tips:同一个用户的文章共享和并发扣费处理支付的一致性,检查库存逻辑等跳过1.查询商品价格,比如70元2.查看商品价格相对于余额是否足够,如果足够,扣除货款,提交订单逻辑if(goodsPrice<=userBalance){$newUserBalance=userBalance-goodsPrice;}else{thrownewUserWalletException(['msg'=>'Insufficientuserbalance']);}Step3:修改数据库的余额UPDATEuser_walletSETbalance=$newUserBalanceWHEREuid=$uidwithoutconcurrency,没有问题这个过程,原来的余额是100,购买了70元的产品,剩下的30元是不正常的。场景ar414Step1:用户同时购买业务A和业务B(不同的实例/服务),以一定概率并行查询余额为100Step2:业务A与业务B进行单独扣款逻辑处理,业务A产品70导致余额为30,业务B的产品80导致余额为20Step3:1先修改业务A,修改后的余额为302,然后修改业务A,修改后的余额为20,此时出现异常是的,原余额100元,商家A和商家B的商品价格总和为150元(70+80),购买成功,余额为20元。异常点:业务A和业务B并行查询余额为100解决悲观锁使用Redis悲观锁,比如抢到一个KEY继续操作,否则禁止操作封装了一个开箱即用的RedisLockconnect('127.0.0.1','6379');$lockTimeOut=5;$redisLock=newRedisLock($redis,$lockTimeOut);$lockKey='lock:user:wallet:uid:1001';$lockExpire=$redisLock->getLock($lockKey);if($lockExpire){try{//selectuserwalletbalanceforuid$userBalance=100;//选择商品价格为goods_id$goodsPrice=80;如果($userBalance>=$goodsPrice){$newUserBalance=$userBalance-$goodsPrice;//TODO在数据库中设置用户余额}else{thrownewException('用户余额不足');$redisLock->releaseLock($lockKey,$lockExpire);}catch(\Throwable$throwable){$redisLock->releaseLock($lockKey,$lockExpire);thrownewException('网络繁忙');}}乐观锁在set中使用CAS(CompareAndSet)回写这时候,加上初始状态的条件比较,只有初始状态不变,才允许set回写成功。保证数据一致性的方法是:UPDATEuser_walletSETbalance=$newUserBalanceWHEREuid=$uid改为:UPDATEuser_walletSETbalance=$newUserBalanceWHEREuid=$uidANDbalance=$oldUserBalance这种情况只有一个并发操作成功,根据影响的行是否为1判断是否成功结论解决方案很多,这个只是其中之一。使用Redis悲观锁方案会降低吞吐量
