☰
本地消息表(本地事务表)
2026/9/27 3:17:37 网站建设 项目流程

目录

一、核心思想

二、本地消息表字段设计(常用)

三、完整执行流程

阶段 1:业务方本地事务(原子)

阶段 2:定时任务轮询投递

阶段 3:消费端处理

四、优缺点

✅ 优点

❌ 缺点

五、常见坑 & 优化点

六、和其他方案对比

七、适用场景

八、变种:事务状态表 / 可靠消息表


核心用途:实现分布式事务,最终一致性方案,最经典的「可靠消息最终一致性」方案,也叫事务消息的落地思路(不依赖 MQ 事务消息能力也能做)。

一、核心思想

把业务操作和消息记录放在同一个本地数据库事务里。

  1. 在同一个事务中:执行业务 SQL + 插入一条本地消息记录(状态:待发送)
  2. 事务提交成功 → 消息已经落库;事务回滚 → 消息记录也一起回滚,不会产生脏消息
  3. 单独起一个消息投递任务(定时任务),轮询本地消息表,把状态 = 待发送的消息投递到 MQ
  4. MQ 投递成功,更新本地消息状态为已发送;投递失败则重试,直到成功
  5. 消费端消费成功后,也可以回调更新状态;消费失败则消费端自行重试 / 人工兜底

一句话:用本地数据库事务保证【业务执行】和【消息落库】原子性;再通过定时任务保证消息一定能发出去。

二、本地消息表字段设计(常用)

sql

CREATE TABLE local_message ( id BIGINT PRIMARY KEY AUTO_INCREMENT, msg_id VARCHAR(64) NOT NULL COMMENT '消息唯一id,幂等key', topic VARCHAR(64) NOT NULL COMMENT 'MQ主题', msg_content TEXT NOT NULL COMMENT '消息体JSON', status TINYINT NOT NULL COMMENT '消息状态:0待发送,1已发送,2发送失败,3已消费', retry_count INT DEFAULT 0 COMMENT '重试次数', next_retry_time DATETIME COMMENT '下一次重试时间(退避)', create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, UNIQUE KEY uk_msg_id(msg_id) );

状态枚举:

  • 0:待发送(业务事务提交成功,消息入库,还没投 MQ)
  • 1:已发送(成功推送到 MQ)
  • 2:发送失败(投递 MQ 失败,等待定时任务重试)
  • 3:已消费(消费方成功处理)

三、完整执行流程

阶段 1:业务方本地事务(原子)

开启本地事务 1. 更新业务数据(例:订单创建、扣库存) 2. insert 本地消息记录 status=0 提交事务

✅ 要么业务和消息同时入库;要么一起回滚,不会出现业务成功,消息没记录的情况。

阶段 2:定时任务轮询投递

定时任务(比如每 5s)查询:status in (0,2) AND next_retry_time <= now() AND retry_count < max

  1. 拿到消息,先锁行(悲观锁 / SELECT ... FOR UPDATE,防止多实例重复投递)
  2. 发送消息到 MQ
    • ✅ MQ 返回成功 → update status=1,更新 update_time
    • ❌ MQ 异常 / 超时 → retry_count+1,设置退避时间,status=2
  3. 超过最大重试次数 → 标记为死信,人工后台处理

⚠️ 这里会有消息重复投递(网络抖动,MQ 收到消息但是 ACK 丢了,本地任务重试),所以消费端必须做幂等!

阶段 3:消费端处理

  1. 消费者收到消息,先根据msg_id做幂等判断(消费记录表)
  2. 执行业务逻辑
  3. 业务处理成功,ACK;失败不 ACK,MQ 重发
  4. 可选:消费成功后回调生产者接口,更新本地消息表 status=3(已消费)

四、优缺点

✅ 优点

  1. 实现简单,不需要依赖特殊的分布式事务组件(TCC、Seata AT)
  2. 不依赖 MQ 的事务消息(RocketMQ 才有事务消息,Kafka/RabbitMQ 没有,本地消息表通用)
  3. 可靠性高:消息持久化到数据库,宕机重启后定时任务继续重试
  4. 可控:重试次数、退避策略、死信人工处理都可以自己控制

❌ 缺点

  1. 对业务库有侵入:多一张消息表,业务表和消息表在同一个库,增加数据库压力
  2. 定时任务轮询有延迟,不适合强实时场景(最终一致性,不是强一致)
  3. 高并发场景:轮询扫描消息表会有数据库压力,需要加索引、分库分表、分页限制
  4. 重复消息必然存在,消费端必须幂等

五、常见坑 & 优化点

  1. 重复投递问题定时任务投递成功但是更新本地消息表超时,任务再次重试投递。 解决:消费端幂等,唯一 msg_id。

  2. 定时任务多实例重复捞取消息多台服务同时跑定时任务,读到同一条消息,并发投递。 解决:数据库行锁select ... for update;或者分布式锁(Redisson)。

  3. 轮询扫全表性能差不要select * from local_message where status=0,status 建立联合索引(status, next_retry_time),分页查询,限制每次捞取条数。

  4. 重试风暴失败消息不要固定间隔重试,使用指数退避:10s → 30s → 1min → 5min。

  5. 消息表数据膨胀历史已消费消息可以定时归档(迁移到历史表),避免表越来越大。

六、和其他方案对比

  1. 本地消息表 VS MQ 事务消息(RocketMQ)

    • 本地消息表:通用性强,所有 MQ 都能用;业务库多一张表,代码量稍多
    • RocketMQ 事务消息:不用建本地消息表;只能 RocketMQ 使用,理解成本高,半消息机制
  2. 本地消息表 VS TCC

    • 本地消息表:最终一致性,适合异步通知场景(订单创建通知积分、通知库存)
    • TCC:强补偿型,适合短事务、资金类,代码侵入更大
  3. 本地消息表 VS Seata AT

    • Seata AT:全局事务,追求接近强一致;依赖 Seata 组件,有锁、性能损耗

七、适用场景

适合异步通知、最终一致性场景:

  • 订单创建成功后,通知积分服务发放积分
  • 支付成功后通知订单、物流、会员系统 不适合:转账、扣余额这类强一致性业务。

八、变种:事务状态表 / 可靠消息表

还有一种消息状态表方案,和本地消息表思路几乎一样,只是把消息体放到 MQ,本地只存消息状态,减少数据库存储压力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询