不一样于单一架构应用(Monolith), 分布式环境下, 进行事务操做将变得困难, 由于分布式环境一般会有多个数据源, 只用本地数据库事务难以保证多个数据源数据的一致性. 这种状况下, 可使用两阶段或者三阶段提交协议来完成分布式事务.可是使用这种方式通常来讲性能较差, 由于事务管理器须要在多个数据源之间进行屡次等待. 有一种方法一样能够解决分布式事务问题, 而且性能较好, 这就是我这篇文章要介绍的使用事件,本地事务以及消息队列来实现分布式事务.数据库
咱们从一个简单的实例入手. 基本全部互联网应用都会有用户注册的功能. 在这个例子中, 咱们对于用户注册有两步操做:
1. 注册成功, 保存用户信息.
2. 须要给用户发放一张代金券, 目的是鼓励用户进行消费.
若是是一个单一架构应用, 实现这个功能很是简单: 在一个本地事务里, 往用户表插一条记录, 而且在代金券表里插一条记录, 提交事务就完成了. 可是若是咱们的应用是用微服务实现的, 可能用户和代金券是两个独立的服务, 他们有各自的应用和数据库, 那么就没有办法简单的使用本地事务来保证操做的原子性了. 如今来看看如何使用事件机制和消息队列来实现这个需求.(我在这里使用的消息队列是kafka, 原理一样适用于ActiveMQ/RabbitMQ等其余队列)编程
咱们会为用户注册这个操做建立一个事件, 该事件就叫作用户建立事件(USER_CREATED). 用户服务成功保存用户记录后, 会发送用户建立事件到消息队列, 代金券服务会监听用户建立事件, 一旦接收到该事件, 代金券服务就会在本身的数据库中为该用户建立一张代金券. 好了, 这些步骤看起来都至关的简单直观, 可是怎么保证事务的原子性呢? 考虑下面这两个场景:
1. 用户服务在保存用户记录, 还没来得及向消息队列发送消息以前就宕机了. 怎么保证用户建立事件必定发送到消息队列了?
2. 代金券服务接收到用户建立事件, 还没来得及处理事件就宕机了. 从新启动以后如何消费以前的用户建立事件?
这两个问题的本质是: 如何让操做数据库和操做消息队列这两个操做成为一个原子操做. 不考虑2PC, 这里咱们能够经过事件表来解决这个问题. 下面是类图. json
EventPublish是记录待发布事件的表. 其中:
id: 每一个事件在建立的时候都会生成一个全局惟一ID, 例如UUID.
status: 事件状态, 枚举类型. 如今只有两个状态: 待发布(NEW), 已发布(PUBLISHED).
payload: 事件内容. 这里咱们会将事件内容转成json存到这个字段里.
eventType: 事件类型, 枚举类型. 每一个事件都会有一个类型, 好比咱们以前提到的建立用户USER_CREATED就是一个事件类型.
EventProcess是用来记录待处理的事件. 字段与EventPublish基本相同.缓存
咱们首先看看事件的发布过程. 下面是用户服务发布用户建立事件的顺序图.
1. 用户服务在接收到用户请求后开启事务, 在用户表建立一条用户记录, 而且在EventPublish表建立一条status为NEW的记录, payload记录的是事件内容, 提交事务.
2. 用户服务中的定时器首先开启事务, 而后查询EventPublish是否有status为NEW的记录, 查询到记录以后, 拿到payload信息, 将消息发布到kafka中对应的topic.
发送成功以后, 修改数据库中EventPublish的status为PUBLISHED, 提交事务.架构
下面是代金券服务处理用户建立事件的顺序图.
1. 代金券服务接收到kafka传来的用户建立事件(其实是代金券服务主动拉取的消息, 先忽略消息队列的实现), 在EventProcess表建立一条status为NEW的记录, payload记录的是事件内容, 若是保存成功, 向kafka返回接收成功的消息.
2. 代金券服务中的定时器首先开启事务, 而后查询EventProcess是否有status为NEW的记录, 查询到记录以后, 拿到payload信息, 交给事件回调处理器处理, 这里是直接建立代金券记录. 处理成功以后修改数据库中EventProcess的status为PROCESSED, 最后提交事务.并发
回过头来看咱们以前提出的两个问题:
1. 用户服务在保存用户记录, 还没来得及向消息队列发送消息以前就宕机了. 怎么保证用户建立事件必定发送到消息队列了?
根据事件发布的顺序图, 咱们把建立事件和发布事件分红了两步操做. 若是事件建立成功, 可是在发布的时候宕机了. 启动以后定时器会从新对以前没有发布成功的事件进行发布. 若是事件在建立的时候就宕机了, 由于事件建立和业务操做在一个数据库事务里, 因此对应的业务操做也失败了, 数据库状态的一致性获得了保证.
2. 代金券服务接收到用户建立事件, 还没来得及处理事件就宕机了. 从新启动以后如何消费以前的用户建立事件?
根据事件处理的顺序图, 咱们把接收事件和处理事件分红了两步操做. 若是事件接收成功, 可是在处理的时候宕机了. 启动以后定时器会从新对以前没有处理成功的事件进行处理. 若是事件在接收的时候就宕机了, kafka会从新将事件发送给对应服务.框架
经过这种方式, 咱们不用2PC, 也保证了多个数据源之间状态的最终一致性.
和2PC/3PC这种同步事务处理的方式相比, 这种异步事务处理方式具备异步系统一般都有的优势:
1. 事务吞吐量大. 由于不须要等待其余数据源响应.
2. 容错性好. A服务在发布事件的时候, B服务甚至能够不在线.
缺点:
1. 编程与调试较复杂.
2. 容易出现较多的中间状态. 好比上面的例子, 在用户服务已经保存了用户并发布了事件, 可是代金券服务还没来得及处理以前, 用户若是登陆系统, 会发现本身是没有代金券的. 这种状况可能在有些业务中是可以容忍的, 可是有些业务却不行. 因此开发以前要考虑好.异步
三、与具体业务场景绑定,偶尔性强,不能够共用分布式
四、消息数据和业务数据同一个库,占用业务数据库资源
另外, 上面的流程在实现的过程当中还有一些能够改进的地方:1. 定时器在更新EventPublish状态为PUBLISHED的时候, 能够一次批量更新多个EventProcess的状态.2. 定时器查询EventProcess并交给事件回调处理器处理的时候, 可使用线程池异步处理, 加快EventProcess处理周期.3. 在保存EventPublish和EventProcess的时候同时保存到Redis, 以后的操做能够对Redis中的数据进行, 可是要当心处理缓存和数据库可能状态不一致问题.4. 针对Kafka, 由于Kafka的特色是可能重发消息, 因此在接收事件而且保存到EventProcess的时候可能报主键冲突的错误(由于重复消息id是相同的), 这个时候能够直接丢弃该消息.