likes
comments
collection
share

并发扣款,如何保证数据的一致性?

作者站长头像
站长
· 阅读数 29

继续解答星球水友提问。 沈老师,我们有个业务,同一个用户在并发 “查询,逻辑计算,扣款” 的情况下,余额可能出现不一致,请问有什么优化方法么?

扣款的业务场景是怎样的?

用户购买商品的过程中,要对余额进行查询与修改,大致的业务流程如下:

第一步,从数据库查询用户现有余额: _SELECT money FROM t_yue WHERE uid=uid;不妨设查询出来的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=newmoneyWHEREuid=new_money WHERE uid=newmoneyWHEREuid=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=newmoneyWHEREuid=new_money WHERE uid=newmoneyWHEREuid=uid; 升级为:

UPDATE t_yue SET money=newmoneyWHEREuid=new_money WHERE uid=newmoneyWHEREuid=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,但确实能解决问题。 但希望大家有收获**,思路比结论重要**。

作业,为什么不能使用: UPDATE t_yue SET money=money-$diff;

转载自:https://juejin.cn/post/6920098218634133517
评论
请登录