本地消息表
ebay方案:https://queue.acm.org/detail.cfm?id=1394128
为解决producer端消息发送和本地事务执行原子性问题,将需要分布式处理的任务通过消息日志方式存储到一个地方,在本地事务完成之前,这个消息对于消费者是不可见的,本地事务执行成功之后,消费者才会看到这个消息并进行消费。
消息日志可以存储到本地文本,数据库或消息队列,通过业务规则自动或者人工方式发起重试。
人工重试多用于支付场景,通过对账系统对事后问题进行处理。
- 预发送消息到mq,消费者是看不到此消息的,因此不会进行消费
- 执行本地事务,比如操作数据库,依赖本地事务
- 如果本地事务执行成功,则进行mq消息确认。如果失败,则回滚mq消息
对于本地消息队列来说,把大事务转变成小事务,举个例子:
- 当扣钱时,需要在扣钱当服务器上增加一个本地消息表,需要把扣钱和减去的库写入存放到本地消息表,依靠数据库本地事务保证一致性
- 定时任务轮训本地消息表,把没有发送的消息发送给商品目标服务器,让其去减库存,达到商品服务的请求先写入本地消息表,进行扣减,扣减成功后,更新消息表状态
- 商品服务通过定时任务扫描消息表,扣减库存成功后修改本地消息表状态
- 如果定时任务扫描到没有执行成功的消息,则进行重发,商品服务接受到消息后判断消息是否重复,保证幂等性
本地事务实现
可以将消息放到一个地方存起来,比如在数据库中建立一个表,将消息放入这个表,称之为本地事务,这个表中有个state字段表示消息状态,在预发送消息阶段,标记成unkonwn状态。
之后根据本地事务执行结果,修改state,执行成功设置为local_commit,执行失败执行local_rollback。
同时可以建立一个异步现场执行兜底,不断从这个表中查询状态为local_commit的消息,将其发送到mq中。
- 如果发送mq成功,整个事务可以任务执行结束,修改状态为global_commit,接下来消费者进行消息消费。
- 如果发送mq失败,可以进行重试,直到成功,如果先限制重试次数,可以在表中增加retry_count字段每次重试就+1,当超过重试阈值后,就不再发送,可以指定一个消息超时时间,超过时间阈值后,就不再发送。对于失败的消息,将其标记为message_error,还可以增加一个cause字段,表示因为什么原因导致消息发送失败。
如果本地消息状态修改失败,那么一个消息可能一直处于unkonwn状态,而异步现成只会发送那些local_commit的消息到mq中,这样一些消息会一直被忽略,就产生了消息丢失。
一般有三种解决方案:
一:扩大事务边界
将预发送消息,执行本地事务,修改本地事务表状态三个操作,合并到一个事务里面。第一步预发送消息之前就开启事务,在第三步执行结束之后提交或回滚事务,,这样通过事务保证了本地消息表的消息记录,和操作产生记录总是成功或者失败。
二:合并事务状态
可以合并事务状态,直接和正常的数据库操作合并到一个事务中,写入到数据库直接就是local_commit,之后异步现成发送逻辑不变。
三:对prepared状态消息进行检查
简单的操作可以直接执行一次DB事务就可以了,如果复杂的一些场景,比如A业务发起方除了需要操作本地数据库,还需要进行RPC调用查询其他业务B,以获取一些mq消息需要的信息。这样可能A需要先将消息保存下来,等到B可以提供消息之后再发送。
这种情况的策略是,A和B之间约定一个可以异步处理的时间阈值,让异步线程除了发送local_commit状态的消息,还需要对prepared状态消息进行检查。依靠设置的时间阈值,在过滤消息时,prepared消息对时间和当前时间必须满足一定的时间阈值,避免和新事务消息的prepared消息状态混淆。
依赖MQ事务
消息队列的事务实现类似于本地消息表,只不过是将实现放到了MQ内部。
流程如下:
- 第一阶段prepared消息,拿到消息地址
- 执行本地事务
- 通过第一阶段拿到地址去访问消息,并修改状态,消息接收者使用这个消息
如果消息确认操作失败,mq broker会定时扫描没有更新状态的消息,如果有消息没有得到确认,会向消息发送者发送消息,判断是否提交了。 如果消息消费超时了,需要一直重试,消息接收端需要保证幂等,如果消息消费失败,需要人工进行处理,因为概率较低,设计复杂的流程反而得不偿失。
消息队列一般有事务处理方案,可以解决producer发送消息和本地事务执行的原子性操作。 MQ的方案一般也是将消息找个地方存起来,RocketMQ将消息存放到内部主题中。为了支持事务,RocketMQ引入了Half Topic及Operation Topic两个内部队列来存储事务消息推进状态。
- Half Topic对应队列中存放着prepare消息,就是预发送消息,消息不直接发送到topic,因此消费者对其不可见,实现暂存。
- Opreation Topic对应的队列存放prepare message对应的commit/rollback消息,消息体中是prepare message对应的offset。
RocketMQ中事务消息发送流程如下:
事务生产者预发送消息
通过TransactionMQProducer发送事务消息,这个producer在一条普通的message上加一些数据,表示这个是一条预发送的事务消息。broker在发现这是一条事务消息的时候,将其放到half topic中。
执行本地事务
发送prepare消息之后,需要执行本地事务,需要实现RocketMQ提供的一个TransactionListener 接口方法完成。
- executeLocalTransaction方法:用于执行本地事务,可以操作数据库或者干一些别的事情
- checkLocalTransaction方法:用于检查事务状态
这两个方法返回一个表示本地事务消息的执行状态LocalTransactionState,事务生产者会将其上报给broker,状态如下:
public enum LocalTransactionState {
COMMIT_MESSAGE,
ROLLBACK_MESSAGE,
UNKNOW,
}
本地事务状态处理
生产者拿到状态后上报broker,broker在处理时,会根据状态进行处理。
如果是commit/rollback状态: brokder会把收到的事务消息状态记录在内部的operation topci中,消息体中是prepare message对应在half topci中的offset。
- 如果是rollback消息,broker将从half topic中删除该prepare消息不进行下发
- 如果是commit消息,broker会把这个消息取出来,发送到原始的目标topci中,此时consumer端可以消费
上图可以观察到,一些异常情况下,可能上报事务消息状态失败,因此operate topci中没有记录,两者之间的差值一般就是unkown为确认中间状态的消息,需要进行特殊处理。
unknow状态消息
如果是unknow状态消息,说明存在不确定的事务状态,broker需要主动询问客户端producer。
出现unknow状态一般由于以下原因造成:
- 如果本地事务支持过程中,执行端挂掉,或者超时时会出现异常状态
- 一些特殊场景,需要等待一段时间满足特定场景,才把消息交给消费者进行消费的,可能需要主动的返回unknow状态,属于有意为之
由于unknown中间状态的消息,不会提交到operation topic中,因此half topci和operation topic这两个内部主题中,服务端通过对比两个主题的差值来找到未被提交的超时任务,进行回查。 所以业务方需要提供一个方法让rocketmq来回调,TransactionListener 中的checkLocalTransaction 就是用于回查。rocketmq会把之前发送的消息当作参数入参,业务实现根据消息内容可以反查业务信息,来确定状态。 broker主动轮训客户端producer事务状态,依赖于broker和producer端的双向通信能力来完成的,broker会主动给客户端producer发请求。
Sage事务
Saga事务是将长事务拆分成多个本地短事务,有Saga协调器协调,如果正常结束则算完成,某个步骤失败,则根据相反顺序调用一次补偿操作。
总结
RocketMQ事务消息可以解决事务的一致性问题,事务发起方需要关注本地事务执行及实现回查接口进行事务状态判断。
RocketMQ事务消息处理的限制:
- 事务消息没有延迟和批量支持,不能使用延迟消息特性和批量发送消息特性
- 为避免多次检查单个消息导致half topic消息积累,默认将单个消息的检查次数限制为15次
- 通过transactionTimeout 配置检查事务消息的固定周期
- 可以多次检查和消费事务消息
- 将事务消息交给目标topic可能会失败,rocketmq本身高可用机制确保高可用性,为保证事务消息不丢失或事务完整性可以采用同步双写机制
- 事务消息生产者id不能和其他类型消息的生产者id共享,和其他类型消息不同,事务消息允许后向查询,mq server按照其生产者id查询客户端
订阅数据库binlog
为避免对于业务的入侵,可以采用binlog实现可靠性发送:
- 先把本地事务执行完成,本地事务每个数据库更新操作都产生binlog event,event在本地事务成功后,才会产生
- 通过binlog订阅组件,订阅数据库变更,订阅到binlog event,说明执行了本地事务信息,可以放心根据event解析相应信息,发送到mq即可
kafka的事务消息
kafka producer支持两种模式:幂等生产者和事务生产者。
- 幂等生产者:将kafka交付语意从at least once加强到exactly once,生产者重试将不会引入重复
- 事务生产者:允许应用程序以原子方式同时发送消息到多个主题和分区
kafka类似于数据库事务的原子性,可以在kafka之前加个代理,由代理暂存事务消息,条件满足后,再发送到目标topic供消费者消费。