最近,在代码开发时,因为加入的对某条记录可以复制的操作,产生了数据不一致的bug。经过排查,发现是在service层接口中,主线程下开了子线程,并且主线程数据的修改没有提交的情况下,子线程的行为又进行了查询操作,读取了未更新前的值,产生了脏数据。
具体业务场景如下图所示

上图总的来说就是主线程修改了数据库中某条数据的requestConfig字段,子线程中执行的调度任务执行了执行器代码也执行了查询这条数据,因为A接口内主线程事务尚未提交,导致执行器中读取的配置还是旧的配置,逻辑下读取的数据是旧数据,导致数据脏读。
解决方法如下图所示

加入中间件kafka,将需要调度的信息存入kafka中,由消费线程去pull消息,然后执行调度任务。因为A接口内主线程事务已提交(用中间件解耦),执行器中读取的配置就是最新的数据,所以数据脏读问题解决。
按照上面的解决办法,此时又引入了新的问题,如果不同的用户同时操作这个记录并且修改的情况下,又会发生脏读的问题。
具体业务场景如下图所示

前提:不同用户同时操作同一记录的编辑按钮执行A service,A1 修改数据库后,将调度任务的数据放入kafka,A2同时修改(修改草稿完成后requestConfig将不允许再被修改)也产生了一条调度任务信息,此时消费线程读取的requestConfig将会是A1、A2修改过后的配置,而且顺序还不能确定(网络抖动原因),后续调度任务执行的也是两条不同配置的requestConfig,而实际想要的就是一条requestConfig 产生的结果,与预期不符合。这还是单点部署情况下产生的问题,如果集群部署,用户数更多的情况下,这样的requestConfig会有更多,调度任务执行的数据会更混乱,显然不是开发想看到的。
解决方法如下图所示

在包含requestConfig的记录中增加version字段,修改后数据库version+1,并将+1后的version和调度信息绑定在一起传入kafka,同理A2 产生的调度信息也是如此,此时消费线程读取,不论是A1还是A2产生的调度信息,消费线程在调度前先查询数据库中对应记录的version,如果从kafka中获取的version大于或者等于数据库中的version,大于说明配置被后来者修改过(修改数据库不成功,传入kafka成功),直接抛弃。相等说明配置还只是自己修改过或者说是自己是最新修改的调度信息,调度任务执行;如果小于(A1执行产生的调度信息version落后于A2执行产生的调度信息version,造成了小于的情况),则直接抛弃,不执行调度任务,防止调度任务执行产生的脏数据。
乐观锁的具体定义,捎带举例解释:
1、假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
2 、在操作员 A 操作的过程中,操作员 B 也读入此用户信息( version=1 ),并 从其帐
户余额中扣除 $20 ( $100-$20 )。
3 操作员 A 完成了修改工作,将数据版本号加1( version=2 ),连同帐户扣 除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大 于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
4 操作员 B 完成了操作,也将版本号加1( version=2 )试图向数据库提交数 据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的 数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记 录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于version=1 的旧数据修改的结果覆盖操作 员 A 的操作结果的可能。 从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系 统整体性能表现。
乐观锁:提交版本必须大于记 录当前版本才能执行更新
而上面的处理方式是:提交的版本必须等于当前版本才能执行。相当于乐观锁的改编版本了。




