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

文章目录

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

一、引言

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

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

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

相关推荐
j7~8 小时前
【MYSQL】在Centos7和ubuntu22.04环境下安装
数据库·c++·mysql·ubuntu·centos
环流_8 小时前
redis:持久化rdb
java·数据库·redis
Donk_678 小时前
MariaDB 数据库管理手册
数据库·mariadb
阿洛学长8 小时前
PostgreSQL 超详细安装与使用教程:从入门到实战
数据库·postgresql
许彰午8 小时前
Oracle Redo日志与Undo回滚段损坏恢复实战
数据库·oracle
qxwlcsdn8 小时前
mysql在事务中执行DDL的后果_MySQL 8.0之前的限制
jvm·数据库·python
難釋懷9 小时前
Redis通信协议-基于Socket自定义Redis的客户端
数据库·redis·缓存
环流_9 小时前
redis:AOF
数据库·redis·spring
2401_884454159 小时前
如何防止SQL触发器导致性能下降_通过精简触发器逻辑
jvm·数据库·python
m0_596749099 小时前
Golang如何做Clean Architecture_Golang整洁架构教程【详解】
jvm·数据库·python