解决Spring事务和锁冲突失效问题

在高并发场景下,Spring事务中的synchronized和Redis分布式锁可能无法有效防止数据重复读,导致线程安全问题。问题源于事务开启、加锁、解锁、提交的顺序与锁的作用域不匹配。解决方案是将锁操作与事务分离,如将锁移到无事务的方法中,或者创建无事务的中间层来调用带事务的方法,确保锁的范围覆盖整个事务过程。
摘要由CSDN通过智能技术生成

背景

在业务的时候,需要保证一个用户只能钱包表中插入一条数据。在service加入synchronize锁和插入前查找的情况下,但是有一天突然发现在短时间出现出现了多条数据。通过日志发现是短时间有多个相同请求造成,我们猜测是多线程高并发造成的。

思考

我们明明在插入之前了synchronized锁,但是结果来看可能是锁失效了,期间我们将sychronzied改redis分布式锁也失效。于是查找资料,发现spring事务下出现了synchronized锁失效的文章,由此我了解spring的事务流程。

spring事务的执行流程

代码执行流程:开启事务--》上锁--》执行业务--》解锁--》提交事务

  • 锁失效的原因:spring事务会在执行方法之前开启事务,然后上锁执行代码逻辑,解锁,提交事务。会出现如下情况:

在第一个线程解锁时候,还没提交事务。第二个线程已经开启事务,上锁,这时候读取的数据不是最新的,造成业务出错。

并且mysql默认重复读,所以出现上面的问题。不懂的可以了解一下mysql数据库的隔离级别

1 spring事务@Transactional和synchronized同时使用失效问题

  • 业务代码

 @Transactional
    public synchronized void insert(int index) throws InterruptedException {
        System.out.println(index);
        Test test = testMapper.selectById(1);
        System.out.println(test);
        test.setNum(test.getNum()+1);
        testMapper.updateById(test);
        System.out.println("线程:"+index+" 执行完成");
    }
  • 测试代码

@Test
public void test() throws InterruptedException {
    CountDownLatch countDownLatch=new CountDownLatch(1);
    for (int i=0;i<100;i++){
        int k=i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    countDownLatch.await();
                    walletService.insert(k);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
​
            }
        }).start();
    }
    countDownLatch.countDown();
    //主线程等待子线程执行完成
    Thread.currentThread().join();
}
  • 测试结果

测试前

预期结果:num=100

测试后:

  • 测试结果分析:从结果可以看出,明显出现了spring事务下,数据库重复读的情况。造成最终的业务代码失效。

  • 失效原因:Synchronized 失效关键原因:是因为Synchronized锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内。出现同步锁失效的原因是:当A(线程) 执行完insertSelective()方法,会进行释放同步锁,去做提交事务,但在A(线程)还没有提交完事务之前,B(线程)进行执行selectById() 方法,执行完毕之后和A(线程)一起提交事务, 这时候就会出现线程安全问题。

2 spring事务和分布式锁同时使用失效问题

  • 业务代码

 @Transactional
    public void insertByRedisLock(int index) throws InterruptedException {
        RLock lock = redissonClient.getLock("wxpay");
        lock.lock();
        try {
            System.out.println(index);
            Test test = testMapper.selectById(1);
            System.out.println(test);
            test.setNum(test.getNum()+1);
            testMapper.updateById(test);
            System.out.println("线程:"+index+" 执行完成");
        }finally {
            lock.unlock();
        }
    }
  • 测试代码

    测试代码同上,基本无区别

  • 测试结果

  • 数据库结果

小结

通过java的synchronized锁和Redis锁,在spring事务下都会出现数据库的数据重复读问题,mysql数据库默认重复读的。但是我们发现Redis锁的情况比synchronized的情况要好一些,但是依旧出现解决问题。

解决办法

通过实验代码我们发现,主要实现spring事务下包含了锁,释放锁之后和提交事务之前,已经从数据库读重复读数据,造成读取的数据不是最新数据。

  • 解决办法核心思路:既然事务下不能使用锁,那我们把锁和事务进行分开。使得在锁环境下包含事务,最终依然是线程安全的。

    • 办法1:将synchronized和Redis锁提取到controller层,不包含任何事务。

    • 办法2:在service下新建无事务的方法,将有事务代码的抽取单独使用。直接在无事务方法调用有事务的方法,这样依旧能保证线程安全。

    • 方法3:将分布式锁替换成数据库的锁比如select for update或者版本号version

synchronized的方法

@Transactional
public  void insert(int index) throws InterruptedException {
    System.out.println(index);
    Test test = testMapper.selectById(1);
    System.out.println(test);
    test.setNum(test.getNum()+1);
    testMapper.updateById(test);
    System.out.println("线程:"+index+" 执行完成");
}
​
@Override
public synchronized void noTxinsert(int index) throws InterruptedException {
    testService.insert(index);
}
​
  • 结果分析:从结果来看,结果是符合预期的。刚好为100

Redis分布式锁的方法

    @Override
    @Transactional
    public void insertByRedisLock(int index) throws InterruptedException {
        System.out.println(index);
        Test test = testMapper.selectById(1);
        System.out.println(test);
        test.setNum(test.getNum()+1);
        testMapper.updateById(test);
        System.out.println("线程:"+index+" 执行完成");
    }
​
    @Override
    public void noTxinsertByRedisLock(int index) throws InterruptedException {
        RLock lock = redissonClient.getLock("wxpay");
        lock.lock();
        try {
            testService.insertByRedisLock(index);
        }finally {
            lock.unlock();
        }
    }
  • 测试结果:

总结

我们在高并发的情况下,如果spring事务包含锁的情况,会造成读取的数据不是最新的情况。所以应该是锁包含整个事务,这样保证线程安全也可以实现锁的情况,不会发生数据重复读的情况。

其他:同一个类非事务方法直接调用事务方法,会出现事务失效。

参考链接:https://blog.csdn.net/zhangkaixuan456/article/details/109082645

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值