title: 基于RabbitMQ的可复用的事务消息方案
什么是事务性消息
可以认为是一个确保分布式系统中的最终一致性的两阶段提交(two-phase commit )的消息实现。
事务性消息确保本地事务的执行和消息的发送是一个原子性操作。
先扔一张图片欣赏下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u4XCIDP4-1592319134529)(rabbit-pic1.PNG)]
我们要达到可复用的事务性消息至少要满足以下特点:
1、消息发送确认机制
2、消息接受确认机制
3、消息重投
4、消费幂等性
5、RabbitMQ 持久化 等等
事务消息原则上只适合弱一致性(或者说最终一致性)的场景,针对强一致性要求的业务逻辑则不适用。因为弱一致性就意味着
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。
以账户交易作为参考,属于强一致性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OSnSxLaE-1592319134535)(rabbit-pic2.PNG)]
一般情况下,要求强一致性说明要严格同步,也就是所有操作必须同时成功或者同时失败,这样就会引入同步带来的额外消耗。如果一个事务消息模块设计合理,补偿、查询、监控等等功能都完毕,由于系统交互是异步的,整体吞吐要比严格同步高。在笔者负责的业务系统中基于事务消息使用还定制了一条基本原则:消息内容正确的前提下,消费方出现异常需要自理。
为了降低代码的入侵性,事务消息需要借助Spring的编程式事务或者声明式事务。编程式事务一般依赖于TransactionTemplate,而声明式事务依托于AOP模块,依赖于注解@Transactional。
针对以上提出的前提条件,作出以下设计
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hnXOp6Aw-1592319134541)(rabbit-pic3.PNG)]
简单介绍下步骤:
- 写入数据库;
- Sender 发送消息;
- Consumer 消息确认;
- 更新数据库消息状态,消息接收成功;
- 定时任务获取数据库消息状态;
- 重试发送;
- 重试数量大于 N 次,修改状态;
小结思考,笔者在设计RabbitMQ可复用事务消息方案
1)为了统一处理补偿推送的问题,使用了有限状态判断消息是否已经推送成功。在事务方法内,保存事务消息的时候,标记消息记录推送状态为处理中;通过事务同步器接口TransactionSynchronization的afterCommit()方法的实现中,推送对应的消息到RabbitMQ,然后更变事务消息记录的状态为推送成功。
2)还有一种极为特殊的情况是RabbitMQ服务端本身出现故障导致消息推送异常,这种情况下需要进行重试(补偿推送),经验证明短时间内的反复重试是没有意义的,故障的服务一般不会瞬时恢复,所以可以考虑使用指数退避算法进行重试,同时需要限制最大重试次数。
插播一则广告:二进制指数退避算法,又称为二元指数后退算法。退避算法是以冲突窗口大小为基准的,每个节点有一个冲突计数器C。退避的时间与冲突次数具有指数关系,冲突次数越多,退避的时间就可能越长,若达到限定的冲突次数,该节点就停止发送数据。
上面一段话不知道大家看懂没,反正我没有看懂,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M6ehQ3IX-1592319134545)(rabbit-pic5.PNG)]
指数值、间隔值、最大重试次数按照具体的业务场景定义。
表设计
CREATE TABLE `t_transactional_message`
(
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
edit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
creator VARCHAR(20) NOT NULL DEFAULT 'admin',
editor VARCHAR(20) NOT NULL DEFAULT 'admin',
deleted TINYINT NOT NULL DEFAULT 0,
current_retry_times TINYINT NOT NULL DEFAULT 0 COMMENT '当前重试次数',
max_retry_times TINYINT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
queue_name VARCHAR(255) NOT NULL COMMENT '队列名',
exchange_name VARCHAR(255) NOT NULL COMMENT '交换器名',
exchange_type VARCHAR(8) NOT NULL COMMENT '交换类型',
routing_key VARCHAR(255) COMMENT '路由键',
business_module VARCHAR(32) NOT NULL COMMENT '业务模块',
business_key VARCHAR(255) NOT NULL COMMENT '业务键',
next_schedule_time DATETIME NOT NULL COMMENT '下一次调度时间',
message_status TINYINT NOT NULL DEFAULT 0 COMMENT '消息状态',
init_backoff BIGINT UNSIGNED NOT NULL DEFAULT 10 COMMENT '退避初始化值,单位为秒',
backoff_factor TINYINT NOT NULL DEFAULT 2 COMMENT '退避因子(也就是指数)',
work_type TINYINT NOT NULL DEFAULT 0 COMMENT '0:由任务调度器自动拉起重推 1:自定义重推',
INDEX idx_queue_name (queue_name),
INDEX idx_create_time (create_time),
INDEX idx_next_schedule_time (next_schedule_time),
INDEX idx_business_key (business_key)
) COMMENT '事务消息表';
CREATE TABLE `t_transactional_message_content`
(
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
message_id BIGINT UNSIGNED NOT NULL COMMENT '事务消息记录ID',
content TEXT COMMENT '消息内容'
) COMMENT '事务消息内容表';
因为此模块有可能扩展出一个后台管理模块,所以要把消息的管理和状态相关字段和大体积的消息内容分别存放在两个表,从而避免大批量查询消息记录的时候MySQL服务IO使用率过高的问题。预留了两个业务字段business_module和business_key用于标识业务模块和业务键(一般是唯一识别号,例如订单号)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGM2nmOV-1592319134549)(rabbit-pic4.PNG)]
代码设计模块
表对象定义
@Data
public class TransactionalMessage {
private Long id;
private LocalDateTime createTime;
private LocalDateTime editTime;
private String creator;
private String editor;
private Integer deleted;
private Integer currentRetryTimes;
private Integer maxRetryTimes;
private String queueName;
private String exchangeName;
private String exchangeType;
private String routingKey;
private String businessModule;
private String businessKey;
private LocalDateTime nextScheduleTime;
private Integer messageStatus;
private Long initBackoff;
private Integer backoffFactor;
}
@Data
public class TransactionalMessageContent {
private Long id;
private Long messageId;
private String content;
}
然后是定义Dao层,这里使用的是MyBatis,方法实在太简单了,笔者在这里就不详细展示了。
public interface TransactionalMessageDao {
void insertSelective(TransactionalMessage record);
void updateStatusSelective(TransactionalMessage record);
List<TransactionalMessage> queryPendingCompensationRecords(LocalDateTime minScheduleTime,
LocalDateTime maxScheduleTime,
int limit);
}
public interface TransactionalMessageContentDao {
void insert(TransactionalMessageContent record);
List<TransactionalMessageContent> queryByMessageIds(String messageIds);
}
代码内容简单易懂,笔者就不写注释了,请各位大佬谅解我随性豪爽的性格。
接着定义事务消息服务接口TransactionalMessageService:
// 对外提供的服务类接口
public interface TransactionalMessageService {
void sendTransactionalMessage(Destination destination, TxMessage message);
}
@Getter
@RequiredArgsConstructor
public enum ExchangeType {
FANOUT("fanout"),
DIRECT

本文介绍了基于RabbitMQ实现的可复用事务消息方案,包括消息发送确认、接收确认、重投、幂等性和持久化。设计中考虑了数据库表结构、代码模块划分,并提出了统一的补偿推送策略,利用Spring的事务管理和指数退避算法处理异常情况。
最低0.47元/天 解锁文章
1741

被折叠的 条评论
为什么被折叠?



