转载:《巧用CAS解决数据一致性问题》

在高并发的分布式环境下,对于数据的查询与修改容易引发一致性问题,本文将分享一种非常简单但有效的优化方法。

业务场景

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

  1. 从数据库查询用户现有余额 SELECT money FROM t_yue WHERE uid=$uid,不妨设查询出来的 $old_money=100元。

解决并发更新导致的一致性问题 - 图1

  1. 业务层实施业务逻辑,比如购买一个80元的商品,并且打九折 if($old_money> 800.9) $new_money=$old_money-800.9=28

解决并发更新导致的一致性问题 - 图2

  1. 将数据库中的余额进行修改 UPDATE t_yue SET money=$new_money WHERE uid=$uid

解决并发更新导致的一致性问题 - 图3

在并发量低的情况下,这个流程没有任何问题,原有金额100元,购买了80元的九折商品(72元),剩余28元。

潜在的问题

在分布式环境中,如果并发量很大,这种“查询+修改”的业务很容易出现数据不一致。极限情况下,可能出现这样的异常流程:

  1. 业务1和业务2同时查询余额,是100元

解决并发更新导致的一致性问题 - 图4

  1. 业务1和业务2进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元

解决并发更新导致的一致性问题 - 图5

  1. 业务1对数据库中的余额先进行修改,设置成28元,业务2对数据库中的余额后进行修改,设置成38元。

解决并发更新导致的一致性问题 - 图6

此时异常出现了,原有金额100元,业务1扣除了72元,业务2扣除了62元,最后剩余38元。

问题原因

高并发环境下,对同一个数据的并发读(两边都读出余额是100)与并发写(一个写回28,一个写回38)导致的数据一致性问题。

业务1的写回:原有金额100,这是一个初始状态,写回金额28,理论上只有在原有金额为100的时候才允许写回成功,这一步没问题。

业务2的写回:的原有金额100,这是一个初始状态,写回金额38,理论上只有在原有金额为100的时候才允许写回成功,可实际上,这个时候数据库中的金额已经变为28了,这一步的写操作不应该成功。

解决方案

在 set 写回的时候,加上初始状态的条件 compare,只有初始状态不变时,才允许 set 写回成功,这正是大家常说的“Compare And Set”(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。

对于上文中的业务场景,只需要将 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. -- 业务1执行
  2. UPDAtE t_yue SET money=28 WHERE uid=$uid AND money=100
  3. -- 业务2执行
  4. UPDAtE t_yue SET money=38 WHERE uid=$uid AND money=100

这两个操作同时进行时,只能有一个执行成功。

怎么判断哪个执行成功,哪个执行失败?

业务能通过 affect rows 得知哪个修改没有成功:

  • 执行成功的业务,affect rows 为1;
  • 执行失败的业务,affect rows 为0。

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/ilg6xq 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。