知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方请评论,我们一起交流!
接口幂等性和消息幂等性是分布式系统中保证操作一致性的重要设计原则,常见解决方案如下:
一、接口幂等性解决方案
1. 唯一请求 ID(Token 机制)
- 核心思想:客户端每次请求时生成一个唯一 ID(如 UUID、雪花 ID),作为请求的唯一标识,服务端通过缓存或数据库记录该 ID 的处理状态,重复请求时直接返回结果。
- 实现步骤:
- 客户端生成唯一请求 ID(如放在请求头或参数中)。
- 服务端接收请求后,先检查该 ID 是否已处理(如 Redis 中查询exists request_id:xxx)。
- 若未处理,则执行逻辑并将 ID 存入缓存(设置过期时间,避免内存泄漏);若已处理,直接返回上次结果。
- 适用场景:表单提交、重试机制(如 Feign/RestTemplate 重试)。
- 示例代码(Redis 去重):
String requestId = request.getHeader("X-Request-ID");
if (redis.exists("idempotent:lock:" + requestId)) {
return "重复请求,已处理"; // 直接返回成功
}
redis.setex("idempotent:lock:" + requestId, 60, "processed"); // 处理后标记为已处理
// 执行业务逻辑...
2. 数据库唯一约束
- 核心思想:利用数据库唯一索引(如订单号、业务 ID),保证相同业务数据仅能插入 / 更新一次。
- 实现方式:
- 插入场景:通过唯一键(如order_no),重复插入时抛DuplicateKeyException,捕获后返回已有结果。
- 更新场景:通过WHERE condition AND version = xxx(乐观锁),确保更新操作幂等。
- 示例:
-- 唯一索引防止重复插入
CREATE UNIQUE INDEX idx_order_no ON orders(order_no);
-- 乐观锁更新(版本号控制)
UPDATE orders SET status=1, version=version+1 WHERE order_no='123' AND version=0;
3. HTTP 方法设计
- 利用 HTTP 幂等性语义:
- GET:幂等,用于查询(无副作用)。
- PUT:幂等,用于更新资源(多次更新结果一致)。
- DELETE:幂等,多次删除同一资源结果一致。
- POST:非幂等,如需幂等,需结合唯一 ID 或令牌机制。
4. 状态机控制
- 核心思想:业务对象的状态流转具有确定性,如订单状态只能从创建→支付→完成,重复操作同一状态不改变结果。
- 实现:在更新状态时,通过条件判断(如status=0时允许支付),确保重复操作无效。
UPDATE orders SET status=1 WHERE order_no='123' AND status=0;
5. 令牌(Token)防重提交
- 适用场景:前端表单重复提交(如按钮快速点击)。
- 流程:
- 客户端请求获取令牌(如 UUID,存入 Session 或 LocalStorage)。
- 提交表单时携带令牌,服务端验证令牌有效性(如 Redis 中删除令牌,删除成功则允许处理,否则拒绝)。
- 优点:防止同一用户重复提交,且令牌单次有效。
二、消息幂等性解决方案
1. 消息唯一 ID 去重
- 核心思想:每条消息携带唯一 ID(如 Message ID、业务流水号),消费端处理前检查是否已消费,避免重复处理。
- 实现方式:
- 去重表:在数据库中创建message_processed表,记录已处理的消息 ID,处理前查询是否存在。
- 缓存去重:使用 Redis 等缓存,以消息 ID 为键,存储处理状态(如SET message:id:123 processed NX PX 3600)。
示例(数据库去重表) :
CREATE TABLE message_processed (
message_id VARCHAR(64) PRIMARY KEY, -- 消息唯一ID
process_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 处理消息前检查
INSERT INTO message_processed (message_id) VALUES ('msg_123') ON DUPLICATE KEY UPDATE process_time=NOW();
IF 插入成功 THEN 处理消息 ELSE 跳过
2. 消费端幂等设计
- 基于业务 ID 的幂等:
- 消息携带业务 ID(如订单号),消费端处理时以业务 ID 为键,保证操作幂等。
- 例:更新订单状态时,无论消费多少次,最终状态由业务逻辑决定(如支付中→已支付,重复操作不改变结果)。
- 示例代码:
String businessId = message.getBusinessId();
// 使用Redis分布式锁或数据库唯一约束,保证同一业务ID仅处理一次
if (redis.setIfAbsent("biz:lock:" + businessId, "1", "NX", "PX", 60000)) {
processMessage(message); // 处理消息
}
3. 幂等性中间件特性
- Kafka:
- 幂等生产者:设置enable.idempotence=true,Kafka 自动为每条消息生成唯一 PID 和 Sequence Number,确保重复发送时仅执行一次。
- 事务:结合transactional.id,保证消息生产和消费的原子性。
- RabbitMQ:
- 发布确认(Publisher Confirm) :通过correlationId标记消息,消费端用consumerTag确保幂等。
- 死信队列(DLQ) :处理失败消息时,结合唯一 ID 避免重复入队。
4. 数据库主键 / 唯一索引
- 核心思想:利用数据库主键或唯一索引的唯一性,确保重复消息插入时自动去重(通过INSERT ... ON DUPLICATE KEY UPDATE)。
- 示例:
-- 消息表包含唯一业务ID
CREATE TABLE messages (
msg_id VARCHAR(64) PRIMARY KEY, -- 消息唯一ID
biz_id VARCHAR(64), -- 业务ID(如订单号)
content TEXT,
UNIQUE KEY idx_biz_id (biz_id)
);
-- 插入时自动去重(或更新)
INSERT INTO messages (msg_id, biz_id, content)
VALUES ('msg_123', 'order_456', 'data')
ON DUPLICATE KEY UPDATE content='data', update_time=NOW();
5. 状态机与版本号
- 适用场景:消息驱动的状态变更(如订单状态更新)。
- 实现:在消息中携带当前版本号,消费端更新时检查版本号是否匹配,确保按顺序处理且幂等。
UPDATE orders SET status=1, version=version+1
WHERE order_no='123' AND version=#{message.version};
三、方案对比与选择
| 场景 | 接口幂等 | 消息幂等 |
|---|---|---|
| 核心目标 | 多次请求结果一致 | 多次消费结果一致 |
| 唯一标识 | 请求 ID、令牌、业务 ID | 消息 ID、业务 ID |
| 存储介质 | 缓存(Redis)、数据库唯一索引 | 数据库去重表、缓存、中间件特性(如 Kafka 幂等性) |
| 典型实现 | Token 防重、HTTP 方法设计、乐观锁 | 消息 ID 去重、幂等生产者、数据库唯一键 |
| 复杂度 | 较低(依赖缓存或数据库) | 中高(需结合中间件特性和消费端逻辑) |
总结
- 接口幂等:优先使用唯一请求 ID或数据库唯一约束,结合 HTTP 方法语义设计,简单高效。
- 消息幂等:依赖消息唯一 ID 去重和消费端幂等逻辑,同时利用中间件的幂等特性(如 Kafka 幂等生产者),确保消息可靠且不重复处理。
- 核心原则:通过唯一标识(ID / 令牌)、数据库约束、状态机等方式,将 “可能重复的操作” 转化为 “确定性的结果”,避免副作用累积。