分析事故原因
我负责的是直播模块 其中的一个业务是直播结束后第三方会通知我去拉取直播的回放,
但是这个回放有可能一条,也有可能是多条,但是我们的业务要求是只需要保存一条直播回放.
插入数据之前,做了校验,如果存在就直接方法结束.不存在再继续玩下走,进行插入数据.
所以我这会做如下操作:
解决问题的过程
首先我模拟了一个并发环境:
- 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
//1. 创建对象赋值
CourseChapterLiveRecord courseChapterLiveRecord = new CourseChapterLiveRecord();
courseChapterLiveRecord.setCourseChapterId(9785454l);
courseChapterLiveRecord.setCreateTime(new Date());
courseChapterLiveRecord.setRecordEndTime(new Date());
courseChapterLiveRecord.setDuration("aaa");
courseChapterLiveRecord.setSiteDomain("ada");
courseChapterLiveRecord.setRecordId("aaaaaaaaa");
//2. 进行插入数据,调用那个插入的方法
courseChapterLiveRecordServiceImpl.saveCourseChapterLiveRecord(courseChapterLiveRecord);
System.out.println("---> end " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
latch.countDown();
}
} - 数据库结果
我去还真出现了 而且是一部分出现脏写,一部分没有成功,我特么 fuck 心理一万次,想说这特么我怎么找.
测了十来次,觉得肯定是有问题的,然后冷静下来,因为我打了日志.发现2个线程确实是顺序执行的(这里的截图就没有贴了).
众所周知,synchronized关键字能够保证所修饰的代码块、方法具有三大特性:有序性、原子性、可见性。
那么这说明什么呢? 我一想肯定Synchronized 它是起到它的作用的,一个线程执行完成之后,另外一个线程再来执行,
突然灵光一闪, 是不是下一个线程再做校验是否存在的时候, 读到了上一次还没有提交的事务, 所以造成了脏读,脏写的原因呢
然后我把再类上的@Transactional
注解去掉,果然后面测了几次 再也没出现上面的情况了
在这里,我再好好的说一下在Spring事务管理下,Synchronized为啥还线程不安全?
开始解决问题
方案1
就如上面所说的,不需要事务就行了,不加@Transactional
注解(但是前提你这个方案确定是不需要事务).
方案2
再这个里面再调用一层service 让那个方法提交事务,这样的话加上Synchronized 也能保证线程安全.
- 错误写法,这种依然不行,是因为saveRecord方法(
@Transactional
修饰的方法)的调用,必须是动态代理对象调用才可以,
这里也就是使用this对象调用的,所以是不可以的.
可以参考事务注解@Transactional不起作用原因及解决办法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
saveRecord(courseChapterLiveRecord);
}
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("把录播的信息插入数据库失败");
}
} - 改正之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
courseChapterLiveRecordServiceImpl.saveRecord(courseChapterLiveRecord);
}
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("把录播的信息插入数据库失败");
}
}
方案3
用redis 分布式锁,也是可以的 就算是多个副本也是能保证线程安全。
参考网上教程,这里就不赘述了.