背景:
最近在维护银行账户系统,运营反馈,二级商户注册的过程中,遇到系统异常的问题,通过查询日志发现账户服务节点报了一个查不到数据的情况。经过反复检查,确认按日志中打印的请求参数、sql语句在uat环境下执行发现可以查到数据,那么问题来了,到底是什么原因导致的程序查不到数据而报错呢?
问题确认过程:
首先,按照日志中的sql以及查询参数,实际执行后能够查询到数据,据此排除请求参数问题。细细评味到底是什么情况导致实际能查到,但程序运行过程中没有查询到数据的报错呢?
然后,通过 对比 日志打印的时间 和 数据落库的时间,发现 日志打印的时间仅仅比数据落库的时间相差十几毫秒。不要被这是几毫秒的时间差欺骗了,首先数据落库时间 是由程序创建的,并非真实数据落库的时间,应早于数据落库的时间。日志打印的时间也并非sql写入的时间,由于系统日志大多是使用拦截器interceptor进行打印的,那么日志打印是晚于数据落库时间的。所以仔细分析,就会发现一下问题:
库表中的创建时间 早于 数据真实写表的时间
未查询到数据的日志打印时间 晚于 数据查询的时间
库表中的创建时间 几乎等于(相差ms之间) 未查询到数据的日志打印时间
那么只能得到一种结果程序出现了数据脏读的问题。那么时候呢情况下会出现数据脏读的情况呢?
仔细回想当初学习过的spring事物的隔离级别和事物会产生的问题,初步猜测这里发生了数据数据脏读的情况。
这里复习下:
事务隔离级
名称 | 结果 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
Read UnCommitted(读未提交) | 什么都不解决 | 是 | 是 | 是 |
Read Committed(读提交) | 解决了脏读的问题 | 否 | 是 | 是 |
Repeatable Read(重复读) | (mysql的默认级别)解决了不可重复读 ) | 否 | 否 | 是 |
Serializable(序列化) | 解决所有问题 | 否 | 否 | 否 |
- READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读、不可重复读和幻读问题。
- READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更数据,可避免脏读,仍会出现不可重复读和幻读问题。
- REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。
- SERIALIZABLE(序列化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可避免所有并发问题,但性能非常低。
事务产生的问题
场景:同一个事务内(同一个服务内)
名称 | 数据的状态 | 实际行为 | 产生原因 |
---|---|---|---|
脏读 | 未提交 | 打算提交但是数据回滚了,读取了提交的数据 | 数据的读取 |
不可重复读 | 已提交 | 读取了修改前的数据 | 数据的修改 |
幻读 | 已提交 | 读取了插入前的数据 | 数据的插入 |
发生在异步线程之间的数据脏读。
仔细阅读代码,发现代码情况大致如下:
//服务A
public 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() {
@Transactional
public String ex() {
//订单数据写入
Order order= new Order();
orderService.save(order);
//订单明细数据写入
OrderDetail orderDetail = new OrderDetail();
orderDetailService.save(orderDetail);
retrue “success”;
}
}
--------------------------------------------------------------------------------------
//服务B
public 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
*/
@Component
public class TransactionCommitHandler {
public void handle(Runnable action){
if (TransactionSynchronizationManager.isActualTransactionActive()){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public 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(() -> {
//发送复审mq
rabbitMqService.sendOrderMsg(order.getOrderId(), order.getPlId(), AssetTypeEnum.AUTO_PLAY);
});
}