最近开始学习后台开发,虽然与我以前从事的 Android 开发一样都是使用 Java 语言,但是技术栈完全不同,有太多的必备的「新」概念要去学习,而在对它们,以及别人写的代码有充分的了解之前,就可能会遇上这种一杯茶,一根烟,一个 Bug 一天根本改不完的情况。
最近遇见的这个 Bug 是在修改项目遗留的问题时偶然发现的,简而言之就是这样:
服务 A 在从外界接收到推送的一条数据后,将数据插入到库里,然后通过 MQ 推送一条消息给 服务 B,服务 B 会根据收到的消息进行一些处理,其中包括远程调用 服务 A 的方法去查询这条数据,但是在测试环境总是报查询不到这条数据。
遇到问题之后,先进行了一些排查:
纳闷了一阵以后,继续排查:
直到我终于留意到一个现象:从日志来看,服务 A 插库与 服务 B 远程调用 服务 A 的方法的时间只相差 1 毫秒。会不会是一切发生得太快了,库里还查不到刚刚写入的数据?抑或者查询的时候插库还根本没有生效?
带着这个疑惑我终于认真去看插库并发消息那块的代码了,于是就看到这样一段代码:
@Override
@Transactional(...)
public boolean doSomething() {
...
// 插入数据
// 发送消息
...
}
是的没错,插入数据和发送消息写在了一个事务里面。虽然我对数据库了解不多,但对事务的特性还是有所了解——发送消息的时候,数据库里确实还没有刚刚插入的数据,事务提交后才会生效,也就是说,服务 B 收到消息后远程调用回 服务 A 想查找刚刚插入的数据,能否查到全凭运气,取决于此时事务已经执行完。
问题时序示意:
要确保消息发出时数据库里已经存在数据了也很简单,将事务粒度控制一下,只包含插入数据这块逻辑即可,插入成功了再发送消息。
PS:如果对消息投递可靠性要求高,可能需要对投递消息失败的情况做一些补偿机制。
@Override
public boolean doSomething() {
...
// 事务开始
// 插入数据
// 事务结束
if (插入数据成功) {
// 发送消息
}
...
}
正常时序示意:
总结: