题目:如何基于本地消息表实现分布式事务?
难度:medium
评分:⭐⭐⭐☆☆
所谓本地消息表,其实就是定义一个相当于工作链的东西,根据不同的状态去做不同处理。每个状态创建一个对应的本地消息,或者是数据库的记录。然后这时候根据这个消息的状态、消息的记录,在不同的服务去查到这个记录之后,执行自己的事。可能是转到 MQ,或者是怎么样。然后做,根据最后的事务状态做消息状态去,如果成功了就无所谓了,整个事务正常。如果失败了,可能就走定时任务去做处理,或者是失败了发送 MQ 消息,然后对应的有监听的服务去对应的做补偿机制或者是处理。
评分:⭐⭐⭐☆☆
优点:
不足:
补充:
INIT (本地事务已提交,消息待发送)
↓ 定时扫表 + MQ 投递
SENT (消息已发送,等待消费)
↓ 消费端 ACK
DELIVERED (消费端已确认)
↓ 业务处理完成
ACKED (业务已完成)
msg_id(唯一键)、status、retry_count、next_retry_timeFOR UPDATE SKIP LOCKED 避免多实例并发扫到同一条msg_id 去重)核心考点定位:考察对本地消息表"用本地事务换分布式事务"的精确理解——核心是"业务 SQL + 写消息表 同一事务"。
知识点拆解:
INIT (本地事务已提交,消息待发送)
↓ 定时扫表 + MQ 投递
SENT (消息已发送,等待消费)
↓ 消费端 ACK
DELIVERED (消费端已确认)
↓ 业务处理完成
ACKED (业务已完成)
每个状态转换都是幂等的(重复执行结果一致)
| 维度 | 本地消息表 | RocketMQ 事务消息 |
|---|---|---|
| 扫表逻辑 | 业务方自建 | MQ 内置回查 |
| 业务侵入 | 中 | 低 |
| 可靠性 | 依赖业务方 | 依赖 MQ |
| 部署复杂度 | 低 | 中 |
msg_id(唯一键)、status、retry_count、next_retry_time、create_timeSELECT * FROM msg_table WHERE status = 'INIT' AND next_retry_time < NOW() LIMIT 100 FOR UPDATE SKIP LOCKEDmsg_id 建唯一键,重复消息直接返回成功考察方向:
延伸递进面试题:
FOR UPDATE SKIP LOCKED 锁住消息行,其他实例跳过;或加分布式锁面试官您好,本地消息表的核心思想是用"本地事务 + 异步消息"换"分布式事务",让业务方在本地事务里就把消息写进去,保证"业务成功 = 消息存在"。 完整流程分三步: 第一步是本地事务阶段。业务代码在一个本地事务里同时做两件事:执行业务 SQL(比如INSERT order)和写消息表(INSERT message_table)。这里的关键是两条 SQL 在同一个本地事务里——要么都成功,要么都失败。这样保证了"业务成功 = 消息存在",不会出现"业务成功了但消息没写"的丢单问题。消息表里要存:消息 ID(唯一键)、业务 ID、消息内容、状态(INIT/SENT/DELIVERED/ACKED)、重试次数、下次重试时间。 第二步是消息投递阶段。有一个定时任务在后台扫表,扫status = 'INIT' AND next_retry_time < NOW()的消息,通过FOR UPDATE SKIP LOCKED加行锁避免多实例重复扫到,然后把这些消息投递到 MQ,更新消息状态为SENT。如果投递失败,更新重试次数和下次重试时间,等下一轮再扫。 第三步是消费确认阶段。消费端消费 MQ 消息,必须做幂等处理——基于消息 ID 建唯一键,重复消息直接返回成功。业务处理成功后,回写消息表状态为DELIVERED或ACKED。如果消费失败,进入消费端重试;超过最大重试次数转入死信队列,人工处理。 这里有个关键点:本地消息表的可靠性靠三件事保证——本地事务保证"不丢"、消息表主键 + 消费端幂等保证"不重复"、定时扫表 + 消费端重试保证"不丢单"。 和 RocketMQ 事务消息相比,本地消息表的扫表逻辑是业务方自己建的,而 RocketMQ 内置了 half message + 回查机制,业务方代码更简洁。选型看业务方运维能力:想自建可控 → 本地消息表;想用成熟 MQ → RocketMQ 事务消息。 工程落地时还有几个细节要注意:扫表任务要集群化(多实例),监控要覆盖消息积压、状态异常;死信队列要有人工处理流程;消息表要定期归档清理,否则数据会无限增长。 总结一句话:本地消息表的灵魂是"本地事务 + 异步消息",业务方在本地事务里把消息写进去,靠定时扫表和消费端幂等来保证最终一致。这是柔性事务工程化的经典模式,特别适合"上下游解耦、可容忍秒级延迟"的场景。