作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《EffectiveJava》独家解析》专栏作者。
热门文章推荐:
- (1)《为什么很多人工作 3 年 却只有 1 年经验?》
- (2)《从失望到精通:AI 大模型的掌握与运用技巧》
- (3)《AI 时代,程序员的出路在何方?》
- (4)《如何写出高质量的文章:从战略到战术》
- (5)《我的技术学习方法论》
- (6)《我的性能方法论》
- (7)《AI 时代的学习方式: 和文档对话》
- (8)《人工智能终端来了,你还在用过时的 iterm?》
程序员小明遇到一个非常诡异的问题,明明在前面已经将数据状态更新成功了,可是有些数据(并非所有)后续按照更新后的状态查询数据没查到,导致防御代码判断为空直接返回,没有执行后续的同步操作。
查了很久,百撕不得其姐。
于是程序员小明求助师兄,师兄说:“说来话长,你直接看明明如月学长的文章吧…”
下面是一个复现问题的代码:
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) {
// 在这里调用新接口来同步数据到其他系统
}
}
注:这里只是为了演示问题,请不要较真细节问题,比如性能问题。
给你 2 分钟的时间思考一下可能得原因有哪些?
[2 分钟]
经验丰富的程序员会有种预感,可能和多线程有关系。
但源码非常让人困惑,虽然是新启动线程池执行任务,根据新状态查询数据,但是线程池任务提交前状态状态已经更新完毕了啊?!
除非…
查问题,我们需要:大胆猜想,小心求证。
有可能代码逻辑有问题,比如更新状态的语句有问题,根据 ID 和状态的查询 SQL 有问题等。
经过重新代码审查,发现逻辑, 底层 SQL 语句也没问题没问题。
通过日志发现没有任何报错,经过核实可能出错的地方都会有异常日志,所以排除。
有一种可能是在异步查询之前,状态被其他线程改掉了。
通过日志和数据库中的数据更新时间都证明,并没有被其他线程修改过。
从上述代码看确实没有看到有开启事务。
继续往上翻,翻了四五层发现的确开启了事务!!
因此,真相大白。
外部开启了事务修改了状态,在线程池中根据新的状态查询部分数据时由于事务还没提交,用新的状态查不到,从而导致后续的同步任务没有更新。
可能有些人会说,这不难吧??
的确,当你看到这里似乎觉得很简单,但当你写代码层数过深时,很容易忘记外部开启了事务。
另外,很多时候有些犯类似错误的同学你问他他都会,写的时候可能没有注意到。
在一个事务中修改了数据状态,但是该事务在你创建新线程去查询这些更改时还没有提交。因此,新线程中的查询不能看到这些未提交的更改,这是因为它处于一个不同的事务或非事务状态中。
解决办法有很多,常见如下:
TransactionSynchronizationManager
来注册一个回调,该回调将在当前事务成功提交后执行。这允许你在事务提交后执行特定的逻辑(更合理)。由于具体实现并不困难,这里就不用代码演示了。具体采用什么策略需要根据实际的情况来决定。
这个问题如果代码审查仔细的话还是能够看出来的。
比如被审查者,从 Facade 一层一层往 Dao 层讲解代码逻辑,审查的同学看到事务和异步,有很大可能看出这个问题。
当然,这不能仅依靠代码审查,大家使用线程池时应该主动思考可能造成的问题。
我认为差问题应该:“大胆猜想,小心求证”。
不要乱猜,乱猜容易浪费大量的时间。
需要根据问题的表现,根据自己的专业能力反向推测可能的原因,并且根据代码、日志、数据库数据等论证自己的猜测。
当然,很多“诡异的问题” 由于“不识庐山真面目,只缘身在此山中”,有时候找周围的同学帮看一眼更容易更早定位原因。
在面试的时候,问求职者:“事务的四大特征”,绝大多数人都可以“倒背如流”。很多人甚至认为这是“八股文”,毫无意义。
然而,实际编码过程中,很容易忘记这些知识,导致知识和运用脱离。
学习的目的是:学以致用,正如孤尽老师所说:“记忆、理解、表达、融合”。
其实记忆并不意味着掌握,能够做到知行合一,能够表达融会贯通才代表真正掌握了知识。
本文讲解事务未提交时异步查询不到数据导致代码效果不符合预期的情况,并给出了解决办法。
大家在事务中使用异步线程执行任务时要特别注意你这个问题。
大家要加强代码审查,有很大概率可以避免一些问题。同时,大家查问题时,一定要以“证据为依据”,“大胆猜想,小心求证”。
欢迎加入我的知识星球,知识星球ID:15165241 (已经营 5 年,会持续经营)一起交流学习。
https://t.zsxq.com/Z3bAiea 申请时标注来自CSDN。