简介
理论模型是ebay 提出的基于可靠消息的分布式事务解决方案
架构图
- 架构图来自吴水成老师的教程,感谢老师
重点
- 可查询,一个事务具备唯一的 bizId/txId , 可以跟踪事务的执行,可以查询各个状态的事务,从而推动事务进入下一步流程
- 重试投递,消费消息失败时,队列可以根据指定的重试策略重试投递,超过重试次数,进入死信队列,报警、人工操作
- 幂等操作
关于ready 状态的思考
- 如果我们将 ready 状态的消息发送到消息队列,消息队列的消费方就会立即消费,不符合我们的需求
- 我们将 ready 状态的消息放到 Redis,设置过期时间,执行业务操作,然后去redis 中获取对应的 ready 消息,如果获取不到,throw
!!!!! 注意 !!!!
- 我们可以采用下面的思路
-
- 将 bizId/txId 放入到队列,同时将 msg (包括当前状态以及其他有效信息) 放入到redis, bizId/txId 作为key
-
- 消费方消费的时候,根据 bizId/txId 当做key 获取到具体的msg , 判断当前 msg.status , 如果是 ready 就不实行,如果是 commited 就可以执行
注意!!!!
- 实际对于上面的情况,默认选择了,confirm 一定在最后并且所有的都会成功,比如下面,如果 confirm 1 发送成功,confirm2 发送失败,这个时候回滚,实际上 confirm1 的时候,msg1 已经被成功消费了
业务-预发送1-业务-预发送2-业务-预发送3-业务-confirm1-confirm2-confirm3
- 针对这个问题,我们可以添加一个 parentBizId , 只有 parentBizId 为confirm 才会真的confirm,这样就成了下面这种
parendBizId.status=0 - 业务 - 预发送1 - 业务 - 预发送2 - 业务 - 预发送3 - 业务 - confirm1 - confirm2 - confirm3 -parendBizId.status=1 如上面的逻辑,如果confirm1 收到消息,查询该消息的 parentBizId 发现不为空,代表需要验证parent , 如果发现parentBizId 的 parendBizId 为空,则代表为最高级parent,这时候直接验证该parendBizId 是否confirm 即可。否则继续验证parentBizId 的parentBizId
关于幂等的一个问题及思考
问题
问题来自微信群
try {
//业务唯一标识幂等
// todo set status
Boolean add = redisApi.addNX(UUID));
if(!add){
logger.error(new Date() + " repeat .");
//返回消息消费成功
return OrderAction.Success;
}
// kill -9
//消费
toService(msg);
logger.info(new Date() + " success“);
// todo 这里是不是可以设置 status
return OrderAction.Success;
} catch (Exception e) {
redisApi.del(UUID);
logger.error("消费失败重试",e);
return OrderAction.Suspend;
}
如果消费过程中 jvm 爆了 下次ack 超时重试的时候 redis里面是有值的 会被认为重复的消息 而实际消费没有成功不会再消费
思考
- 首先,该代码中没有体现消费失败后重试次数的问题,需要注意
- 该代码在业务执行完毕之后是不是可以添加一个 setStatus 操作,标记该 bizId 的 status 为 finished
- 我们应该是有消息确认机制的,对于一个bizId ,他的 执行 status 流程应该是一直的,也就是我们首先可以观察到该bizId 是不是执行过
- 如果真的存在redis 中标记执行过,实际业务未执行过的情况,那么我们是不是可以设置一个参数字段,比如对于人工处理的时候,可以手动执行是否跳过幂等检查,这样就可以再次执行业务
注意
- 只要主动方发送成功消息,我们就认为整个事务成功,认为被动方一定会执行成功
- 被动方的处理结果不影响主动方的处理结果, 被动方的消息处理操作是幂等操作
- 如果我们不采用中间件,不采用 ready 中间状态,只是基于传统队列,基于队列的ack,采用下面的流程
业务操作-发送消息-收到ack-true/false(commit/rollback)
首先我们注意右边是直接的 mq , 如果消息中间件接收消息成功,消息被立即消费,但是第2步出现问题,造成 biz 回滚,造成业务不一致。 如果采用下面的流程
首先我们注意右边是直接的 mq , 如果 ack1 返回true , 就会消费掉msg1 , 这时候生产者发送msg2,但是msg2 如果发送失败,这时候应该回滚所有操作包括msg1,但是这时候msg1 早就被消费掉了,造成数据不一致
参考
| 节点 | 角色说明| | :————: |:—————| :—: | | 消息服务子系统 | 暴露服务的服务提供方,业务服务实现(预发送消息、确认发送消息等)| |消息确认子系统 | 对于长时间未确认的消息,与上层业务系统确认消息是否发送| | 消息恢复子系统 | 对于长时间未消费的消息,重新发送消息给下层业务系统| | 消息管理子系统 | 消息可视化管理后台,消息队列配置、消息查看、重发、删除等| | 消息中间件 | 提供消息队列功能,ActiveMQ,RabbitMQ …|