文章目录
记录一次线上问题事件:
实习生小A在做一个需求时,需要从订单系统中生产业务消息,仓储系统中的消息订阅者进行消费。在这个过程中,订单、仓储系统都需要查询并且更新order表获取字段updateTime的值,但是系统上线后发现updateTime值更新异常,仓储系统查询到了订单update前的数据。
排查流程
- 怀疑是多个消息到达仓储系统,如果订单update更新时,仓储在消费多消息时将订单更新的updateTime的值进行覆盖。
应对这种情况,小A对两个系统的业务按照订单维度进行加锁。
但是问题继续出现了。。。。。。 - 查看了关键流程的日志记录,发现事务均提交成功,并且数据事务隔离级别都是RR。也不存在主从数据库的情况。
日志没发现啥异常 - 小A请求组内成员大佬,得出:发送MQ消息得在事务提交成功之后,在进行大事务操作时,需要对代码进行细致检查,或者对实现方式进行思考。
解决方案
对于事务中发送消息的情况
- 消息发送的前提,最好是在事务提交成功之后进行投递。
- 可以使用事务消息。
部分MQ产品提供事务消息特性,允许生产者先发送半事务消息,在本地事务完成后提交事务,此时消息队列才会真正投递消息给消费者。若本地事务失败,则MQ不会投递这条消息。
- 潜在的问题
- 若只是简单地将消息发送置于订单系统事务中,而未做特殊处理,一旦订单事务回滚,如果没有相应的事务消息支持机制,可能会出现消息已发送但订单记录未成功插入数据库的情况,反之亦然,这会导致数据不一致。
- 另一方面,即使消息成功发送到MQ,仓储系统在消费消息并执行事务操作时,其自身事务的失败并不会自动回滚订单系统的事务,因此也需要额外的补偿机制来保证最终一致性。
- 如果希望订单系统的数据库事务和消息发送事务保持原子性,即要么订单创建成功且消息发送成功,要么两者都失败回滚,您可以采用消息队列提供的事务消息功能。例如,某些MQ产品支持XA事务或者两阶段提交来保证分布式事务的一致性。
- 对于不同系统都需要更新相同表资源,技术方案的设计是不是有问题。
额外小知识
在分布式系统中,尤其是涉及到多个子系统共同操作同一份数据时,确实会面临一致性问题。针对您所描述的场景,可以补充以下几个方面的小知识和解决思路:
分布式事务:
- 分布式事务是指跨越多个数据库或服务的事务,为确保数据一致性,常见的解决方案有二阶段提交(2PC)、三阶段提交(3PC)、Saga模式、TCC(Try-Confirm-Cancel)模式等。其中,部分MQ产品支持的事务消息正是基于这些理论的一种实现方式。
消息顺序性:
- 在此案例中,由于订单和仓储系统都在更新
updateTime
,要确保更新顺序,除了通过事务机制,还可以借助消息队列的顺序消息功能,确保消息严格按序消费。
- 在此案例中,由于订单和仓储系统都在更新
资源竞争与锁优化:
- 即使在单个系统内部,面对多线程并发访问同一资源(如updateTime),也需合理设计锁机制,避免死锁和并发问题。此外,可考虑使用乐观锁或悲观锁,甚至条件变量等手段提高并发性能。
领域驱动设计(DDD)和数据模型优化:
- 在设计系统时,可以运用领域驱动设计方法,明确区分各个子系统的职责边界,减少共享资源的竞争。例如,是否可以通过增加一个只由仓储系统维护的状态字段,而不是直接更新公共的
updateTime
字段。
- 在设计系统时,可以运用领域驱动设计方法,明确区分各个子系统的职责边界,减少共享资源的竞争。例如,是否可以通过增加一个只由仓储系统维护的状态字段,而不是直接更新公共的
异步解耦:
- 异步消息队列的引入就是为了实现系统间的解耦,但在实际操作中,确实需要注意消息生产和消费的事务一致性问题。在本例中,改为事务提交后再发送消息是一种有效解决方案。
幂等性设计:
- 确保消息消费者的业务逻辑具备幂等性,即使重复收到同一条消息也能正确处理,这对于解决消息重发、乱序等问题十分重要。
对于上述场景,建议调整为事务提交后发送消息,并根据实际情况选择适合的分布式事务解决方案。同时,重新审视系统间交互的设计,尽量减少对同一资源的竞争和更新冲突。