Spring Boot 4.0 事务消息与 Outbox:消息最终一致性
前言
订单事务已经提交,调用 RocketMQ 时网络超时。直接回滚数据库不行,继续无限重试也会影响接口响应。这就是典型的本地事务与外部消息一致性问题。
Outbox 把“待发送消息”写入业务数据库的同一事务,之后由后台任务重试投递。
配套源码:dvsusan/spring-boot4-demo。
最终结果

图 1:业务数据和 PENDING 消息同事务提交,投递成功后再标记 PUBLISHED。
第 1 步:定义 Outbox 表
// 与业务订单共用数据库,才能获得本地事务原子性。
@Entity
@Table(name = "outbox_message")
public class OutboxMessageEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 64)
private String eventId;
@Enumerated(EnumType.STRING)
private OutboxStatus status;
private Instant createdAt;
private Instant publishedAt;
}eventId 唯一约束防止同一事件重复写入。状态至少包含 PENDING 和 PUBLISHED,大型系统还可增加失败次数和下次重试时间。
第 2 步:在事务中写入
// 正式项目必须让订单写入和 Outbox 写入处于同一事务。
@Transactional
public OutboxMessageEntity append(
String eventId,
String aggregateId,
String eventType,
String payload
) {
return repository.save(new OutboxMessageEntity(
eventId,
aggregateId,
eventType,
payload
));
}正式业务需要把订单保存和 Outbox 写入放在同一个本地事务。当前章节先提供独立的 Outbox 基础设施,企业项目阶段会把两步合并到订单事务服务。
第 3 步:运行并写入消息
默认 Profile 使用 H2,不需要 Docker:
mvn clean package -DskipTests
java -jar demo-web/target/demo-web-1.0.0-SNAPSHOT.jar \
--server.port=18095curl -X POST "http://localhost:18095/api/v1/chapter4/outbox" \
--data-urlencode "eventId=evt-1001" \
--data-urlencode "aggregateId=SO1001" \
--data-urlencode "payload={\"orderNo\":\"SO1001\"}"查询待投递消息:
curl "http://localhost:18095/api/v1/chapter4/outbox"预期能看到状态 PENDING 的 evt-1001。
第 4 步:投递后更新状态
// 只有在 MQ 明确返回成功后,才更新 Outbox 状态。
message.markPublished();
repository.save(message);发送消息与修改 Outbox 状态不在同一个数据库事务里。可能出现“MQ 已收到,但状态没更新”,因此消费者仍必须按 eventId 幂等。
清理与重试
- 只扫描
PENDING,按创建时间升序限制批次。 - 连续失败增加退避时间和最大重试次数。
- 已完成消息保留一段时间后归档或删除。
- 多实例扫描使用
SELECT ... FOR UPDATE SKIP LOCKED或租约字段。 - 监控最老 PENDING 消息年龄,而不只看数量。
常见问题排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 订单成功但没有事件 | 两步不在同一事务 | 合并订单和 Outbox 写入 |
| 重复投递 | 发送后状态更新失败 | 消费者按 eventId 幂等 |
| 消息长期 PENDING | 扫描任务未启动或 MQ 不健康 | 检查调度、重试和告警 |
| 多实例重复处理 | 没有抢占或租约 | SKIP LOCKED 或状态 CAS |
| 表无限增长 | 没有归档策略 | 已发布消息定期清理 |
练习题
增加 retryCount 和 nextRetryAt,实现指数退避。
总结
Outbox 把不可靠的跨系统发送变成可重试的本地记录。它保证消息“最终会尝试发送”,消费者幂等才能保证最终结果正确。
下一篇用 Quartz 周期扫描并投递这些消息。
上一节:RocketMQ 消息治理
下一节:Quartz 定时任务
13 个企业级实战项目 · 源码 + 教程 + 答疑
简历包装指导AI Agent 实战高并发·微服务Offer 导向持续更新3000+ 同行交流1V1 答疑3 天包退
覆盖高并发、微服务、分布式与 AI Agent:100万QPS短链、企业智能知识库、商城微服务、SaaS点餐、秒杀、刷题小程序、代码生成器、AI Agent 等。券后 ¥129(原价 ¥159),不满意 3 天包退。
