• 诡异,明明更新成功了状态,查不出来了


    作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。

    热门文章推荐

    明明更新成功了状态  查不出来了.png

    一、前言

    程序员小明遇到一个非常诡异的问题,明明在前面已经将数据状态更新成功了,可是有些数据(并非所有)后续按照更新后的状态查询数据没查到,导致防御代码判断为空直接返回,没有执行后续的同步操作。

    查了很久,百撕不得其姐。
    image.png
    于是程序员小明求助师兄,师兄说:“说来话长,你直接看明明如月学长的文章吧…”

    二、场景复现

    下面是一个复现问题的代码:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    @Service
    public class MyService {
    
        @Autowired
        private MyRepository myRepository; 
    
        private final ExecutorService threadPool = Executors.newFixedThreadPool(5);
    
    
        public void updateAndSyncData(List<Long> ids, String newState) {
            log.info("更新状态和同步数据, ids:{}, newState:{}" ,ids, newState)
            // Step 1: 修改数据状态
            List<MyEntity> entities = myRepository.findByIds(ids);
            entities.foreach(entity->{
                entity.setState(newState);
                myRepository.save(entity);   
            });
    
            // Step 2: 在新的线程中查询数据状态并调用新接口来同步数据
            threadPool.submit(() -> {
                 ids.foreach(id->{
                     try{
                          // 根据新的状态查询数据
                           MyEntity entity  = myRepository.findByIdAndState(id,newState);
                           if(entity == null){
                                log.info("未查询到数据, id:{}, newState:{}" ,id, newState)
                                return;
                            }
                
                            //调用下游接口同步
                            log.info("执行下游同步, id:{}, newState:{}" ,id, newState)
                            callNewInterfaceToSyncData(entity, newState);
    
                      }catch(Exception e){
                         log.error("执行下游同步失败, id:{}, newState:{}" ,id, newState)
                     }
                 });
               
            });
        }
    
        private void callNewInterfaceToSyncData(MyEntity entity,String state) {
            // 在这里调用新接口来同步数据到其他系统
           
        }
    }
    
    
    • 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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    注:这里只是为了演示问题,请不要较真细节问题,比如性能问题。

    给你 2 分钟的时间思考一下可能得原因有哪些?

    [2 分钟]

    经验丰富的程序员会有种预感,可能和多线程有关系。

    但源码非常让人困惑,虽然是新启动线程池执行任务,根据新状态查询数据,但是线程池任务提交前状态状态已经更新完毕了啊?!

    除非…

    三、问题分析

    查问题,我们需要:大胆猜想,小心求证。

    image.png

    3.1 猜想1:代码逻辑有误?

    有可能代码逻辑有问题,比如更新状态的语句有问题,根据 ID 和状态的查询 SQL 有问题等。
    经过重新代码审查,发现逻辑, 底层 SQL 语句也没问题没问题。

    3.1 猜想2:有报错,导致状态修改失败或者查询成功同步失败?

    通过日志发现没有任何报错,经过核实可能出错的地方都会有异常日志,所以排除。

    3.2 猜想3:查询前被其他线程修改了?

    有一种可能是在异步查询之前,状态被其他线程改掉了。
    通过日志和数据库中的数据更新时间都证明,并没有被其他线程修改过。

    3.3 猜想4:外层有事务?

    从上述代码看确实没有看到有开启事务。
    继续往上翻,翻了四五层发现的确开启了事务!!
    因此,真相大白。
    外部开启了事务修改了状态,在线程池中根据新的状态查询部分数据时由于事务还没提交,用新的状态查不到,从而导致后续的同步任务没有更新。

    可能有些人会说,这不难吧??
    的确,当你看到这里似乎觉得很简单,但当你写代码层数过深时,很容易忘记外部开启了事务。
    另外,很多时候有些犯类似错误的同学你问他他都会,写的时候可能没有注意到。

    四、相关知识点

    4.1 事务四大特性

    • 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚,不会出现部分执行的情况。
    • 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说事务执行前后,数据库中的数据满足预定义的规则和约束。
    • 隔离性(Isolation):事务之间是相互隔离的,一个事务的执行不会受到另一个事务的影响。不同的隔离级别可以防止脏读、不可重复读和幻读等并发问题。
    • 持久性(Durability):事务一旦提交,它对数据库中数据的改变就是永久性的,即使发生系统故障或者数据库崩溃,也不会丢失已提交的数据。

    image.png
    在一个事务中修改了数据状态,但是该事务在你创建新线程去查询这些更改时还没有提交。因此,新线程中的查询不能看到这些未提交的更改,这是因为它处于一个不同的事务或非事务状态中。

    4.2 事务和线程的关系

    • 事务是指数据库中一组逻辑上相关的操作,它们要么都执行,要么都不执行。事务的四大特性是原子性、一致性、隔离性和持久性。
    • 线程是指程序中一条执行路径,它可以并发地执行多个任务。线程之间可以共享内存和资源,但也需要同步和协调。
    • 事务和线程的关系主要取决于数据库连接和事务管理的方式。数据库连接是指程序和数据库之间的通信通道,事务管理是指控制事务的开始、提交和回滚的过程。
    • 一种常见的方式是基于线程绑定的数据库连接和声明式的事务管理。这种方式下,每个线程在执行事务时会获取一个独立的数据库连接,并通过注解或配置来声明事务的边界和属性。这样可以保证每个线程有自己的事务上下文,不会相互干扰。
    • 另一种方式是基于共享的数据库连接和编程式的事务管理。这种方式下,多个线程可以使用同一个数据库连接,并通过代码来手动控制事务的开始、提交和回滚。这样可以节省数据库连接资源,但也需要注意线程安全和事务隔离问题。

    五、解决办法

    解决办法有很多,常见如下:

    • 在异步执行前先在事务查询出来(事务如果在后续回滚,异步的逻辑可能也会被正常执行)。
    • 在执行异步逻辑之前提交事务。
    • 可以使用 TransactionSynchronizationManager来注册一个回调,该回调将在当前事务成功提交后执行。这允许你在事务提交后执行特定的逻辑(更合理)。
    • 去掉异步逻辑,都改成同步逻辑。

    由于具体实现并不困难,这里就不用代码演示了。具体采用什么策略需要根据实际的情况来决定。

    六、启示

    在这里插入图片描述

    6.1 注重代码审查

    这个问题如果代码审查仔细的话还是能够看出来的。
    比如被审查者,从 Facade 一层一层往 Dao 层讲解代码逻辑,审查的同学看到事务和异步,有很大可能看出这个问题。

    当然,这不能仅依靠代码审查,大家使用线程池时应该主动思考可能造成的问题。

    6.2 大胆猜想,小心求证

    我认为差问题应该:“大胆猜想,小心求证”。

    不要乱猜,乱猜容易浪费大量的时间。

    需要根据问题的表现,根据自己的专业能力反向推测可能的原因,并且根据代码、日志、数据库数据等论证自己的猜测。

    当然,很多“诡异的问题” 由于“不识庐山真面目,只缘身在此山中”,有时候找周围的同学帮看一眼更容易更早定位原因。

    6.3 知行合一、学以致用(“八股文”的误解)

    在面试的时候,问求职者:“事务的四大特征”,绝大多数人都可以“倒背如流”。很多人甚至认为这是“八股文”,毫无意义。

    然而,实际编码过程中,很容易忘记这些知识,导致知识和运用脱离。

    学习的目的是:学以致用,正如孤尽老师所说:“记忆、理解、表达、融合”。

    其实记忆并不意味着掌握,能够做到知行合一,能够表达融会贯通才代表真正掌握了知识。

    七、总结

    本文讲解事务未提交时异步查询不到数据导致代码效果不符合预期的情况,并给出了解决办法。

    大家在事务中使用异步线程执行任务时要特别注意你这个问题。

    大家要加强代码审查,有很大概率可以避免一些问题。同时,大家查问题时,一定要以“证据为依据”,“大胆猜想,小心求证”。


    创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。

    欢迎加入我的知识星球,知识星球ID:15165241 (已经营 5 年,会持续经营)一起交流学习。
    https://t.zsxq.com/Z3bAiea 申请时标注来自CSDN。

  • 相关阅读:
    MongoDB 和 MySQL 的区别
    2023-08-31 LeetCode每日一题(一个图中连通三元组的最小度数)
    Docker从入门到精通|2022版
    SpringCloud - Spring Cloud 之 Gateway网关(十三)
    IE浏览器,文件下载失败,onDownloadProgress方法里报错:无法获取未定义或null引用的属性“getResponseheader“
    查看电脑jdk/jre版本以及安装路径并测试是否可以正常使用(检查运行环境)
    在伦敦银投资中,技术是万能的?
    硅谷15菜单权限
    【刷题-牛客】链表内指定区间反转
    Ubuntu上的论文翻译软件 --- 兰译
  • 原文地址:https://blog.csdn.net/w605283073/article/details/132992038