问题现象
前段时间,客户反映批量给用户添加积分的时候有些用户能够操作成功,有些用户不能操作成功。如果不是批量添加(是前段连续多次调用添加积分的接口)每个用户都能操作成功。错误代码如下:
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean addByUserCode(String userCode, String ruleCode, int point, String dynamicRuleCode) throws SQLException {
//1.查询积分记录表中的该用户的积分记录是否存在
//2.如果不存在,新建一条记录
// 如果存在,根据用户编号修改积分记录表中的该用户的积分数据。
return true;
}
报错。提示索引建唯一冲突,因为我们的用户积分记录表中,设置的userCode为唯一索引。
定位过程
- 第一反应是多线程的问题,当对一个用户同时多次调用添加积分操作时,第一个线程可能执行了①步,正在执行还没执行完成②步的时候,第二个线程就已经执行了①步,判断该用户记录不存在,所以当第二个线程再执行②步的时候会报错。出现这个问题,不用想我都知道需要用到分布式锁来控制程序的执行顺序。于是给老大反应了情况之后,引入分布式锁,下面是引入分布式锁后的伪代码
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean addByUserCode(String userCode, String ruleCode, int point, String dynamicRuleCode) throws SQLException {
//加锁
RLock lock = redission.getLock(userCode);
try{
//1.查询积分记录表中的该用户的积分记录是否存在
//2.如果不存在,新建一条记录
// 如果存在,根据用户编号修改积分记录表中的该用户的积分数据。
}catch(Exception e){
log.error(e)
}finally{
lock.unlock();
}
return true;
}
这样看起来,应该没有什么问题了吧。每次都只能有一个线程来修改或者添加用积分记录。但是我一测试,并没有解决问题。此时陷入了沉思… 让我一度怀疑是Redission的问题。
- 怀疑是Redisson分布式锁没有生效的问题。这时我使用蠢办法,一行代码一行代码的试着加锁,然后测试,发现把上面方法中的每一行都试了一个遍还是不行… 后面我发现addByUserCode 方法,是由另外一个service中的方法addByPhoneNumber调用addByPhoneNumber的代码
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean addByPhoneNumber(String phoneNumber){
User user = queryUserByPhoneNumber(phoneNumber);
addByUserCode(user.getUserCode());
...
}
然后我最后一步尝试将锁添加在User user = queryUserByPhoneNumber(phoneNumber);
之前,就行这样
`
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean addByPhoneNumber(String phoneNumber){
//加锁
RLock lock = redission.getLock(userCode);
User user = queryUserByPhoneNumber(phoneNumber);
addByUserCode(user.getUserCode());
lock.unlock();
}
最终问题得以解决。
解决方案
- 将加锁的范围,提到最外层。
- 删除addByUserCode上的事务注解。
- 修改Mysql的隔离级别个读已提交(read-commited)
原因及总结
事务提交之前锁已经被释放了,我们知道Spring的事务管理是通过AOP的方式代理的,也就是说在addByUserCode()中 unLock是在 commit()之前的,这里又因为Mysql的默认的隔离级别是可重复读。在事务A未提交之前,事务B读的数据是事物A相同。所以总结:
线程1抢占到锁以后,进行了一次数据库查询,此时事务并没有被提交,锁又被释放了。此时线程2抢占到锁,发现有个在线程1开启的事务A中有个相同的查询,且事务A还没有被提交,由于Mysql的MVVC机制,导致线程2的事务B中查询的数据和线程1的事务A中的数据相同。这就是并发控制不住的原因