使用数据库乐观锁的方式解决数值累加的问题
背景
做应用的时候,涉及到数值累加,比如积分的累加,余额的累加,这些在我们开发过程中,一定要保证数值的一致性,比如积分总计值,一定要跟积分记录里的积分总和对应上。这样才能算是数值的正确,那么什么时候会遇到数值不正确的情况呢。
1:记录表添加了,但是积分表因为某些不可控的情况下没有添加,此类问题比较好解决,用数据库事务就能解决。
2:微服务场景下,要保证积分的累积值跟其他服务产生的积分记录的累加要一致,这个就涉及到数据库事务。可以用 rocketmq 这种可以解决分布式事务的 mq 来处理,或者其他分布式事务的方式来解决
3:如果积分累加的这个接口,并发调用,产生的积分数值覆盖。
今天我们就讨论第三种情况应该怎么解决。
方案分析
创建一条数据
首先我们看以下,并发情况下,数据库会出现怎么个现象。
先创建一个数据库,表是,user_score
添加一条用户积分数据
模拟测试
1、先设置隔离级别 set session transaction isolation level REPEATABLE READ; 可重复读
场景:
假设有两个并发过来,同事要给 score 加 1 个积分,如下面的顺序进行操作
事务执行顺序 1:
最终数据库里的 score 是 1,因为查询都是先执行的,所以 scoreA 跟 scoreB 都是 1,因为原始查出来的时候,A 事务跟 B 事务 select 语句获取的值都是 1
事务执行顺序 2:
这种执行顺序,最终 score 的结果还是 1
这个执行顺序,有争议的地方主要在第 6 步,事务 2 获取的 score 的值,这里答案是 0,因为设置的事务隔离级别是可重复读。所以事务 A 在提交之前更新的值对事务 B 是不可见的,所以第 6 步查询的值还是 0。
2、设置隔离级别 set session transaction isolation level READ COMMITTED; 读已提交
设置这个隔离级别后,第一个事务执行顺序还是会有问题,最终结果还是 0
但是,事务执行顺序 2,是我们想要的结果了。因为隔离级别降低,导致第 6 步,可以读取到已更新的数据了。
其他读未提交,以及串行读就不做考虑了。
可以看到,上面的场景,针对数据库并发上,会导致数据一致性的问题,因为并发会导致,最终积分数据不一致的问题。此问题应该怎么解决呢。就是我们今天讨论的内容
解决方案
此问题解决方案常规有两种
1、应用程序方面加锁,保证数据库层面不是并发处理即可。此方案相对比较重。业务系统方面层面处理就好,比如 java 的一些并发锁机制就可以解决。
2、使用乐观锁的方式来处理。
我们主要讨论这种方案
乐观锁主要是一种 CAS 机制,即数据变更的时候要比较下版本。具体实施如下
在数据库表设计上,我们添加一个版本字段。
1、set session transaction isolation level READ COMMITTED;
执行事务 1:
此隔离级别下,最终结果是 2
执行事务 2:
此方案,第 10 行执行下来,影响行数是 0,因为此隔离级别下,当前 version 已经是 1 了, 所以 update 未执行,这样可以通过影响行数为 0 判断是否执行成功,让业务重新执行此事务。以达到 score 再加 1 的效果。
2、set session transaction isolation level REPEATABLE READ;
此隔离级别下也是一样的,最后一个 update 的影响行数一直是 0,所以处理逻辑还是跟上面的步骤一样。
所以以上就形成了一个解决并发的处理方案。
正常积分的添加,修改。还需要有个积分记录表,形成一个事务。然后那个 version 字段可以就绑定最后一个记录表的 id 来解决。
类似以下解决方案
评论