背景:
最近在维护银行账户系统,运营反馈,二级商户注册的过程中,遇到系统异常的问题,通过查询日志发现账户服务节点报了一个查不到数据的情况。经过反复检查,确认按日志中打印的请求参数、sql语句在uat环境下执行发现可以查到数据,那么问题来了,到底是什么原因导致的程序查不到数据而报错呢?
问题确认过程:
首先,按照日志中的sql以及查询参数,实际执行后能够查询到数据,据此排除请求参数问题。细细评味到底是什么情况导致实际能查到,但程序运行过程中没有查询到数据的报错呢?
然后,通过 对比 日志打印的时间 和 数据落库的时间,发现 日志打印的时间仅仅比数据落库的时间相差十几毫秒。不要被这是几毫秒的时间差欺骗了,首先数据落库时间 是由程序创建的,并非真实数据落库的时间,应早于数据落库的时间。日志打印的时间也并非sql写入的时间,由于系统日志大多是使用拦截器interceptor进行打印的,那么日志打印是晚于数据落库时间的。所以仔细分析,就会发现一下问题:
库表中的创建时间 早于 数据真实写表的时间
未查询到数据的日志打印时间 晚于 数据查询的时间
库表中的创建时间 几乎等于(相差ms之间) 未查询到数据的日志打印时间
那么只能得到一种结果程序出现了数据脏读的问题。那么时候呢情况下会出现数据脏读的情况呢?
仔细回想当初学习过的spring事物的隔离级别和事物会产生的问题,初步猜测这里发生了数据数据脏读的情况。
这里复习下:
事务隔离级
| 名称 | 结果 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|---|
| Read UnCommitted(读未提交) | 什么都不解决 | 是 | 是 | 是 |
| Read Committed(读提交) | 解决了脏读的问题 | 否 | 是 | 是 |
| Repeatable Read(重复读) | (mysql的默认级别)解决了不可重复读 ) | 否 | 否 | 是 |
| Serializable(序列化) | 解决所有问题 | 否 | 否 | 否 |
- READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读、不可重复读和幻读问题。
- READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更数据,可避免脏读,仍会出现不可重复读和幻读问题。
- REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。
- SERIALIZABLE(序列化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可避免所有并发问题,但性能非常低。
事务产生的问题
场景:同一个事务内(同一个服务内)
| 名称 | 数据的状态 | 实际行为 | 产生原因 |
|---|---|---|---|
| 脏读 | 未提交 | 打算提交但是数据回滚了,读取了提交的数据 | 数据的读取 |
| 不可重复读 | 已提交 | 读取了修改前的数据 | 数据的修改 |
| 幻读 | 已提交 | 读取了插入前的数据 | 数据的插入 |
发生在异步线程之间的数据脏读。
仔细阅读代码,发现代码情况大致如下:
//服务Apublic class A() {@Transactional(rollbankFor = Exception.class)public String ex() {B b = new B();b.ex();}}public class B() {public String ex() {C c = new C();c.ex();//kafka异步执行kafkaPublish.publish("topic_msg", msg);}}public class C() {@Transactionalpublic String ex() {//订单数据写入Order order= new Order();orderService.save(order);//订单明细数据写入OrderDetail orderDetail = new OrderDetail();orderDetailService.save(orderDetail);retrue “success”;}}--------------------------------------------------------------------------------------//服务Bpublic class D() {@KafkaListener(topic = "topic_msg")public void ex() {....................................//订单数据查询Order order= orderService.findById(orderId);..................}}
- 此时可以大胆想象:
服务A写入订单数据事物还没来得及提交,便发送了kafka异步消息,服务B在监听到服务A发送的kafka消息,立即去查询服务A刚刚采用事务提交的订单写入数据。此时由于服务A的数据写入事务未提交,数据未持久化,自然服务B的异步消费不能够查询到服务A提交的数据。由此出现了数据查不到的情况。
当然这种情况的发生是随机发什的,取决于谁执行的快一些:
- 若服务A事物提交 早于 服务B读取数据的时间,则能够正常查到数据;
- 若服务A事物提交 晚于 服务B读取数据的时间, 则查不到数据。
解决方案:
/*** 事务提交后的处理器,action执行严格依赖调用方的事务提交* 如果调用方没有事务,不执行;* 如果调用方事务回滚,不执行;* action异常不影响调用方事务提交;** @Author tz* @Date 2020/12/18 14:12* @Version 1.0*/@Componentpublic class TransactionCommitHandler {public void handle(Runnable action){if (TransactionSynchronizationManager.isActualTransactionActive()){TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCommit() {//具体的异步操作action.run();}});}}}
/*** 保存工单及发送消息*/@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED, rollbackFor = Exception.class)public void submitOrder(Integer ottStatus, Integer mppStatus, AssetPlaylistDto newPl) throws Exception {//... 其它操作AssetPlaylistOrders order = this.constructOrder(ottStatus, mppStatus, newPl);//保存工单this.save(order);transactionCommitHandler.handle(() -> {//发送复审mqrabbitMqService.sendOrderMsg(order.getOrderId(), order.getPlId(), AssetTypeEnum.AUTO_PLAY);});}
