由并发扣款如何保证数据一致性引起的ABA问题的思考

起因是看了公众号架构师之路 推送的一篇文章:并发扣款,如何保证数据的一致性?

1. 场景:

用户购买商品,对余额进行查询和修改,如果余额大于商品价格,购买商品并修改余额,单机或者无并发情况下,这个步骤没有任何问题;但分布式环境下,不同站点实例的多个业务操作同一个用户进行并发扣款(虽然我也不知道怎么会有这种场景,用户怎么可能在同一时间买多个商品),这样进程内互斥锁肯定无法起作用,最终导致数据不一致。以下是异常流程:

  1. 业务1和业务2并发查询余额,都是100元;
  2. 然后并发进行业务计算,业务1算出余额是28元,业务2算出余额是38元,
  3. 那么无论谁写回余额,都会覆盖另一个业务的操作结果。

这不就是第二类丢失更新吗?网上有一个关系表格图:
由并发扣款如何保证数据一致性引起的ABA问题的思考_第1张图片
如图,mysql默认隔离级别为可重复读,能解决第二类丢失更新,那上面怎么这个是怎么回事?
这里有一个误区,所说能解决是基于update语句的加减不会丢失,比如 update t_account set money=money-1 where id=1, 如下:事务1在T3时刻查询余额为100,事务2在T5时刻执行update减1操作修改为99,T6时刻提交,由于可重复读,事务1在T7时刻再查询一次仍然为100,T8时刻执行update减1,执行完会发现变成98,即更新未丢失;

时刻 事务1 事务2
T1 开始事务
T2 开始事务
T3 查询余额为100
T4 查询余额为100
T5 执行update减1,余额修改为99
T6 提交事务
T7 查询余额为100
T8 执行update减1
T9 查询余额为98
T10 提交事务

而上面描述的业务操作是在代码层级实现,即先查询后修改,加减是在代码里操作的,所以产生丢失更新;

那么问题来了,为什么不直接执行update加减操作呢?因为并发情况下,缺少了查询判断,很有可能会把余额减成负数。

2. 解决方案

2.1 分布式锁

由于是分布式环境,采用分布式锁可以保证数据一致性,但是这是小概率事件,并且引入新组件(redis/zk),还会降低吞吐量。
分布式锁参考:https://blog.csdn.net/unclecoco/article/details/99442998

2.2 悲观锁

在查询语句加 for update,行记录加上排它锁,这样后来的事务会阻塞查询,这样就避免了数据不一致。

2.3 乐观锁

CAS(Compare And Swap):内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

通过CAS操作,即旧值和预期值相同时执行修改,例如:
update t_account set money=#{new_money} where id=#{id};
修改为:
update t_account set money=#{new_money} where id=#{id} and money=#{old_money};

这样在并发情况下,只能有一个修改成功,affect row为1;其他事务由于money不等于旧值,修改失败,affect row为0。

3. 思考

3.1 ABA问题

线程1准备用CAS修改变量值A为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以线程1的CAS操作成功。但实际上这时已经和最初不同了。有点场景可能没影响,但在下例堆栈操作,就会产生严重问题:

线程1:读取到栈顶为A,准备替换为B,结果为B.next=Y;
在这里插入图片描述
线程2:取出A,push X、A入栈;
在这里插入图片描述
线程1:栈顶仍然为A,执行CAS操作,实际结果B.next=X,并且此时堆栈结构已经发生变化;
在这里插入图片描述

Java中以AtomicInteger为例可能产生ABA问题,可采用版本戳version来对记录或对象标记解决这个问题,AtomicStampedReference就是实现了这个作用。

那么在题中的场景,事务2将余额修改后,如果又进行了一系列其他操作,最后余额等于旧值,事务1进行CAS操作成功,这就是ABA问题。

3.2 解决方案

基于值的CAS乐观锁,可能产生ABA问题。
这个问题可以通过加版本号的方案解决,本质还是CAS乐观锁,数据库增加一个版本号字段version,引入版本号,每次查询把版本号也查询出来,修改后比对版本号,如果期间其他事务修改过这条行记录,那么由于版本号不匹配,一定会修改失败。如下:
update t_account set money=#{new_money} where id=#{id} and money=#{old_money};
修改为:
update t_account set money=#{new_money} , version=#{new_version} where id=#{id} and version=#{old_version} ;

你可能感兴趣的:(数据库)