在Spring事务管理下,Synchronized为啥还线程不安全?

简介: 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

前言


文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…

种一棵树最好的时间是十年前,其次是现在


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) {
        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 里面

先自我批评一下

在技术的道路上真的不要自己觉得是什么就是什么 上面的代码是错误的 其实我并没有测试过 就贴到文章上了 这是一个大忌 为什么很多技术文章有问题 因为很多就像我上面的一样 所以敦促自己以后做事情还是要扎扎实实

感谢 紫雨飞星 读者提出我的错误 具体错误的原因是因为调用savRecord方法的时候使用的是this对象,其实是没有被AOP处理的,也就是这个Transactional不会生效~~~

修改后的代码  自己注入自己


@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 分布式锁 也是可以的 就算是多个副本也是能保证线程安全。这个后面的文章会有写到


结论


在多线程环境下,就可能会出现:方法执行完了(synchronized代码块执行完了),事务还没提交,别的线程可以进入被synchronized修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。


相关文章
|
4天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
19 4
|
2月前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
210 10
spring多线程实现+合理设置最大线程数和核心线程数
|
28天前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
23 2
|
1月前
|
安全 Java 对象存储
安全性考量:Spring Security与Netflix OSS在微服务安全中的作用
安全性考量:Spring Security与Netflix OSS在微服务安全中的作用
36 1
|
1月前
|
Java 编译器 程序员
【多线程】synchronized原理
【多线程】synchronized原理
51 0
|
2月前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
3月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
28 2
|
3月前
|
Java 测试技术
Java多线程同步实战:从synchronized到Lock的进化之路!
Java多线程同步实战:从synchronized到Lock的进化之路!
99 1
|
3月前
|
Java Spring 开发者
掌握Spring事务管理,打造无缝数据交互——实用技巧大公开!
【8月更文挑战第31天】在企业应用开发中,确保数据一致性和完整性至关重要。Spring框架提供了强大的事务管理机制,包括`@Transactional`注解和编程式事务管理,简化了事务处理。本文深入探讨Spring事务管理的基础知识与高级技巧,涵盖隔离级别、传播行为、超时时间等设置,并介绍如何使用`TransactionTemplate`和`PlatformTransactionManager`进行编程式事务管理。通过合理设计事务范围和选择合适的隔离级别,可以显著提高应用的稳定性和性能。掌握这些技巧,有助于开发者更好地应对复杂业务需求,提升应用质量和可靠性。
43 0
|
3月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
52 0