前言
传统单体架构下,所有的功能模块都在一个应用下,所有的代码和业务逻辑都在同一个应用下实现,所以保证数据的一致性就很简单,保证相关操作都在同一个本地事务下就可以了。
但是在微服务架构下,将一个应用拆分成了多个独立的服务,每个服务都能有自己的数据库,服务间通信都是通过远程调用实现,实现一个功能可能需要由几个不同的服务来共同实现。这就会带来一个问题,不同的服务之间无法做到使用同一个事务,这就无法保证数据的一致性了。
解决分布式事务的问题,一劳永逸的方式就是直接使用 Seata,Seata 是一个开源的分布式事务解决方案,用于解决分布式系统中的数据一致性问题。但是,引入 Seata 实在是太重了,在实际工作中接触过的系统,并没有那么多的业务需要使用到分布式事务,为了解决那么一两个业务的问题却要为整个系统引入分布式事务服务,代价实在是太大了。而且,相关的业务实际上只需要保证数据的最终一致性,不用保证强一致性,所以在实践可以使用本地消息表的方案来解决分布式事务问题。
本地消息表方案
如果系统中只有少数服务需要用到分布式事务,那么直接在该服务下创建一张本地消息表,结合消息队列,就能够实现数据的最终一致性了。
表设计
本地消息表的设计如下
字段 | 类型 | 注释 |
id | long | id |
msg_type | varchar | 消息类型 |
biz_id | varchar | 业务唯一标志 |
content | text | 消息体 |
state | varchar | 状态(待发送,已消费) |
create_time | datetime | 创建时间 |
update_time | datetime | 更新时间 |
时序图
plantuml
@startuml 'https://plantuml.com/sequence-diagram autonumber participant Scheduled_Task database DB_A participant Service_A queue Queue participant Service_B database DB_B group Service_A本地事务 DB_A <- Service_A: 写本地业务数据 DB_A <- Service_A: 写本地消息数据 end Service_A -> Queue: 发送消息 note left 发送消息放到事务外 防止由于网络延迟出现 Service_A认为发送失败导致事务回滚 而实际上MQ收到了消息 Service_B也消费了消息 从而出现数据不一致的情况 end note Queue -> Service_B: 消费消息 Service_B -> Service_B: 幂等判断 note right 防止重复消费消息 end note group Service_B本地事务 Service_B -> DB_B: 写本地业务数据 end group 更新本地消息表状态 Scheduled_Task <- Service_B: 更新本地消息表状态 Scheduled_Task -> DB_A: 更新本地消息表状态 end note right 这里即使失败了也无所谓 两个服务的数据已经一致了 没有更新消息状态,定时任务会重新投递 Service_B做好幂等处理 等下次消费再去修改消息状态即可 end note loop 定时任务 Scheduled_Task -> DB_A: 查询未成功投递的消息 Scheduled_Task -> Queue: 重新投递 end @enduml
消息服务方案
本地消息表只只用系统中只有少量业务需要实现分布式事务的情况,如果系统中的绝大多数服务都存在分布式事务的业务场景,为每个服务都创建一张本地消息表显然很麻烦,难以维护。
用到的地方多,实际上就可以考虑上 Seata 了,专业的事要交给专业的服务来做。不过这里也可以改进一下本地消息表的方案,增加一个专门处理分布式事务消息的消息服务和消息库。
表设计
表设计同上面一样,消息服务只不过将所有本地消息表合并成一张表了
只不过 state 字段需要多一个状态来区分
在本地消息表方案中,由于写Service_A业务和创建消息是在同一个事务中,它们要么同时成功,要么同时失败,所以两个状态(待发送,已消费)就能表示整个消息的生命周期。
- 待发送状态,则表示Service_A业务和消息同时写入库成功,等待Service_B消费消息
- 已消费状态,则表示Service_B已经成功消费了消息
在消息服务方案中,写业务和创建消息不在同一个事务中,所以需要再加一个状态(发送中)
- 待发送状态,则表示刚创建消息,此时Service_A还没有写库
- 发送中状态:则表示Service_A已经写库成功,等待Service_B消费消息
- 已消费状态,则表示Service_B已经成功消费了消息
时序图
plantuml
@startuml 'https://plantuml.com/sequence-diagram autonumber database DB_A participant Service_A participant Service_Msg database DB_Msg queue Queue participant Service_B database DB_B group 创建消息 Service_A -> Service_Msg: 创建消息 note left: 生成业务id Service_Msg -> DB_Msg: 创建消息 note left: 消息状态:待发送 Service_A <- Service_Msg: 返回消息ID end group Service_A本地事务 DB_A <- Service_A: 写本地业务数据 end Service_A -> Service_Msg: 发送消息 note left: 携带消息ID Service_Msg -> DB_Msg: 更新消息状态 note right: 消息状态:发送中 Service_Msg -> Queue: 发送消息 Queue -> Service_B: 消费消息 Service_B -> Service_B: 幂等判断 group Service_B本地事务 Service_B -> DB_B: 写本地业务数据 end group 更新消息状态 Service_Msg <- Service_B: 更新消息状态 Service_Msg -> DB_Msg: 更新消息状态 note right: 消息状态:已消费 end loop 定时任务 Service_Msg -> DB_Msg: 查询状态为发送中的消息 Service_Msg -> Queue: 重新投递 end @enduml