背景:银行多个异步通知先后到达,需要依次更新同一条数据A(wherte acountId=aaa)(acountId是唯一索引)的不同状态,每一次更新需要在上一次更新的基础上进行。
及数据A(wherte acountId=aaa)原本状态status=0、openstatus=0
——》在收到通知1后,在方法methodA中,将状态更新为status=1、openstaus=0,注更新前需要查询到数据A(wherte acountId=aaa)
——》在收到通知2后,在方法methodB中,将状态更新为status=1、openstaus=1或者status=1、openstaus=2,注更新前需要查询到数据A(wherte acountId=aaa)
注:1、数据A的大概字段:sysNo(主键)、accountId(唯一键)、status、openStatus、createTime、......
2、在进行某一项交易后,通知1和通知2会先后到来,但是先后的时间差不能保证,那么存在两种情况:情况A,通知2来时执行方法methodB时可能通知1的处理方法methodA已经执行完毕,情况B,也可能methodA方法还在执行中。
为了防止情况B在methodB开始执行的时候,methodA还未执行完毕导致的数据更新不是在最新数据上进行的更新(专业术语:更新丢失或者更新覆盖),及methodB是直接在原始数据上进行的更新,而不是在methodA更新后的数据上进行更新的。为了防止更新丢失,可以有多种解决办法:悲观锁(for update)方式、乐观锁(版本号、时间戳)方式、通过分布式锁(redis)等。
问题:由于公司项目并发不高,所以直接使用悲观锁来简单处理的,但是在处理过程中发现一个奇怪的现象(注该现象是在开发和测试环境中出现),如下图:
描述:在同一个事务中,第一步通过for update获取到的排他锁,但是在进行更新update的时候发现该笔数据的排它锁却无法获取,是不是感觉很奇怪?
于是下面就开始找问题,为什么update获取不到上面的同一笔数据的锁?
一、定位数据更新update语句,是不是更新到了索引?检查了update语句,在update语句中并没有更新到索引字段。
二、在数据库中直接执行是否报错?数据库中直接执行没有问题,说明代码中前后执行不是在同一个事务中(定位问题)。
三、数据库事务是不是统一管理的?数据库索引会不会存在一部分spring管理,一部分mybatis管理?确认了数据库事务统一由Spring管理,所以不是该原因造成的。
四、是否使用主从数据库导致数据来源不一致?
描述:公司生产上使用主从数据库,本地开发和测试环境都是单一数据库,没有主从,但是通过ShardingJDBC配置了主从设置
问题原因就在这,虽然测试环境实际上只有一个数据库,但是ShardingJDBC配置了主从,那么即使只有一个数据库,也会产生两条连接,一条用于主、一条用于从,所以在select for update语句的时候默认使用从链接的事务中获取数据,并且将数据加锁,而表面上看代码是同一事务的update时,实际上是使用主链接的事务进行更新数据,所以select 和 update不在同一个事务中,故代码层面看是同一事务的先后两个操作,update时实际获取不到之前的select的锁,所以发生等待直至超时报错。
解决办法:强制统一使用主链接读取和修改数据,代码如下:
补充:
1、悲观锁导致的死锁
在使用悲观锁的过程中,注意加锁顺序,如果methodA方法是更新数据1和数据2,而methodB是更新数据2和数据1,如果两个线程同时进入到methodA和methodB,分别对数据1和数据2上锁,然后又分别等待数据2和数据1释放锁后自己获取,那么就会造成死锁。
避免死锁的方法:
A:统一加锁顺序,例如按照id自然序来进行加锁操作,这样事务之间的加锁操作就不会存在死锁。
B:不使用悲观锁(乐观锁、分布式锁、Redis锁)或者在使用悲观锁钱在加一个全局锁,如在redis存储一个lock标记,当事务获取到这个lock标记时,才允许进行更新操作,否则等待锁。
2、ShardingJDBC
当当网开发的主要用于处理数据分片和读写分离的框架。