消息可靠性保证详解
一、知识概述
消息队列在分布式系统中扮演着重要角色,但消息的可靠性问题一直是开发者的痛点。消息可能丢失、重复、乱序,这些问题在金融、电商等场景下可能导致严重的业务问题。
本文将深入分析消息可靠性问题的根源,并介绍幂等设计、消息去重、消息补偿等解决方案,帮助构建高可靠的消息系统。
二、消息可靠性问题分析
2.1 消息丢失场景
消息流转全链路:
Producer → Network → Broker → Storage → Broker → Network → Consumer
↓ ↓ ↓ ↓ ↓ ↓ ↓
[1] [2] [3] [4] [5] [6] [7]
消息丢失风险点:
[1] Producer 发送失败
- 网络超时
- Broker 不可用
- 序列化异常
[2] 网络传输丢失
- 网络分区
- 连接断开
[3] Broker 接收后未持久化
- Broker 宕机
- 内存消息丢失
[4] 存储失败
- 磁盘满
- IO 异常
[5] Broker 发送失败
- Consumer 不在线
- 网络问题
[6] 网络传输丢失
- 网络抖动
[7] Consumer 处理失败
- 业务异常
- 系统崩溃
2.2 消息重复场景
消息重复原因:
1. 生产者重试
Producer Broker
| |
|--- Send Message --------->|
| |
|<-- ACK (网络超时) | ← ACK 丢失
| |
|--- Retry Send ----------->| ← 重试发送
| |
结果:消息被存储两次
2. 消费者重复消费
Broker Consumer
| |
|--- Push Message --------->|
| |
| Process Message |
| |
|<-- ACK (网络超时) | ← ACK 丢失
| |
|--- Push Again ----------->| ← 重新投递
| |
结果:消息被消费两次
3. Rebalance 期间
- 消费者组 Rebalance
- 未提交的 offset 丢失
- 消息被重新消费
2.3 消息乱序场景
消息顺序问题:
生产顺序:Msg1 → Msg2 → Msg3
场景1:多队列并发
Queue 0: Msg1 → Msg3
Queue 1: Msg2
消费顺序可能是:Msg1 → Msg2 → Msg3 或 Msg2 → Msg1 → Msg3
场景2:消费者并发处理
Consumer Thread 1: Msg1 (处理慢)
Consumer Thread 2: Msg2 (处理快)
实际处理顺序:Msg2 → Msg1
场景3:重试导致乱序
Msg1 消费失败 → 进入重试队列
Msg2 消费成功
Msg3 消费成功
Msg1 重试成功
实际顺序:Msg2 → Msg3 → Msg1
三、幂等设计
3.1 幂等概念
幂等(Idempotent)是指:同一操作执行多次与执行一次的效果相同。
/**
* 幂等性分析
*/
// 幂等操作
int x = 1; // 赋值是幂等的
x = Math.max(x, 2); // max 是幂等的
set.add("a"); // Set 添加是幂等的
// 非幂等操作
x++; // 自增不是幂等的
list.add("a"); // List 添加不是幂等的
account += 100; // 累加不是幂等的
3.2 幂等设计方案
方案一:唯一 ID + 数据库唯一索引
/**
* 基于数据库唯一索引的幂等设计
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单(幂等)
*/
@Transactional
public void createOrder(Order order) {
try {
// 插入订单,orderId 有唯一索引
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 唯一键冲突,说明订单已存在
log.info("订单已存在,跳过: {}", order.getOrderId());
// 幂等返回,不报错
}
}
}
/**
* 订单表设计
*/
CREATE TABLE `t_order` (
`order_id` varchar(64) NOT NULL COMMENT '订单ID(消息Key)',
`user_id` bigint NOT NULL COMMENT '用户ID',
`amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` int NOT NULL COMMENT '订单状态',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`order_id`), -- 主键保证幂等
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
方案二:去重表
/**
* 基于去重表的幂等设计
*/
@Service
public class IdempotentService {
@Autowired
private JdbcTemplate jdbcTemplate;
private static final String INSERT_DEDUP_SQL =
"INSERT INTO t_msg_dedup (msg_id, create_time) VALUES (?, NOW())";
/**
* 检查并标记消息已处理
* @return true=首次处理,false=重复消息
*/
public boolean checkAndMarkProcessed(String msgId) {
try {
// 尝试插入去重表
jdbcTemplate.update(INSERT_DEDUP_SQL, msgId);
return true; // 插入成功,首次处理
} catch (DuplicateKeyException e) {
return false; // 唯一键冲突,重复消息
}
}
}
/**
* 消息去重表设计
*/
CREATE TABLE `t_msg_dedup` (
`msg_id` varchar(64) NOT NULL COMMENT '消息唯一ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/**
* 使用示例
*/
@Service
class OrderMessageConsumer {
@Autowired
private IdempotentService idempotentService;
@Autowired
private OrderService orderService;
public void handleOrderMessage(MessageExt msg) {
String msgId = msg.getKeys(); // 使用业务唯一标识
// 幂等检查
if (!idempotentService.checkAndMarkProcessed(msgId)) {
log.info("重复消息,跳过: {}", msgId);
return;
}
// 处理业务
Order order = parseOrder(msg);
orderService.process(order);
}
private Order parseOrder(MessageExt msg) {
return JSON.parseObject(new String(msg.getBody()), Order.class);
}
}
方案三:Token 机制
/**
* 基于 Token 的幂等设计
*/
@Service
public class TokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 生成 Token
*/
public String createToken(String businessType) {
String token = UUID.randomUUID().toString();
String key = "token:" + businessType + ":" + token;
// Token 有效期 30 分钟
redisTemplate.opsForValue().set(key, "1", 30, TimeUnit.MINUTES);
return token;
}
/**
* 校验并删除 Token(原子操作)
* @return true=Token 有效,false=Token 无效或已使用
*/
public boolean checkAndDeleteToken(String businessType, String token) {
String key = "token:" + businessType + ":" + token;
// Lua 脚本保证原子性
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(key), "1");
return result != null && result == 1;
}
}
/**
* Controller 层使用 Token
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private TokenService tokenService;
@Autowired
private OrderService orderService;
/**
* 获取 Token
*/
@GetMapping("/token")
public String getToken() {
return tokenService.createToken("order");
}
/**
* 创建订单(幂等)
*/
@PostMapping("/create")
public Result createOrder(@RequestHeader("Idempotent-Token") String token,
@RequestBody OrderRequest request) {
// 校验 Token
if (!tokenService.checkAndDeleteToken("order", token)) {
return Result.fail("重复请求或 Token 无效");
}
// 处理业务
orderService.createOrder(request);
return Result.success();
}
}
方案四:状态机幂等
/**
* 基于状态机的幂等设计
*/
@Service
public class OrderStateMachine {
@Autowired
private OrderMapper orderMapper;
/**
* 订单状态
*/
public enum OrderStatus {
CREATED(1, "已创建"),
PAID(2, "已支付"),
DELIVERED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消");
private final int code;
private final String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() { return code; }
}
/**
* 支付成功(幂等)
*/
@Transactional
public boolean paySuccess(String orderId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 状态检查:只有 CREATED 状态才能支付
if (order.getStatus() != OrderStatus.CREATED.getCode()) {
// 幂等:已支付或其他状态,直接返回成功
log.info("订单状态不是已创建,跳过支付: orderId={}, status={}",
orderId, order.getStatus());
return true;
}
// 状态转换:CREATED → PAID
int rows = orderMapper.updateStatus(
orderId,
OrderStatus.CREATED.getCode(), // 期望原状态
OrderStatus.PAID.getCode() // 新状态
);
if (rows == 0) {
// 状态已变更(并发导致),幂等返回
log.info("订单状态已变更,跳过支付: orderId={}", orderId);
return true;
}
// 执行支付后逻辑
afterPaySuccess(order);
return true;
}
private void afterPaySuccess(Order order) {
// 发送发货消息、通知用户等
}
}
/**
* Mapper 层:乐观锁更新
*/
@Mapper
public interface OrderMapper {
/**
* 更新订单状态(乐观锁)
*/
@Update("UPDATE t_order SET status = #{newStatus}, " +
"update_time = NOW() " +
"WHERE order_id = #{orderId} AND status = #{oldStatus}")
int updateStatus(@Param("orderId") String orderId,
@Param("oldStatus") int oldStatus,
@Param("newStatus") int newStatus);
@Select("SELECT * FROM t_order WHERE order_id = #{orderId}")
Order selectById(@Param("orderId") String orderId);
}
3.3 幂等方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 唯一索引 | 简单、可靠 | 依赖数据库 | 有数据库表的业务 |
| 去重表 | 灵活、业务无关 | 需要额外表 | 通用场景 |
| Token | 性能好、无存储 | 需要两次请求 | 前端防重复提交 |
| 状态机 | 业务语义清晰 | 需要明确状态定义 | 状态流转类业务 |
四、消息去重
4.1 去重原理
消息去重核心:保证消息处理的唯一性
关键点:
1. 消息必须有唯一标识(MessageId / Key)
2. 去重容器要支持快速查询
3. 去重记录要有过期策略
4.2 Redis 去重
/**
* 基于 Redis 的消息去重
*/
@Service
@Slf4j
public class RedisMessageDedup {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String DEDUP_KEY_PREFIX = "msg:dedup:";
private static final long EXPIRE_DAYS = 7; // 去重记录保留 7 天
/**
* 检查消息是否已处理
*/
public boolean isDuplicate(String msgId) {
String key = DEDUP_KEY_PREFIX + msgId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 标记消息已处理
*/
public void markProcessed(String msgId) {
String key = DEDUP_KEY_PREFIX + msgId;
redisTemplate.opsForValue().set(key, "1", EXPIRE_DAYS, TimeUnit.DAYS);
}
/**
* 检查并标记(原子操作)
* @return true=首次处理,false=重复消息
*/
public boolean checkAndMark(String msgId) {
String key = DEDUP_KEY_PREFIX + msgId;
// setIfAbsent = SETNX
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, "1", EXPIRE_DAYS, TimeUnit.DAYS);
return Boolean.TRUE.equals(success);
}
/**
* 批量去重
*/
public List<String> filterDuplicate(List<String> msgIds) {
List<String> uniqueMsgIds = new ArrayList<>();
for (String msgId : msgIds) {
if (checkAndMark(msgId)) {
uniqueMsgIds.add(msgId);
}
}
return uniqueMsgIds;
}
}
/**
* 使用示例
*/
@Service
class OrderConsumer {
@Autowired
private RedisMessageDedup dedupService;
@Autowired
private OrderService orderService;
public void handleOrderMessage(MessageExt msg) {
String msgId = msg.getKeys(); // 业务唯一标识
// 去重检查
if (!dedupService.checkAndMark(msgId)) {
log.info("重复消息,跳过: {}", msgId);
return;
}
// 处理业务
try {
Order order = parseOrder(msg);
orderService.process(order);
} catch (Exception e) {
// 处理失败,删除去重标记,允许重试
dedupService.remove(msgId);
throw e;
}
}
private Order parseOrder(MessageExt msg) {
return JSON.parseObject(new String(msg.getBody()), Order.class);
}
}
// RedisMessageDedup 补充方法
class RedisMessageDedup {
public void remove(String msgId) {
String key = DEDUP_KEY_PREFIX + msgId;
redisTemplate.delete(key);
}
}
4.3 Bloom Filter 去重
/**
* 基于 Bloom Filter 的消息去重(海量数据场景)
*/
@Service
@Slf4j
public class BloomFilterDedup {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String BLOOM_KEY = "msg:bloom:filter";
/**
* 添加到 Bloom Filter
*/
public void addToBloomFilter(String msgId) {
// 使用 Redis 的 SETBIT 实现简化版 Bloom Filter
long[] positions = hash(msgId);
for (long position : positions) {
redisTemplate.opsForValue().setBit(BLOOM_KEY, position, true);
}
}
/**
* 检查是否可能存在
* @return true=可能存在,false=一定不存在
*/
public boolean mightContain(String msgId) {
long[] positions = hash(msgId);
for (long position : positions) {
Boolean bit = redisTemplate.opsForValue().getBit(BLOOM_KEY, position);
if (!Boolean.TRUE.equals(bit)) {
return false; // 一定不存在
}
}
return true; // 可能存在
}
/**
* 多重哈希
*/
private long[] hash(String msgId) {
// 简化实现:使用 3 个哈希函数
long[] positions = new long[3];
int hash1 = msgId.hashCode();
int hash2 = msgId.hashCode() * 31;
int hash3 = msgId.hashCode() * 37;
// 映射到 2^32 位空间
positions[0] = Math.abs(hash1) % Integer.MAX_VALUE;
positions[1] = Math.abs(hash2) % Integer.MAX_VALUE;
positions[2] = Math.abs(hash3) % Integer.MAX_VALUE;
return positions;
}
/**
* Bloom Filter + 去重表组合方案
*/
public boolean checkAndMark(String msgId) {
// 第一层:Bloom Filter 快速过滤
if (!mightContain(msgId)) {
// 一定不存在,添加并返回首次处理
addToBloomFilter(msgId);
return true;
}
// 第二层:精确去重表检查
// 可能存在,需要精确查询去重表
// ...
return false;
}
}
/**
* Guava Bloom Filter 示例(本地内存版)
*/
class LocalBloomFilterDedup {
private final BloomFilter<String> bloomFilter;
public LocalBloomFilterDedup() {
// 预计元素数量 100 万,误判率 0.01%
this.bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1_000_000,
0.0001
);
}
public boolean checkAndMark(String msgId) {
if (bloomFilter.mightContain(msgId)) {
return false; // 可能重复
}
bloomFilter.put(msgId);
return true;
}
}
4.4 数据库去重
/**
* 基于数据库的消息去重(可靠性最高)
*/
@Service
public class DatabaseMessageDedup {
@Autowired
private JdbcTemplate jdbcTemplate;
private static final String INSERT_SQL =
"INSERT INTO t_message_log " +
"(msg_id, topic, tag, key, status, retry_count, create_time) " +
"VALUES (?, ?, ?, ?, 'PROCESSING', 0, NOW()) " +
"ON DUPLICATE KEY UPDATE retry_count = retry_count";
private static final String UPDATE_SUCCESS_SQL =
"UPDATE t_message_log SET status = 'SUCCESS', update_time = NOW() " +
"WHERE msg_id = ?";
/**
* 开始处理消息
* @return true=可以处理,false=重复消息
*/
public boolean startProcess(String msgId, String topic, String tag, String key) {
try {
int rows = jdbcTemplate.update(INSERT_SQL, msgId, topic, tag, key);
return rows > 0;
} catch (Exception e) {
log.warn("消息已处理过: {}", msgId);
return false;
}
}
/**
* 标记处理成功
*/
public void markSuccess(String msgId) {
jdbcTemplate.update(UPDATE_SUCCESS_SQL, msgId);
}
/**
* 标记处理失败
*/
public void markFailed(String msgId, String error) {
jdbcTemplate.update(
"UPDATE t_message_log SET status = 'FAILED', error = ?, " +
"retry_count = retry_count + 1, update_time = NOW() WHERE msg_id = ?",
error, msgId
);
}
}
/**
* 消息处理日志表
*/
CREATE TABLE `t_message_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`msg_id` varchar(64) NOT NULL COMMENT '消息ID',
`topic` varchar(128) NOT NULL COMMENT 'Topic',
`tag` varchar(128) DEFAULT NULL COMMENT 'Tag',
`key` varchar(128) DEFAULT NULL COMMENT '业务Key',
`status` varchar(32) NOT NULL COMMENT '状态:PROCESSING/SUCCESS/FAILED',
`retry_count` int NOT NULL DEFAULT '0' COMMENT '重试次数',
`error` text COMMENT '错误信息',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_msg_id` (`msg_id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
五、消息补偿机制
5.1 补偿原理
消息补偿场景:
1. 消息发送失败
Producer → [失败] → Broker
解决方案:
- 本地消息表记录发送状态
- 定时任务扫描重发
2. 消息消费失败
Broker → [失败] → Consumer
解决方案:
- 重试队列
- 死信队列
- 人工处理
3. 消息状态不确定
Producer → [超时] → Broker (可能成功,可能失败)
解决方案:
- 事务消息
- 消息状态查询
5.2 本地消息表方案
/**
* 本地消息表方案
*
* 核心思想:将业务操作和消息发送放在同一事务中
*/
@Service
@Slf4j
public class LocalMessageService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageRecordMapper messageRecordMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 创建订单并发送消息(事务性)
*/
@Transactional
public void createOrderWithMessage(Order order) {
// 1. 插入订单
orderMapper.insert(order);
// 2. 插入消息记录(同一事务)
MessageRecord record = new MessageRecord();
record.setMessageId(UUID.randomUUID().toString());
record.setTopic("ORDER_TOPIC");
record.setTag("TagA");
record.setMessageBody(JSON.toJSONString(order));
record.setStatus("PENDING"); // 待发送
record.setCreateTime(new Date());
messageRecordMapper.insert(record);
// 事务提交后,定时任务会扫描并发送消息
}
/**
* 定时任务:扫描待发送消息
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行
public void sendPendingMessages() {
// 查询待发送消息
List<MessageRecord> records = messageRecordMapper
.selectByStatus("PENDING", 100);
for (MessageRecord record : records) {
try {
// 发送消息
SendResult result = rocketMQTemplate.syncSend(
record.getTopic() + ":" + record.getTag(),
record.getMessageBody()
);
// 更新状态为已发送
messageRecordMapper.updateStatus(
record.getMessageId(), "SENT", null);
log.info("消息发送成功: {}", record.getMessageId());
} catch (Exception e) {
log.error("消息发送失败: {}", record.getMessageId(), e);
// 更新重试次数
int retryCount = record.getRetryCount() + 1;
if (retryCount >= 5) {
// 超过重试次数,标记为失败
messageRecordMapper.updateStatus(
record.getMessageId(), "FAILED", e.getMessage());
} else {
messageRecordMapper.updateRetryCount(
record.getMessageId(), retryCount);
}
}
}
}
}
/**
* 消息记录表
*/
@Data
class MessageRecord {
private Long id;
private String messageId;
private String topic;
private String tag;
private String messageBody;
private String status; // PENDING/SENT/FAILED
private Integer retryCount;
private String errorMessage;
private Date createTime;
private Date updateTime;
}
/**
* Mapper
*/
@Mapper
interface MessageRecordMapper {
@Insert("INSERT INTO t_message_record " +
"(message_id, topic, tag, message_body, status, retry_count, create_time) " +
"VALUES (#{messageId}, #{topic}, #{tag}, #{messageBody}, #{status}, 0, NOW())")
void insert(MessageRecord record);
@Select("SELECT * FROM t_message_record WHERE status = #{status} " +
"ORDER BY create_time LIMIT #{limit}")
List<MessageRecord> selectByStatus(@Param("status") String status,
@Param("limit") int limit);
@Update("UPDATE t_message_record SET status = #{status}, " +
"error_message = #{error}, update_time = NOW() " +
"WHERE message_id = #{messageId}")
void updateStatus(@Param("messageId") String messageId,
@Param("status") String status,
@Param("error") String error);
@Update("UPDATE t_message_record SET retry_count = #{retryCount}, " +
"update_time = NOW() WHERE message_id = #{messageId}")
void updateRetryCount(@Param("messageId") String messageId,
@Param("retryCount") int retryCount);
}
CREATE TABLE `t_message_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`message_id` varchar(64) NOT NULL,
`topic` varchar(128) NOT NULL,
`tag` varchar(64) DEFAULT NULL,
`message_body` text NOT NULL,
`status` varchar(32) NOT NULL COMMENT 'PENDING/SENT/FAILED',
`retry_count` int NOT NULL DEFAULT '0',
`error_message` text,
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_message_id` (`message_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
5.3 死信队列处理
/**
* 死信队列处理
*/
@Service
@Slf4j
public class DeadLetterQueueHandler {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private MessageFailLogMapper failLogMapper;
/**
* 消费死信队列消息
*/
@RocketMQMessageListener(
topic = "%DLQ%ORDER_CONSUMER_GROUP", // 死信队列 Topic
consumerGroup = "DLQ_HANDLER_GROUP"
)
public class DlqListener implements RocketMQListener<MessageExt> {
@Override
public void onMessage(MessageExt message) {
log.warn("收到死信消息: {}", new String(message.getBody()));
// 记录失败日志
MessageFailLog failLog = new MessageFailLog();
failLog.setMessageId(message.getKeys());
failLog.setTopic(message.getTopic());
failLog.setTag(message.getTags());
failLog.setMessageBody(new String(message.getBody()));
failLog.setReason("Dead letter message");
failLog.setStatus("PENDING");
failLog.setCreateTime(new Date());
failLogMapper.insert(failLog);
// 可以选择:
// 1. 人工处理
// 2. 自动重试
// 3. 发送告警
sendAlert(failLog);
}
}
/**
* 重新处理死信消息
*/
public void retryDeadLetterMessage(Long failLogId) {
MessageFailLog failLog = failLogMapper.selectById(failLogId);
if (failLog == null) {
throw new BusinessException("记录不存在");
}
try {
// 重新发送到原 Topic
rocketMQTemplate.syncSend(
failLog.getTopic() + ":" + failLog.getTag(),
failLog.getMessageBody()
);
// 更新状态
failLogMapper.updateStatus(failLogId, "RETRIED");
log.info("死信消息重新发送成功: {}", failLogId);
} catch (Exception e) {
log.error("死信消息重新发送失败: {}", failLogId, e);
throw new BusinessException("重试失败");
}
}
private void sendAlert(MessageFailLog failLog) {
// 发送告警(邮件、短信、钉钉等)
log.error("死信告警: {}", failLog);
}
}
/**
* 消息失败日志
*/
@Data
class MessageFailLog {
private Long id;
private String messageId;
private String topic;
private String tag;
private String messageBody;
private String reason;
private String status; // PENDING/RETRIED/RESOLVED
private Date createTime;
}
5.4 消息对账机制
/**
* 消息对账机制
*/
@Service
@Slf4j
public class MessageReconciliation {
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageRecordMapper messageRecordMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 定时对账:检查订单状态与消息发送状态
*/
@Scheduled(cron = "0 0 * * * ?") // 每小时执行
public void reconcile() {
log.info("开始消息对账...");
// 1. 查找已支付但未发送消息的订单
List<Order> unpaidOrders = orderMapper.selectPaidButNoMessage();
for (Order order : unpaidOrders) {
// 补发消息
compensateMessage(order);
}
// 2. 查找消息已发送但业务未处理的记录
List<MessageRecord> sentRecords = messageRecordMapper
.selectByStatus("SENT", 1000);
for (MessageRecord record : sentRecords) {
// 检查业务是否已处理
if (!isBusinessProcessed(record)) {
// 重新发送或标记异常
handleUnprocessedMessage(record);
}
}
log.info("消息对账完成");
}
private void compensateMessage(Order order) {
log.info("补发消息: orderId={}", order.getOrderId());
// 发送消息
rocketMQTemplate.syncSend("ORDER_TOPIC:TagA",
JSON.toJSONString(order));
// 更新订单消息状态
orderMapper.updateMessageSent(order.getOrderId());
}
private boolean isBusinessProcessed(MessageRecord record) {
// 检查业务是否已处理
// 例如:检查库存是否已扣减
return true;
}
private void handleUnprocessedMessage(MessageRecord record) {
// 处理未处理的消息
// 可以重新发送或标记异常
log.warn("消息未处理: {}", record.getMessageId());
}
}
六、完整示例
6.1 可靠消息消费者
/**
* 可靠消息消费者完整示例
*/
@Service
@Slf4j
@RocketMQMessageListener(
topic = "ORDER_TOPIC",
consumerGroup = "ORDER_CONSUMER_GROUP",
consumeMode = ConsumeMode.CONCURRENTLY
)
public class ReliableOrderConsumer implements RocketMQListener<MessageExt> {
@Autowired
private RedisMessageDedup dedupService;
@Autowired
private OrderService orderService;
@Autowired
private MessageFailLogMapper failLogMapper;
@Override
public void onMessage(MessageExt message) {
String msgId = message.getKeys();
String msgBody = new String(message.getBody());
log.info("收到消息: msgId={}, topic={}, tag={}",
msgId, message.getTopic(), message.getTags());
// 1. 幂等检查
if (!dedupService.checkAndMark(msgId)) {
log.info("重复消息,跳过: {}", msgId);
return;
}
try {
// 2. 解析消息
Order order = JSON.parseObject(msgBody, Order.class);
// 3. 业务处理(带状态机幂等)
orderService.processOrder(order);
// 4. 处理成功(去重记录已在 Redis 中)
log.info("消息处理成功: {}", msgId);
} catch (RetryableException e) {
// 可重试异常:删除去重标记,触发重试
log.warn("消息处理失败,等待重试: msgId={}, error={}", msgId, e.getMessage());
dedupService.remove(msgId);
throw e;
} catch (NonRetryableException e) {
// 不可重试异常:记录失败日志,不重试
log.error("消息处理失败(不重试): msgId={}", msgId, e);
// 记录失败日志
MessageFailLog failLog = new MessageFailLog();
failLog.setMessageId(msgId);
failLog.setTopic(message.getTopic());
failLog.setTag(message.getTags());
failLog.setMessageBody(msgBody);
failLog.setReason(e.getMessage());
failLog.setStatus("FAILED");
failLog.setCreateTime(new Date());
failLogMapper.insert(failLog);
// 不抛异常,确认消息消费成功
}
}
}
/**
* 订单服务
*/
@Service
class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 处理订单(幂等)
*/
@Transactional
public void processOrder(Order order) {
// 1. 查询订单
Order existingOrder = orderMapper.selectById(order.getOrderId());
// 2. 状态检查(幂等)
if (existingOrder != null && existingOrder.getStatus() == 1) {
// 订单已处理
log.info("订单已处理,跳过: {}", order.getOrderId());
return;
}
// 3. 创建订单
if (existingOrder == null) {
try {
orderMapper.insert(order);
} catch (DuplicateKeyException e) {
// 并发插入,幂等返回
log.info("订单已存在,跳过: {}", order.getOrderId());
return;
}
}
// 4. 执行业务逻辑(扣库存、通知等)
// ...
}
}
七、总结
消息可靠性保证策略
| 问题 | 解决方案 |
|---|---|
| 消息丢失 | 生产者 ACK、Broker 持久化、消费者确认 |
| 消息重复 | 幂等设计、去重机制 |
| 消息乱序 | 顺序消息、单队列消费 |
| 消息积压 | 增加消费者、批量处理 |
| 消息失败 | 重试机制、死信队列、补偿机制 |
最佳实践
- 生产者:使用事务消息或本地消息表保证发送可靠性
- 消费者:实现幂等处理,区分可重试和不可重试异常
- 监控:监控消息积压、消费延迟、失败率
- 补偿:定期对账,发现并修复不一致
六、思考与练习
思考题
-
基础题:消息幂等与消息去重有什么区别?为什么说幂等是业务层面的概念,而去重是技术层面的概念?
-
进阶题:分析四种幂等方案(唯一索引、去重表、Token、状态机)的优缺点,在什么场景下应该选择哪种方案?
-
实战题:设计一个完整的消息可靠性保障体系,涵盖生产者发送、Broker存储、消费者处理三个环节,并考虑监控告警与故障恢复机制。
编程练习
练习:实现一个完整的可靠消息消费者框架,包含:(1) Redis去重服务;(2) 数据库去重表作为二级保障;(3) 幂等业务处理(状态机);(4) 死信队列处理;(5) 消息处理日志记录与对账功能。
章节关联
- 前置章节:RocketMQ核心原理与实战(事务消息)
- 后续章节:无(消息队列系列完结)
- 扩展阅读:《数据密集型应用系统设计》第七章、分布式事务模式
📝 系列结语
本系列文章涵盖了分布式系统的核心理论与实践:从CAP/BASE理论基础,到分布式ID、锁、事务的实现方案,再到三大消息队列(RabbitMQ、Kafka、RocketMQ)的深度解析,最后以消息可靠性保障收尾。希望读者能够将这些知识融会贯通,在实际项目中设计出高可用、高可靠的分布式系统。
本章完
参考资料:
- RocketMQ 官方文档:rocketmq.apache.org/docs/
- Kafka 官方文档:kafka.apache.org/documentati…
- 分布式事务解决方案:本地消息表、事务消息、TCC