写点什么

使用数据库乐观锁的方式解决数值累加的问题

用户头像
陈靓-哲露
关注
发布于: 23 小时前

背景

做应用的时候,涉及到数值累加,比如积分的累加,余额的累加,这些在我们开发过程中,一定要保证数值的一致性,比如积分总计值,一定要跟积分记录里的积分总和对应上。这样才能算是数值的正确,那么什么时候会遇到数值不正确的情况呢。

1:记录表添加了,但是积分表因为某些不可控的情况下没有添加,此类问题比较好解决,用数据库事务就能解决。

2:微服务场景下,要保证积分的累积值跟其他服务产生的积分记录的累加要一致,这个就涉及到数据库事务。可以用 rocketmq 这种可以解决分布式事务的 mq 来处理,或者其他分布式事务的方式来解决

3:如果积分累加的这个接口,并发调用,产生的积分数值覆盖。


今天我们就讨论第三种情况应该怎么解决。


方案分析

创建一条数据


首先我们看以下,并发情况下,数据库会出现怎么个现象。

先创建一个数据库,表是,user_score

CREATE TABLE `user_score` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `user_id` int(11) DEFAULT NULL,  `score` int(10) DEFAULT NULL,  `update_time` datetime DEFAULT NULL,  `create_time` datetime DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
复制代码


添加一条用户积分数据

INSERT INTO `study`.`user_score`(`id`, `user_id`, `score`) VALUES (1, 1, 0);
复制代码


模拟测试

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 来解决。


类似以下解决方案

BEGINSELECT score,last_id FROM user_score WHERE user_id = 1;insert into score_log (id,score) VALUES (1,1);UPDATE user_score SET score = 1,last_id = score_id WHERE user_id = 1 and last_id = last_id;COMMIT
复制代码


用户头像

陈靓-哲露

关注

还未添加个人签名 2018.04.12 加入

还未添加个人简介

评论

发布
暂无评论
使用数据库乐观锁的方式解决数值累加的问题