在Spring事务@Transactional管理下,Synchronized线程不安全

参考
https://blog.csdn.net/prin_at/article/details/90671332

分析事故原因

我负责的是直播模块 其中的一个业务是直播结束后第三方会通知我去拉取直播的回放,
但是这个回放有可能一条,也有可能是多条,但是我们的业务要求是只需要保存一条直播回放.

插入数据之前,做了校验,如果存在就直接方法结束.不存在再继续玩下走,进行插入数据.
所以我这会做如下操作:
在这里插入图片描述
在这里插入图片描述

解决问题的过程

首先我模拟了一个并发环境:

  • 示例代码
    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
    @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);
    //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
     @Override
    public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
    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("把录播的信息插入数据库失败");
    }
    }
  • 改正之后
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     @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("把录播的信息插入数据库失败");
    }
    }

方案3

用redis 分布式锁,也是可以的 就算是多个副本也是能保证线程安全。
参考网上教程,这里就不赘述了.