vlambda博客
学习文章列表

异步读取Spring事务还未提交的数据,你确定能读取的到?

后台回复如下关键字,获取相关资料

0101-开发软件工具大礼包


傍晚五点出头是最美好的时刻,因为离下班不远了~


然鹅,老大一个消息把我拉近群里,协助排查问题。一个同事查了快一天了,也不清楚问题出现在哪里。



入会,boss、架构师、同事正襟危坐,开始说现象:

有一笔下发给其他系统的数据中,是更新之前的数据,而数据库显示的却是更新之后的数据。


而同事也斩钉截铁的说:是更新之后再查询数据发给其他系统的,不可能还是更新之前的数据。


查日志、看MQ消息,捞MongoDB,最终由日志文件中找到的 SELECT 语句打印的日志确实是更新之前的数据。


直接拉代码看,不看不知道,一看就出事(因涉密我这里列出的是模拟的代码):

@Service
public class UserService {
    @Autowired
    private UserCellService userCellService;

    @Autowired
    private MqTemplate mqTemplate;

    /**
     * 更新用户
     */

    public void updateUser(UserVO userVO) {
        // 创建用户
        userCellService.update(userVO);

        // 异步下发数据
        sendData(userVO.getUserId());
    }

    /**
     * 异步推送数据
     */

    private void sendData(Long userId) {
        CompletableFuture.runAsync(() -> {
            UserVO userVO = userCellService.findUserById(userId);
            mqTemplate.send(userVO);
        });
    }
}
@Service
public class UserCellService {
    @Transactional
    public void updateUser(UserVO userVO) {
        // 1.校验用户合法性
        validateUser(userVO);

        // 2.新增用户
        userDao.updateUser(userVO);
    }
}

调用流程:controller类 --> UserService.updateUser  --> UserCellService.updateUser --> UserService.sendData


项目使用声明式事务,会将以 Service 为后缀的方法纳入事务管理。


实际的代码比上面的复杂,但调用流程是差不多的,我定睛一看:出事了。


内心保持冷静,再推敲下:真出事了。


分析结果:

  • controller 调用 UserService.updateUser 方法的时候已经开启 Spring 事务,进入到 UserCellService.updateUser 方法时,也只是沿用了上一层的事务而已

  • 退出 UserCellService.updateUser 方法时,数据其实还没真正 commit 到数据库,因为 UserService.updateUser 整个方法还未退出

  • 这时,又启动了一个子线程去查询主线程中还未提交到数据库的数据,当子线程处理速度快过主线程,主线程还未commit,这时候子线程查到的就是之前旧的数据


上面出现的问题,测试人员是基本测不出来的,因为这是偶发现象。


这种问题在工作中还是经常会出现的,Spring事务、异步处理要非常小心这种情况。对应的解决方案其实也有:

  • 在外层将整个 VO 传到子线程

  • 等待主线程commit完毕,再处理子线程逻辑(这个SpringBoot有提供一个事务提交异步监听机制,很方便,后续我会写一篇文章说明)