背景
在业务的时候,需要保证一个用户只能钱包表中插入一条数据。在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