文章目录
事务性发件箱模式设计与实现
一、引言
在分布式系统中,如何保证消息可靠投递是一个经典难题。传统的做法是:先操作数据库,再发送消息。但如果发送消息失败,就会导致数据不一致。
事务性发件箱模式(Outbox Pattern)通过将消息写入与业务数据相同的数据库事务中,确保了消息投递的可靠性。
二、核心思路
事务性发件箱模式的核心设计:
- 业务数据与消息写入同一事务:在业务事务中同时写入业务表和发件箱表
- 事务提交后异步发送:通过Spring的事务事件监听机制,在事务提交后异步发送消息
- 定时任务兜底重试:补偿Job定期扫描未发送消息,进行重试
三、表结构设计
sql
CREATE TABLE `outbox_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`idempotent_key` VARCHAR(64) NOT NULL COMMENT '幂等键,调用者生成',
`topic` VARCHAR(128) NOT NULL COMMENT 'MQ Topic',
`routing_key` VARCHAR(128) DEFAULT NULL COMMENT '路由键,用于消息路由',
`message_content` TEXT NOT NULL COMMENT '消息内容',
`status` VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '状态 PENDING-未发送 SENT-已发送 FAILED-发送失败',
`mq_type` VARCHAR(32) DEFAULT NULL COMMENT 'MQ类型',
`mq_properties` VARCHAR(512) DEFAULT NULL COMMENT 'MQ属性',
`error_msg` TEXT DEFAULT NULL COMMENT '错误信息',
`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
`send_time` DATETIME DEFAULT NULL COMMENT '发送时间',
`delay_time` BIGINT DEFAULT NULL COMMENT '延迟时间(毫秒)',
`listener_retry_count` INT DEFAULT 0 COMMENT 'Listener重试次数',
`job_retry_count` INT DEFAULT 0 COMMENT 'Job重试次数',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_idempotent_key` (`idempotent_key`),
KEY `idx_status_create_time` (`status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='发件箱记录表';
字段说明:
| 字段 | 说明 |
|---|---|
idempotent_key |
幂等键,由调用者生成,确保消息不重复 |
topic |
MQ Topic,各MQ通用概念 |
routing_key |
路由键,用于消息路由(兼容RabbitMQ/Kafka) |
message_content |
消息内容,发件箱存储的消息数据 |
status |
消息状态:PENDING-未发送、SENT-已发送、FAILED-发送失败 |
listener_retry_count |
Listener即时重试次数 |
job_retry_count |
Job补偿重试次数 |
四、消息幂等性设计
- 幂等键由调用者生成:调用方负责生成唯一标识
- 数据库唯一约束:通过唯一键防止重复写入
- 框架校验:框架层校验幂等键不能为空
java
public void sendOrderMessage(OrderEvent event) {
OutboxRecord record = OutboxRecord.newBuilder()
.idempotentKey(UUID.randomUUID().toString())
.topic("order-topic")
.routingKey("order_created")
.messageContent(JSONObject.toJSONString(event))
.build();
outboxRecordService.addOutboxRecord(record);
}
五、多MQ中间件支持设计
采用适配器模式,灵活支持多种MQ:
java
public interface MQAdapter {
String getMqType();
void send(OutboxRecord record);
void sendBatch(List<OutboxRecord> records);
void close();
boolean isAvailable();
}
| 适配器 | 映射关系 |
|---|---|
| RocketMQAdapter | Topic + Tag |
| RabbitMQAdapter | Exchange + RoutingKey |
| KafkaAdapter | Topic |
六、核心组件
1. OutboxRecordService
消息写入服务,负责将消息写入发件箱表并发布事件。
java
@Transactional
public Boolean addOutboxRecord(OutboxRecord record) {
record.setStatus(PENDING);
record.setCreateTime(new Date());
this.save(record); // 写入发件箱表
eventPublisher.publishEvent(record); // 发布事务事件
return true;
}
@Resource
private ApplicationEventPublisher eventPublisher;
2. OutboxSendEventListener
事务提交后事件监听器,触发消息发送。
java
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleSendMessageEvent(OutboxRecord record) {
sendWithRetry(record);
}
3. OutboxCompensateJob
XXL-Job定时补偿任务,扫描未发送消息进行重试。
java
@XxlJob("outboxCompensateJob")
public ReturnT<String> compensate() {
List<OutboxRecord> pending = getPendingMessages();
for (OutboxRecord record : pending) {
if (sendMessage(record)) {
markAsSent(record);
} else {
incrementJobRetryCount(record);
}
}
}
七、重试机制设计
本框架采用分离重试设计,Listener和Job各自独立重试:
重试流程
业务事务提交 → OutboxSendEventListener → 尝试发送(最多2次)
↓
失败 → 记录 listenerRetryCount → 保持PENDING
↓
定时任务(OutboxCompensateJob) → 轮询PENDING消息 → 尝试发送(最多10次)
↓
失败 → 记录 jobRetryCount → 达到上限 → 标记FAILED
重试次数配置
| 组件 | 适用场景 | 重试次数 |
|---|---|---|
| Listener | 事务提交后即时发送,处理网络瞬时抖动 | 2次 |
| Job | 系统稳定后的兜底机制,处理持久性失败 | 10次 |
八、总结
注意:本文仅提供事务性发件箱模式的关键设计思路,具体实现可能存在细节优化空间,请根据实际业务场景调整。
事务性发件箱模式通过以下设计确保消息可靠投递:
- 事务一致性:消息与业务数据在同一事务
- 消息幂等:唯一键约束 + 调用者生成幂等键
- 多MQ支持:适配器模式支持灵活扩展
- 分离重试:Listener即时重试 + Job兜底重试
该方案适用于最终一致保证的分布式事务场景并可结合事件驱动思想。