背景:

  1. 最近在维护银行账户系统,运营反馈,二级商户注册的过程中,遇到系统异常的问题,通过查询日志发现账户服务节点报了一个查不到数据的情况。经过反复检查,确认按日志中打印的请求参数、sql语句在uat环境下执行发现可以查到数据,那么问题来了,到底是什么原因导致的程序查不到数据而报错呢?

问题确认过程:

首先,按照日志中的sql以及查询参数,实际执行后能够查询到数据,据此排除请求参数问题。细细评味到底是什么情况导致实际能查到,但程序运行过程中没有查询到数据的报错呢?
然后,通过 对比 日志打印的时间 和 数据落库的时间,发现 日志打印的时间仅仅比数据落库的时间相差十几毫秒。不要被这是几毫秒的时间差欺骗了,首先数据落库时间 是由程序创建的,并非真实数据落库的时间,应早于数据落库的时间。日志打印的时间也并非sql写入的时间,由于系统日志大多是使用拦截器interceptor进行打印的,那么日志打印是晚于数据落库时间的。所以仔细分析,就会发现一下问题:

库表中的创建时间 早于 数据真实写表的时间
未查询到数据的日志打印时间 晚于 数据查询的时间
库表中的创建时间 几乎等于(相差ms之间) 未查询到数据的日志打印时间

那么只能得到一种结果程序出现了数据脏读的问题。那么时候呢情况下会出现数据脏读的情况呢?

仔细回想当初学习过的spring事物的隔离级别和事物会产生的问题,初步猜测这里发生了数据数据脏读的情况。
这里复习下:

事务隔离级

名称 结果 脏读 不可重复读 幻读
Read UnCommitted(读未提交) 什么都不解决
Read Committed(读提交) 解决了脏读的问题
Repeatable Read(重复读) (mysql的默认级别)解决了不可重复读 )
Serializable(序列化) 解决所有问题
  • READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读、不可重复读和幻读问题。
  • READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更数据,可避免脏读,仍会出现不可重复读和幻读问题。
  • REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。
  • SERIALIZABLE(序列化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可避免所有并发问题,但性能非常低。

事务产生的问题

场景:同一个事务内(同一个服务内)

名称 数据的状态 实际行为 产生原因
脏读 未提交 打算提交但是数据回滚了,读取了提交的数据 数据的读取
不可重复读 已提交 读取了修改前的数据 数据的修改
幻读 已提交 读取了插入前的数据 数据的插入

其实,还有一个问题:

发生在异步线程之间的数据脏读。

仔细阅读代码,发现代码情况大致如下:

  1. //服务A
  2. public class A() {
  3. @Transactional(rollbankFor = Exception.class)
  4. public String ex() {
  5. B b = new B();
  6. b.ex();
  7. }
  8. }
  9. public class B() {
  10. public String ex() {
  11. C c = new C();
  12. c.ex();
  13. //kafka异步执行
  14. kafkaPublish.publish("topic_msg", msg);
  15. }
  16. }
  17. public class C() {
  18. @Transactional
  19. public String ex() {
  20. //订单数据写入
  21. Order order= new Order();
  22. orderService.save(order);
  23. //订单明细数据写入
  24. OrderDetail orderDetail = new OrderDetail();
  25. orderDetailService.save(orderDetail);
  26. retrue success”;
  27. }
  28. }
  29. --------------------------------------------------------------------------------------
  30. //服务B
  31. public class D() {
  32. @KafkaListener(topic = "topic_msg")
  33. public void ex() {
  34. ..................
  35. ..................
  36. //订单数据查询
  37. Order order= orderService.findById(orderId);
  38. ..................
  39. }
  40. }
  • 此时可以大胆想象:

服务A写入订单数据事物还没来得及提交,便发送了kafka异步消息,服务B在监听到服务A发送的kafka消息,立即去查询服务A刚刚采用事务提交的订单写入数据。此时由于服务A的数据写入事务未提交,数据未持久化,自然服务B的异步消费不能够查询到服务A提交的数据。由此出现了数据查不到的情况。
当然这种情况的发生是随机发什的,取决于谁执行的快一些:

  • 若服务A事物提交 早于 服务B读取数据的时间,则能够正常查到数据;
  • 若服务A事物提交 晚于 服务B读取数据的时间, 则查不到数据。

解决方案:

  1. /**
  2. * 事务提交后的处理器,action执行严格依赖调用方的事务提交
  3. * 如果调用方没有事务,不执行;
  4. * 如果调用方事务回滚,不执行;
  5. * action异常不影响调用方事务提交;
  6. *
  7. * @Author tz
  8. * @Date 2020/12/18 14:12
  9. * @Version 1.0
  10. */
  11. @Component
  12. public class TransactionCommitHandler {
  13. public void handle(Runnable action){
  14. if (TransactionSynchronizationManager.isActualTransactionActive()){
  15. TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
  16. @Override
  17. public void afterCommit() {
  18. //具体的异步操作
  19. action.run();
  20. }
  21. });
  22. }
  23. }
  24. }
  1. /**
  2. * 保存工单及发送消息
  3. */
  4. @Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
  5. public void submitOrder(Integer ottStatus, Integer mppStatus, AssetPlaylistDto newPl) throws Exception {
  6. //... 其它操作
  7. AssetPlaylistOrders order = this.constructOrder(ottStatus, mppStatus, newPl);
  8. //保存工单
  9. this.save(order);
  10. transactionCommitHandler.handle(() -> {
  11. //发送复审mq
  12. rabbitMqService.sendOrderMsg(order.getOrderId(), order.getPlId(), AssetTypeEnum.AUTO_PLAY);
  13. });
  14. }