记一次Mysql隔离级别和分布式锁引起的“惨案”

问题现象

前段时间,客户反映批量给用户添加积分的时候有些用户能够操作成功,有些用户不能操作成功。如果不是批量添加(是前段连续多次调用添加积分的接口)每个用户都能操作成功。错误代码如下:

    @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中的数据相同。这就是并发控制不住的原因

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值