一、方案概述
本地消息表方案 是一种通过 数据库本地事务 保证业务操作与消息发送原子性的分布式事务解决方案。其核心思想是:将消息存储在与业务数据相同的数据库中,利用本地事务的原子性,确保业务操作与消息记录同时成功或失败,再通过 异步重试机制 实现消息的可靠投递,最终达成数据一致性。
二、核心原理
1. 架构角色
• 事务发起方(Producer) :执行本地业务操作并写入消息表。 • 本地消息表:存储待发送消息,与业务数据同库同事务。 • 消息消费者(Consumer) :订阅并处理消息,保证幂等性。 • 消息状态补偿服务:定时扫描消息表,重试失败消息。
2. 工作流程
- 业务操作与消息记录 • 事务发起方在本地事务中执行业务操作(如更新订单状态),并同步插入一条消息到本地消息表。 • 原子性保障:业务操作与消息插入在同一事务中,确保二者同时成功或回滚。
- 消息异步投递 • 事务提交后,通过独立线程或定时任务读取消息表中的 待发送 消息,调用消息队列(MQ)发送。 • 发送成功后更新消息状态为 已发送;失败则记录重试次数,等待下次重试。
- 消息消费与确认 • 消费者从MQ拉取消息,执行业务逻辑(如扣减库存)。 • 消费成功后返回ACK,MQ删除消息;失败则重试投递(需消费者幂等)。
- 消息状态补偿 • 定时任务扫描消息表,处理 发送失败 或 未确认 的消息,重新投递或标记为死亡消息(需人工介入)。
三、关键技术实现
1. 本地消息表设计
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT, -- 消息ID
biz_id VARCHAR(64) NOT NULL, -- 业务唯一ID(如订单号)
topic VARCHAR(128) NOT NULL, -- MQ主题
content TEXT NOT NULL, -- 消息内容(JSON格式)
status TINYINT NOT NULL DEFAULT 0, -- 状态(0-待发送,1-已发送,2-已确认,3-死亡)
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
create_time DATETIME NOT NULL, -- 创建时间
update_time DATETIME NOT NULL -- 更新时间
);
• 索引优化:对 status 和 create_time 建立联合索引,加速补偿任务查询。
2. 消息发送与重试
• 发送线程: 独立线程池轮询消息表中 status=0 的消息,调用MQ发送。
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
public void scanAndSendMessages() {
List<Message> messages = messageDao.selectPendingMessages();
for (Message msg : messages) {
try {
mqProducer.send(msg.getTopic(), msg.getContent());
messageDao.updateStatus(msg.getId(), Status.SENT);
} catch (Exception e) {
messageDao.incrementRetryCount(msg.getId());
if (msg.getRetryCount() >= MAX_RETRY) {
messageDao.markAsDead(msg.getId());
}
}
}
}
3. 消费者幂等性设计
• 唯一业务标识:消息中携带 biz_id,消费前检查是否已处理。
public void handleMessage(Message message) {
String bizId = message.getBizId();
if (duplicateCheckService.isProcessed(bizId)) {
return; // 已处理,直接跳过
}
// 执行业务逻辑
inventoryService.deductStock(message.getProductId(), message.getQuantity());
// 记录处理状态
duplicateCheckService.markAsProcessed(bizId);
}
• 数据库唯一约束:通过唯一索引或乐观锁防重。
4. 死亡消息处理
• 人工干预:提供管理界面查看并手动重试死亡消息。 • 告警机制:当死亡消息数量超过阈值时触发告警。
四、最佳实践示例:订单支付与库存扣减
场景描述
• 用户支付成功后,订单服务需异步通知库存服务扣减库存。 • 要求:支付成功必须扣减库存,允许短暂延迟。
实现步骤
-
订单服务(Producer) • 支付成功后,在本地事务中更新订单状态并插入消息:
BEGIN; UPDATE orders SET status = 'PAID' WHERE id = 'ORDER_001'; INSERT INTO local_message (biz_id, topic, content, status) VALUES ('ORDER_001', 'stock_deduction', '{"productId":"P1001","quantity":1}', 0); COMMIT; -
消息补偿服务 • 定时任务扫描
local_message表中status=0的记录,发送到MQ(如RocketMQ):// 发送逻辑参考前文代码 -
库存服务(Consumer) • 监听MQ主题
stock_deduction,消费消息并扣减库存:@RocketMQMessageListener(topic = "stock_deduction", consumerGroup = "stock_group") public class StockListener implements RocketMQListener<String> { @Override public void onMessage(String message) { OrderEvent event = parseMessage(message); if (redisCache.get(event.getOrderId()) != null) { return; // 幂等检查 } inventoryService.deduct(event.getProductId(), event.getQuantity()); redisCache.set(event.getOrderId(), "PROCESSED", 24, TimeUnit.HOURS); } }
容错处理
• 消息发送失败:补偿任务最多重试3次,超过后标记为死亡消息。 • 消费端宕机:MQ自动重试投递,消费者依赖幂等性避免重复扣减。 • 数据不一致:定时核对订单与库存状态,触发对账补偿。
五、方案优缺点
优点
• 强可靠性:利用本地事务保证消息持久化,无消息丢失风险。 • 业务低侵入:无需改造现有服务接口(对比TCC)。 • 适用于异构系统:消费者只需实现幂等逻辑,无需感知生产者细节。
缺点
• 数据库压力:高频消息场景下,消息表可能成为性能瓶颈。 • 时效性有限:依赖定时任务轮询,消息延迟通常在秒级。 • 需额外组件:需实现消息补偿服务,增加运维复杂度。
六、适用场景
- 异步通知场景:如订单支付后通知库存、物流、营销系统。
- 数据同步场景:如数据库变更同步到缓存或搜索引擎。
- 最终一致性要求:允许短暂延迟,如用户积分发放、日志记录。
七、优化策略
- 消息表分库分表:按业务ID哈希分片,避免单表过大。
- 批量消息发送:补偿任务批量读取消息,减少数据库IO。
- MQ事务消息集成:结合RocketMQ事务消息,替代本地消息表(需MQ支持)。
- CDC(变更数据捕获) :通过数据库日志(如MySQL Binlog)捕获变更事件,替代手动写入消息表。
八、总结
本地消息表方案通过 本地事务原子性 和 异步重试机制 平衡了可靠性与性能,是分布式系统中实现最终一致性的经典模式。关键成功因素: • 消息表设计:合理分片、索引优化。 • 幂等消费:避免重复处理导致数据错误。 • 监控与告警:实时跟踪消息积压与处理延迟。
通过合理架构设计,该方案可广泛应用于电商、金融、物流等领域,成为高可用分布式系统的基石。