分布式系统开发实战:消息篇

本地消息表的实现方式应该是业界使用最多的 , 其核心思想是将分布式事务拆分成本地事务进行处理 , 这种思路来源于ebay 。 我们可以从图18-20所示的本地消息表流程中看出一些细节 。
分布式系统开发实战:消息篇
文章图片
本地消息表基本流程如下 。
消息生成方 , 需要额外建一个消息表 , 并记录消息发送状态 。 消息表和业务数据要在一个本地事务里提交 , 也就是说它们要在同一个数据库里面 。 然后消息会经过消息中间件(比如Kafka等)发送到消息的消费方 。 如果消息发送失败 , 会进行重新发送 。 消息消费方 , 需要处理这个消息 , 并完成自己的业务逻辑 。 此时如果本地事务处理成功 , 表明已经处理成果成功了 , 如果失败 , 那么就会重试执行 。 如果是业务上的失败 , 还可以给生产方发送一个业务补偿消息 , 通知生产方进行回滚操作 。 生产方和消费方定时扫描本地消息表 , 把还没有处理完成的消息或者失败的消息再发送一遍 。 如果有靠谱的自动对账补账逻辑 , 这种方案还是非常实用的 。这种方案遵循BASE理论 , 采用的是最终一致性 , 这种方案既不会出现像2PC那样复杂的实现(当调用链很长的时候 , 2PC的可用性是非常低的) , 也不会像TCC(Try-Confirm-Cancel)那样可能出现确认或者回滚不了的情况 。
·优点:一种非常简单的实现 , 避免了分布式事务 , 实现了最终一致性 。 ·缺点:消息表会耦合到业务系统中 , 消息表的处理会带来一定的工作量 。 关系型数据库的吞吐量和性能方面存在瓶颈 , 频繁地读写消息会给数据库造成压力 。 所以 , 在真正的高并发场景下 , 该方案也会有瓶颈和限制 。有一些第三方的消息中间件是支持事务消息的 , 比如RocketMQ , 它们支持事务消息的方式也是类似于采用的二阶段提交 , 实际上是对本地消息表的一个封装 , 将本地消息表移动到了消息中间件内部 。 市面上一些主流的消息中间件内部大多是不支持事务消息的 , 比如ActiveMQ、RabbitMQ和Kafka都不支持 。
以RocketMQ中间件为例 , 事务消息的基本流程如下 。
·主事务向消息队列发送预备(Prepared)消息 。 ·主事务收到ACK之后本地执行主事务 。 ·根据执行的结果(成功或失败)向消息队列发送提交或者回滚消息 。也就是说在业务方法内要想消息队列提交两次请求:一次发送消息和一次确认消息 。 如果确认消息发送失败了 , RocketMQ会定期扫描消息集群中的事务消息 , 这时候发现了Prepared消息 , 它会向消息发送者确认 , 所以生产方需要实现一个check接口 , RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息 。 这样就保证了消息发送与本地事务同时成功或同时失败 , 如图18-21所示 。
分布式系统开发实战:消息篇
文章图片
对于消费发送者而言 , 如果消费超时 , 则需要一直重试 , 消息接收者需要保证幂等 , 如图18-22所示 。 如果消息消费失败 , 这时就需要人工进行处理 , 这个概率较低 , 如果为了这种小概率事件而设计复杂的流程反而得不偿失 。
分布式系统开发实战:消息篇
文章图片
·优点:实现了最终一致性 , 不需要依赖本地数据库事务 。 ·缺点:实现难度大 , 主流消息中间件不支持 , RocketMQ事务消息部分代码也未开源 。可以依赖数据库本身的功能来提供幂等性 。 比如单条insert操作 , 我们一般会依赖唯一键 , 这个唯一键是跟业务相关的一个序列号 。 如果同个序列号被重复执行了insert , 那么数据库自然就会抛出异常而后回滚事务 。