在Spring事务管理下,Synchronized为啥还线程不安全?
技术文章第一时间送达!
juejin.im/post/5ddc7a23e51d452331202721
在synchronized 锁住方法的情况下,竟然出现了脏写
Tips
昨天本来打算是准备着一支烟 一杯咖啡 一个bug写一天的,突然我们组长跟我们说线上环境报错了,还出现了"服务器异常,请联系管理员"
这特么不是一级事故吗?虽然有测试在前面扛枪。但是是我负责的直播模块,心理慌的一批(ps 报错图当时没保存了)
分析事故原因
因为是报错(因为我做这条数据查询的时候是selectOne 所以会报出现了sql异常) 原因到是很快找到了 数据库出现了脏写如图:
我负责的是直播模块 其中的一个业务是直播结束后第三方会通知我去拉取直播的回放,但是这个回放有可能一条,也有可能是多条,但是我们的业务要求是只需要保存一条直播回放所以我这会做如下操作:
我在做插入之前我会做一个校验,并且我还加了一个方法级别的锁 并且线上我们只有一个副本,竟然还出现了脏写 我的fuck,我这是见了鬼了吧
解决问题的过程
我怀着百思不得其解的心理打算去找答案
首先我模拟了一个并发环境:
@Test
public void TEST_TX() throws Exception {
int N = 2;
CountDownLatch latch = new CountDownLatch(N);
for (int i = 0; i < N; i++) {
Thread.sleep(100L);
new Thread(() -> {
try {
latch.await();
System.out.println("---> start " + Thread.currentThread().getName());
Thread.sleep(1000L);
CourseChapterLiveRecord courseChapterLiveRecord = new CourseChapterLiveRecord();
courseChapterLiveRecord.setCourseChapterId(9785454l);
courseChapterLiveRecord.setCreateTime(new Date());
courseChapterLiveRecord.setRecordEndTime(new Date());
courseChapterLiveRecord.setDuration("aaa");
courseChapterLiveRecord.setSiteDomain("ada");
courseChapterLiveRecord.setRecordId("aaaaaaaaa");
courseChapterLiveRecordServiceImpl.saveCourseChapterLiveRecord(courseChapterLiveRecord);
System.out.println("---> end " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
}
通过CountDownLatch 去模拟并发看看数据是否会有问题:结果测试线的数据如下:
我去还真出现了 而且是一部分出现脏写,一部分没有成功,我特么 fuck 心理一万次想说这特么我怎么找
测了十来次 然后觉得肯定是有问题的 然后冷静下来 因为我打了日志 发现2个线程确实是顺序执行的(这里的截图就没有贴了)
众所周知,synchronized方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性。
那么这说明什么呢 我一想肯定Synchronized 它是起到它的作用的 一个线程执行完成之后,另外一个线程再来执行,突然灵光一闪 是不是下一个线程再做幂等校验的时候 读到了上一次还没有提交的事务 所以造成了脏读,脏写的原因呢
然后我把在类上的 @Transactional 注解去掉
果然后面测了几次 再也没出现上面的情况了
Tips 特别感谢一位不愿透露姓名的大佬的指出说我没有把标题的内容说清楚和后面的解决问题的收场的时候有点草率
在这里 我再好好的说一下我标题是 在Spring事务管理下,Synchronized为啥还线程不安全?
其实又是自己并没有用Synchronized 锁住 Spring 的事务
因为我的列子上的@Transaction注解是再类上面(也就是再方法上面)Spring的声明事事务他是利用了aop的思想
我虽然锁住了第一个线程 但是等到第一个线程的事务 还没提交的时候,第二个线程就去查询了 所以就会导致线程不安全问题
解决问题
方案1 很简单 那就是不开事务就行了,再这个方法上不加事务就行 因为 Synchronized 可以保证线程安全。
这个方案的意思就是说不要再同一个方法上用@Transaction 和 Synchronized 例子图就没有贴了 就像我前面的 把注解去掉就好了 (但是前提你这个方案确定是不需要事务)
方案2 在这个里面再调用一层service 让那个方法提交事务,这样的话加上Synchronized 也能保证线程安全。
方案2我贴下代码吧
@Override
public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
courseChapterLiveRecordServiceImpl.saveRecord(courseChapterLiveRecord);
}
@Transactional
public void saveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
//先查数据看是否已经存了
if (findOrder(courseChapterLiveRecord)){ return;}
int row = this.insertSelective(courseChapterLiveRecord);
if (row<1){
log.info("把录播的信息插入数据库失败 参数是->{}", JSON.toJSONString(courseChapterLiveRecord));
throw new RRException("把录播的信息插入数据库失败");
}
}
其实也就是说把事务包裹在Synchronized 里面
利用中午的时间测了几次 确实是不会出现线程安全问题了
方案3 用redis 分布式锁 也是可以的 就算是多个副本也是能保证线程安全。这个后面的文章会有写到
结论
在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。
参考了一位读者的结论
Synchronized 失效关键原因:是因为Synchronized锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内。
出现同步锁失效的原因是:当A(线程) 执行完insertSelective()方法,会进行释放同步锁,去做提交事务,但在A(线程)还没有提交完事务之前,B(线程)进行执行findOrder() 方法,执行完毕之后和A(线程)一起提交事务, 这时候就会出现线程安全问题。
END
Java面试题专栏