事务性发件箱模式设计与实现

文章目录

事务性发件箱模式设计与实现

一、引言

在分布式系统中,如何保证消息可靠投递是一个经典难题。传统的做法是:先操作数据库,再发送消息。但如果发送消息失败,就会导致数据不一致。

事务性发件箱模式(Outbox Pattern)通过将消息写入与业务数据相同的数据库事务中,确保了消息投递的可靠性。

二、核心思路

事务性发件箱模式的核心设计:

  1. 业务数据与消息写入同一事务:在业务事务中同时写入业务表和发件箱表
  2. 事务提交后异步发送:通过Spring的事务事件监听机制,在事务提交后异步发送消息
  3. 定时任务兜底重试:补偿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补偿重试次数

四、消息幂等性设计

  1. 幂等键由调用者生成:调用方负责生成唯一标识
  2. 数据库唯一约束:通过唯一键防止重复写入
  3. 框架校验:框架层校验幂等键不能为空
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次

八、总结

注意:本文仅提供事务性发件箱模式的关键设计思路,具体实现可能存在细节优化空间,请根据实际业务场景调整。

事务性发件箱模式通过以下设计确保消息可靠投递:

  1. 事务一致性:消息与业务数据在同一事务
  2. 消息幂等:唯一键约束 + 调用者生成幂等键
  3. 多MQ支持:适配器模式支持灵活扩展
  4. 分离重试:Listener即时重试 + Job兜底重试

该方案适用于最终一致保证的分布式事务场景并可结合事件驱动思想。

相关推荐
zxrhhm21 小时前
MySQL 8.4 LTS 数据库巡检脚本
数据库·mysql
AI木马人21 小时前
9.【AI任务队列实战】如何在高并发下保证系统不崩?(Redis + Celery完整方案)
数据库·人工智能·redis·神经网络·缓存
2401_883600251 天前
golang如何理解weak pointer弱引用_golang weak pointer弱引用总结
jvm·数据库·python
aLTttY1 天前
【Redis实战】分布式锁的N种实现方案对比与避坑指南
数据库·redis·分布式
2301_773553621 天前
mysql如何评估SQL语句的索引开销_mysql性能追踪与分析
jvm·数据库·python
pele1 天前
PHP源码运行受主板供电影响吗_供电相数重要性说明【技巧】
jvm·数据库·python
sinat_383437361 天前
CSS如何实现元素悬浮在页面底部_利用fixed定位与底部间距
jvm·数据库·python
gmaajt1 天前
mysql如何备份与恢复函数定义_mysql mysqldump导出存储对象
jvm·数据库·python
阿丰资源1 天前
基于SpringBoot的在线视频教育平台的设计与实现(附源码+数据库+文档,一键运行)
数据库·spring boot·后端
qq_460978401 天前
Python爬虫怎么模拟手机端抓取_设置手机型号User-Agent字符串
jvm·数据库·python