继续解答星球水友提问.
===
沈老师, 我们有个业务, 同一个用户在 并发 "查询, 逻辑计算, 扣款" 的情况下, 余额可能出现不一致, 请问有什么优化方法么?
===
扣款的业务场景是怎样的?
用户购买商品的过程中, 要对余额进行查询与修改, 大致的业务流程如下:
第一步 , 从数据库 查询
用户现有余额:
SELECT money FROM t_yue WHERE uid=$uid; 不妨设查询出来的 $old_money=100 元.
第二步 , 业务层实施 业务逻辑计算
, 比如:
(1) 先查询购买商品的价格, 例如是 80 元;
(2) 再查询产品是否有活动, 以及活动折扣, 例如是 9 折;
(3) 比对余额是否足够, 足够时才往下走;
- if($old_money> 80*0.9){
- $new_money=$old_money-80*0.9=28
- } else {
- return "Not enough minerals";
- }
第三步 , 将数据库中的余额进行 修改
- .
- UPDATE t_yue SET money=$new_money WHERE uid=$uid;
在并发量低的情况下, 这个流程没有任何问题, 原有金额 100 元, 购买了 80 元的九折商品 (72 元), 剩余 28 元.
同一个用户, 并发扣款可能出现什么问题?
在分布式环境中, 如果并发量很大, 这种 "查询 + 修改" 的业务有一定概率出现数据不一致.
极限情况下, 可能出现这样的异常流程:
步骤一 , 业务 1 和业务 2 并发查询余额, 是 100 元.
画外音: 这些并发查询, 是在不同的站点实例 / 服务实例上完成的, 进程内互斥锁肯定解决不了.
步骤二 , 业务 1 和业务 2 并发进行逻辑计算, 算出各自业务的余额, 假设业务 1 算出的余额是 28 元, 业务 2 算出的余额是 38 元.
步骤三 , 业务 1 对数据库中的余额先进行修改, 设置成 28 元.
业务 2 对数据库中的余额后进行修改, 设置成 38 元.
此时异常出现了, 原有金额 100 元, 业务 1 扣除了 72 元, 业务 2 扣除了 62 元, 最后剩余 38 元.
画外音: 假设业务 1 先写回余额, 业务 2 再写回余额.
常见的解决方案?
对于此案例, 同一个用户, 并发扣款时, 有小概率会出现异常, 可以对每一个用户进行 分布式锁互斥
, 例如: 在 Redis/zk 里抢到一个 key 才能继续操作, 否则禁止操作.
这种 悲观锁 方案确实可行, 但要引入额外的组件 (Redis/zk), 并且会降低吞吐量.
对于小概率的不一致, 有没有乐观锁的方案呢?
对并发扣款进行进一步的分析发现:
(1) 业务 1 写回时, 旧余额 100, 这是一个 初始状态 ; 新余额 28, 这是一个 结束状态
. 理论上只有在旧余额为 100 时, 新余额才应该写回成功.
而业务 1 并发写回时, 旧余额确实是 100, 理应写回成功.
(2) 业务 2 写回时, 旧余额 100, 这是一个 初始状态 ; 新余额 28, 这是一个 结束状态
. 理论上只有在旧余额为 100 时, 新余额才应该写回成功.
可实际上, 这个时候数据库中的金额已经变为 28 了, 所以业务 2 的并发写回, 不应该成功.
如何低成本实施乐观锁?
在 set 写回的时候, 加上初始状态的条件 compare, 只有初始状态不变时, 才允许 set 写回成功, Compare And Set (CAS), 是一种常见的降低读写锁冲突, 保证数据一致性的方法.
此时业务要怎么改?
使用 CAS 解决高并发时数据一致性问题, 只需要在进行 set 操作时, compare 初始值, 如果初始值变换, 不允许 set 成功.
具体到这个 case, 只需要将:
UPDATE t_yue SET money=$new_money WHERE uid=$uid; 升级为:
UPDATE t_yue SET money=$new_money WHERE uid=$uid AND money=$old_money ;
即可.
并发操作发生时:
业务 1 执行:
UPDATE t_yue SET money=28 WHERE uid=$uid AND money=100;
业务 2 执行:
UPDATE t_yue SET money=38 WHERE uid=$uid AND money=100;
这两个操作同时进行时, 只可能有一个执行成功.
怎么判断哪个并发执行成功, 哪个并发执行失败呢?
set 操作, 其实无所谓成功或者失败, 业务能 通过 affect rows 来判断 :
写回成功的, affect rows 为 1
写回失败的, affect rows 为 0
总结
高并发 "查询并修改" 的场景, 可以用 CAS (Compare and Set)
的方式解决数据一致性问题. 对应到业务, 即在 set 的时候, 加上初始条件的比对即可.
优化不难, 只改了半行 SQL, 但确实能解决问题.
但希望大家有收获 , 思路比结论重要 .
欢迎大家继续提问, 有问必答.
答球友问 : 《 用 DB 自增键生成 uid 了, 还能分库吗? 》
《 粉丝关系链, 10 亿数据, 如何设计? 》
《 几万条群离线消息, 如何高效拉取? 》
《 每秒 30W 次的点赞业务, 怎么优化? 》
作业
, 为什么不能使用:
UPDATE t_yue SET money=money-$diff;
来源: http://www.tuicool.com/articles/yaaae27