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

文章目录

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

一、引言

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

事务性发件箱模式(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兜底重试

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

相关推荐
小灰灰搞电子2 小时前
完全驾驭 Qt 与数据库:C++ ORM 框架 QxOrm 原理与实践指南
数据库·c++·qt
软件聚导航4 小时前
「聚小软 AI 助手」技术升级:从数据库检索到 RAG 知识库问答
前端·数据库·mysql·微信小程序·小程序·ai编程·rag
学习星球4 小时前
单调栈——从“找下一个更大的“到柱状图中的最大矩形
数据库·c++·算法·leetcode·xcode
GoppViper5 小时前
RDF资源描述框架深度解析:语义Web的数据基石与实战逻辑
前端·数据库
2601_962174175 小时前
Redis 简介
数据库·redis·wpf
夜雪一千5 小时前
MySQL 各种数据同步方案全梳理:从简单复制到异构同步实战
数据库·mysql
Highcharts.js6 小时前
Highcharts 下钻(Drilldown)柱状图 Demo
数据库·信息可视化·hbase·highcharts·drilldown·js模块·下钻图表
翼龙云_cloud6 小时前
阿里云国际渠道代理商:如何在ECS上部署Flask应用?
运维·数据库·阿里云·flask·云计算
进阶的小木桩7 小时前
mysql 安装问题解决方案记录
数据库·mysql
2601_962071577 小时前
如何利用SpringSecurity进行认证与授权
java·服务器·数据库