文章目录
-
- 一、核心概念深度解析
-
- [1.1 什么是幂等性](#1.1 什么是幂等性)
-
- [1.1.1 数学定义](#1.1.1 数学定义)
- [1.1.2 分布式系统中的幂等性](#1.1.2 分布式系统中的幂等性)
- [1.1.3 HTTP方法幂等性详解](#1.1.3 HTTP方法幂等性详解)
- [1.1.4 业务场景深度分析](#1.1.4 业务场景深度分析)
- [1.2 为什么需要幂等性](#1.2 为什么需要幂等性)
-
- [1.2.1 分布式环境下的挑战](#1.2.1 分布式环境下的挑战)
- [1.2.2 八大典型问题场景](#1.2.2 八大典型问题场景)
- [1.3 幂等性设计原则](#1.3 幂等性设计原则)
- 二、六大幂等性实现方案
-
- [2.1 数据库唯一约束](#2.1 数据库唯一约束)
-
- [2.1.1 方案原理](#2.1.1 方案原理)
- [2.1.2 方案一: 单字段唯一约束](#2.1.2 方案一: 单字段唯一约束)
- [2.1.3 方案二: 联合唯一约束](#2.1.3 方案二: 联合唯一约束)
- [2.2 分布式锁方案](#2.2 分布式锁方案)
- [2.3 Token令牌方案](#2.3 Token令牌方案)
- [2.4 状态机方案](#2.4 状态机方案)
- [2.5 乐观锁版本号](#2.5 乐观锁版本号)
- [2.6 消息队列幂等](#2.6 消息队列幂等)
- 三、综合实战案例
-
- [3.1 电商下单场景 (多重幂等保证)](#3.1 电商下单场景 (多重幂等保证))
- [3.2 支付回调幂等处理](#3.2 支付回调幂等处理)
- [3.3 定时任务幂等](#3.3 定时任务幂等)
- 四、方案选型对比
- 五、最佳实践建议
-
- [5.1 设计原则](#5.1 设计原则)
- [5.2 常见陷阱](#5.2 常见陷阱)
- [5.3 性能优化](#5.3 性能优化)
- [5.4 监控指标](#5.4 监控指标)
一、核心概念深度解析
1.1 什么是幂等性
1.1.1 数学定义
在数学和计算机科学中,幂等性指的是某操作执行多次与执行一次的效果相同:
f(f(x)) = f(x)
例如:
- 绝对值函数: abs(abs(-5)) = abs(-5) = 5
- 设置操作: set(x, 10) 无论执行多少次,x的值都是10
1.1.2 分布式系统中的幂等性
在分布式系统中,幂等性是指使用相同参数重复执行同一操作,系统状态和返回结果保持一致。
关键要点:
- ✅ 相同的输入参数
- ✅ 相同的业务语义
- ✅ 系统最终状态一致
- ✅ 可能返回相同或不同的响应(如第一次返回订单ID,后续返回"已存在")
1.1.3 HTTP方法幂等性详解
| 方法 | 幂等性 | 说明 | 示例 |
|---|---|---|---|
| GET | ✅ 是 | 查询操作,不改变服务器状态 | GET /api/users/1 |
| DELETE | ✅ 是 | 删除资源,第一次删除成功,后续返回404 | DELETE /api/users/1 |
| UPDATE | ✅ 是 | 更新资源,多次执行结果可能相同 | PUT /api/users/1 {"name":"张三"} |
| ❌ 否 | 更新资源,多次执行结果可能不同 | PUT /api/users/1 {"age": age+1} |
|
| POST | ✅ 是 | 创建资源,每次创建新实体,有主键唯一性约束 | POST /api/orders |
| ❌ 否 | 创建资源,每次创建新实体,没有有主键唯一性约束 | POST /api/orders |
1.1.4 业务场景深度分析
1. 天然幂等的操作:
针对GET、DELETE、未做计算的UPDATE、有唯一性约束的POST
java
// 示例1: 设置用户状态
UPDATE t_user SET status = 'ACTIVE' WHERE id = 1001;
// 无论执行多少次,用户状态都是ACTIVE
// 示例2: 删除订单
DELETE FROM t_order WHERE order_no = 'ORD20241225001';
// 第一次删除成功,后续执行影响行数为0,但结果一致
// 示例3: 查询余额
SELECT balance FROM t_account WHERE user_id = 1001;
// 查询操作不改变状态,天然幂等
// 示例4: 设置商品价格
UPDATE t_product SET price = 99.00 WHERE product_id = 2001;
2. 非幂等的操作(需要改造):
java
// 示例1: 创建订单
INSERT INTO t_order (order_no, user_id, amount)
VALUES ('ORD20241225001', 1001, 999.00);
// 问题: 每次执行都创建新订单
// 解决: 订单号唯一约束 + 异常处理
// 示例2: 扣减库存
UPDATE t_product SET stock = stock - 1 WHERE product_id = 2001;
// 问题: 每次执行都减1,重复执行库存会变负
// 解决: 乐观锁 + 库存检查
// 示例3: 增加积分
UPDATE t_user_points SET points = points + 100 WHERE user_id = 1001;
// 问题: 每次执行都加100,重复执行积分增多
// 解决: 积分流水表 + 唯一约束
// 示例4: 发送通知
sendEmail(userId, "订单支付成功");
// 问题: 每次执行都发送邮件,用户收到多封
// 解决: 通知记录表 + 去重
3. 容易混淆的场景:
java
// 场景1: 看似幂等,实则非幂等
UPDATE t_order SET update_time = NOW() WHERE order_no = 'ORD001';
// update_time每次都不同,严格来说非幂等
// 但业务上可接受,关键字段未变
// 场景2: 条件删除的幂等性
DELETE FROM t_order WHERE user_id = 1001 AND status = 'PENDING';
// 如果有多条PENDING订单,第一次可能删除多条
// 第二次执行删除0条
// 结果不同,但最终状态一致,可认为幂等
// 场景3: 批量操作的幂等性
UPDATE t_product SET status = 'ONLINE'
WHERE category = '电子产品' AND status = 'OFFLINE';
// 第一次: 更新100条
// 第二次: 更新0条(都已ONLINE)
// 最终状态一致,幂等
1.2 为什么需要幂等性
1.2.1 分布式环境下的挑战
场景1: 网络不稳定导致重试
时间线: t0 t1 t2 t3 t4
|
客户端: |--发送请求--> 重试请求-->
|
ネット: | <丢包> OK
|
服务端: | 处理完成 再次处理
| ↓ ↓
结果: | 订单A创建 订单B创建 ❌*
问题:
- 用户只想创建1个订单,但系统创建了2个
- 用户可能被扣款2次
- 库存被扣减2次
场景2: 超时时间设置不当
java
// 客户端配置
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(1000); // 连接超时1秒
factory.setReadTimeout(3000); // 读取超时3秒
restTemplate.setRequestFactory(factory);
// 服务端处理
@PostMapping("/api/orders")
public Order createOrder(@RequestBody OrderRequest request) {
// 业务处理需要5秒
Order order = orderService.createOrder(request); // 耗时5秒
return order;
}
// 问题分析:
// t0: 客户端发起请求
// t3: 客户端读取超时(3秒),抛出SocketTimeoutException
// t5: 服务端处理完成,创建订单成功
// 客户端认为失败,重试 -> 创建第二个订单
1.2.2 八大典型问题场景
场景1: 前端重复提交
javascript
// 问题代码
<button onclick="submitOrder()">提交订单</button>
function submitOrder() {
// 没有防重复点击
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData)
});
}
// 用户快速点击3次 -> 创建3个订单
解决方案:
javascript
// 方案1: 按钮禁用
let submitting = false;
function submitOrder() {
if (submitting) return;
submitting = true;
document.getElementById('submitBtn').disabled = true;
fetch('/api/orders', {
method: 'POST',
body: JSON.stringify(orderData)
}).finally(() => {
submitting = false;
document.getElementById('submitBtn').disabled = false;
});
}
// 方案2: Token机制(后文详述)
场景2: HTTP客户端自动重试
java
// Spring Cloud Feign默认重试配置
@FeignClient(name = "order-service")
public interface OrderClient {
@PostMapping("/api/orders")
Order createOrder(@RequestBody OrderRequest request);
}
// application.yml
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
httpclient:
max-connections: 200
connection-timeout: 2000
# Ribbon重试配置(危险!)
ribbon:
MaxAutoRetries: 1 # 同一台服务器重试1次
MaxAutoRetriesNextServer: 1 # 切换服务器重试1次
OkToRetryOnAllOperations: true # ⚠️ 所有操作都重试,包括POST
# 问题:
# POST创建订单接口,如果超时会自动重试
# 第一次请求: 创建订单A
# 重试请求: 创建订单B
正确配置:
yaml
ribbon:
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
OkToRetryOnAllOperations: false # 只重试GET请求
# 或使用Resilience4j
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
retryExceptions:
- java.net.SocketTimeoutException
ignoreExceptions:
- com.example.BusinessException
场景3: 消息队列At-Least-Once语义
java
// Kafka消费者
@KafkaListener(topics = "order-paid")
public void handleOrderPaid(ConsumerRecord<String, String> record) {
String orderId = record.value();
// 处理支付成功逻辑
addUserPoints(orderId); // 增加用户积分
deductStock(orderId); // 扣减库存
sendNotification(orderId); // 发送通知
// ⚠️ 如果处理过程中JVM崩溃,消息未提交offset
// 重启后会再次消费同一消息
}
// 问题:
// 第一次消费: 增加100积分
// JVM崩溃,offset未提交
// 重启后再次消费: 又增加100积分
// 用户实际获得200积分 ❌
Kafka重复消费的4种情况:
1. 消费者处理完消息,但提交offset前崩溃
2. 网络抖动导致offset提交失败
3. Rebalance触发,offset未及时提交
4. 消费者处理时间过长,被剔除消费组,重新消费
场景4: 微服务调用链重试
用户 -> 网关 -> 订单服务 -> 库存服务 -> 数据库
↓
超时3s
↓
重试 -> 库存服务 -> 数据库
↓
再次扣减库存 ❌
详细案例:
java
// 订单服务
@Service
public class OrderService {
@Autowired
private StockClient stockClient;
public Order createOrder(OrderRequest request) {
// 1. 创建订单
Order order = saveOrder(request);
// 2. 调用库存服务扣减库存
try {
stockClient.deductStock(request.getProductId(), request.getQuantity());
} catch (ReadTimeoutException e) {
// 超时,但不知道库存服务是否执行成功
// 重试 -> 可能导致库存重复扣减
stockClient.deductStock(request.getProductId(), request.getQuantity());
}
return order;
}
}
// 库存服务(非幂等)
@Service
public class StockService {
public void deductStock(Long productId, Integer quantity) {
// 非幂等的SQL
jdbcTemplate.update(
"UPDATE t_product SET stock = stock - ? WHERE product_id = ?",
quantity, productId
);
// 问题: 第一次扣减成功,返回响应超时
// 订单服务重试,再次扣减
// 实际库存扣减了2倍 ❌
}
}
场景5: 定时任务重叠执行
java
// 每天凌晨1点执行订单自动完成任务
@Scheduled(cron = "0 0 1 * * ?")
public void autoCompleteOrders() {
List<Order> orders = orderRepository.findPendingOrders();
for (Order order : orders) {
// 处理订单,耗时较长
completeOrder(order); // 假设处理1万个订单需要2小时
}
}
// 问题:
// 1:00 开始执行任务A
// 2:00 任务A还在执行中
// 次日1:00 启动任务B,与任务A并发执行
// 同一订单被处理2次 ❌
场景6: 数据库主从延迟
java
// 写主库
@Transactional
public void createOrder(OrderRequest request) {
Order order = new Order();
order.setOrderNo(request.getOrderNo());
orderRepository.save(order); // 写入主库
}
// 立即读从库
public Order getOrder(String orderNo) {
return orderRepository.findByOrderNo(orderNo); // 读从库
}
// 问题:
// t0: 创建订单,写入主库
// t1: 客户端超时,认为失败
// t2: 客户端重试创建订单
// t2: 查询订单是否存在(读从库),因主从延迟,查询不到
// t2: 认为不存在,再次创建 ❌
场景7: 分布式事务回滚
java
// Seata分布式事务
@GlobalTransactional
public void createOrder(OrderRequest request) {
// 1. 创建订单
orderService.createOrder(request);
// 2. 扣减库存
stockService.deductStock(request.getProductId(), request.getQuantity());
// 3. 扣减余额
accountService.deductBalance(request.getUserId(), request.getAmount());
// 问题: 如果第3步失败,前2步回滚
// 但如果回滚过程中网络异常,可能导致部分回滚,部分未回滚
}
场景8: 缓存击穿导致重复写入
java
public Product getProduct(Long productId) {
// 1. 查缓存
String cacheKey = "product:" + productId;
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 缓存未命中,查数据库
product = productRepository.findById(productId).orElse(null);
if (product != null) {
// 3. 写入缓存
redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
}
return product;
}
// 问题:
// 高并发下,缓存失效瞬间,1000个请求同时到达
// 1000个请求都发现缓存未命中
// 1000个请求都去查数据库
// 1000个请求都写入缓存
// 数据库压力暴增 ❌
1.3 幂等性设计原则
识别操作类型:
- 天然幂等: 查询、删除、绝对值设置
- 需要保证: 创建、累加、状态流转
唯一性标识:
- 业务主键: 订单号、流水号
- 技术唯一键: 幂等Token、请求ID
状态机设计:
- 严格的状态转换规则
- 记录状态变更历史
二、六大幂等性实现方案
2.1 数据库唯一约束
2.1.1 方案原理
核心思想: 利用数据库的UNIQUE约束特性,在数据库层面保证数据唯一性。
工作流程:
1. 设计唯一业务主键(如订单号、流水号)
2. 在数据库表上创建UNIQUE约束或UNIQUE索引
3. 插入数据时,如果主键重复,数据库抛出异常
4. 应用层捕获异常,返回已存在的数据
优势分析:
- ✅ 简单可靠: 数据库原生特性,不需要额外组件
- ✅ 性能优秀: 单次数据库操作,无需分布式锁
- ✅ 强一致性: 数据库事务保证,ACID特性
- ✅ 维护成本低: 不依赖Redis/Zookeeper等中间件
劣势分析:
- ❌ 业务主键设计: 需要提前约定唯一键生成规则
- ❌ 异常处理: 需要精确识别DuplicateKeyException
- ❌ 无法控制时间窗口: 一旦创建,永久存在(除非手动清理)
- ❌ 跨表场景复杂: 多表操作需要额外设计
2.1.2 方案一: 单字段唯一约束
场景: 订单创建、支付流水记录
数据库设计:
sql
-- 订单表
CREATE TABLE t_order (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
order_no VARCHAR(50) UNIQUE NOT NULL COMMENT '订单号(业务主键)',
user_id BIGINT NOT NULL COMMENT '用户ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
quantity INT NOT NULL COMMENT '数量',
unit_price DECIMAL(10,2) NOT NULL COMMENT '单价',
total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
status VARCHAR(20) DEFAULT 'CREATED' COMMENT '状态',
remark VARCHAR(500) COMMENT '备注',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-- 唯一约束
UNIQUE KEY uk_order_no (order_no),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
完整业务实现:
java
package com.example.idempotent.service.impl;
import com.example.idempotent.entity.Order;
import com.example.idempotent.dto.CreateOrderRequest;
import com.example.idempotent.repository.OrderRepository;
import com.example.idempotent.util.OrderNoGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Slf4j
@Service
public class OrderServiceImpl {
@Autowired
private OrderRepository orderRepository;
@Autowired
private OrderNoGenerator orderNoGenerator;
@Autowired
private ProductService productService;
@Autowired
private UserService userService;
/**
* 创建订单 - 方式1: 后端生成订单号
*
* 幂等保证: 订单号唯一约束
* 适用场景: 后端控制订单号生成
*/
@Transactional(rollbackFor = Exception.class)
public Order createOrderV1(CreateOrderRequest request) {
log.info("[createOrderV1] 开始创建订单: userId={}, productId={}, quantity={}",
request.getUserId(), request.getProductId(), request.getQuantity());
// 1. 生成订单号
String orderNo = orderNoGenerator.generate(request.getUserId());
try {
// 2. 构建订单对象
Order order = buildOrder(orderNo, request);
// 3. 保存订单(唯一约束保证幂等)
Order saved = orderRepository.save(order);
// 4. 后续业务逻辑
afterOrderCreated(saved);
return saved;
} catch (DuplicateKeyException e) {
// 订单号重复,理论上不应该发生(因为包含时间戳+随机数)
log.error("[createOrderV1] 订单号重复(极小概率): orderNo={}", orderNo, e);
// 重新生成订单号再试一次
return createOrderV1(request);
} catch (Exception e) {
log.error("[createOrderV1] 创建订单失败", e);
throw new RuntimeException("创建订单失败: " + e.getMessage());
}
}
/**
* 创建订单 - 方式2: 客户端请求ID
*
* 幂等保证: 使用客户端的requestId作为唯一键
* 适用场景: 需要精确追踪每次请求
*/
@Transactional(rollbackFor = Exception.class)
public Order createOrderV3(String requestId, CreateOrderRequest request) {
log.info("[createOrderV3] 开始创建订单: requestId={}", requestId);
// 1. 先查询是否已处理该请求
Order existOrder = orderRepository.findByRequestId(requestId);
if (existOrder != null) {
log.info("[createOrderV3] 请求已处理,幂等返回: requestId={}, orderId={}",
requestId, existOrder.getId());
return existOrder;
}
// 2. 生成订单号
String orderNo = orderNoGenerator.generate(request.getUserId());
try {
// 3. 构建订单(包含requestId)
Order order = buildOrder(orderNo, request);
order.setRequestId(requestId);
// 4. 保存订单
Order saved = orderRepository.save(order);
log.info("[createOrderV3] 订单创建成功: orderId={}", saved.getId());
afterOrderCreated(saved);
return saved;
} catch (DuplicateKeyException e) {
// requestId重复,幂等处理
log.warn("[createOrderV3] 请求ID重复: requestId={}", requestId);
return orderRepository.findByRequestId(requestId);
}
}
/**
* 订单创建后的后续处理
*/
private void afterOrderCreated(Order order) {
// 1. 扣减库存
// 2. 锁定优惠券
// 3. 发送MQ消息
// 4. 发送通知
}
}
2.1.3 方案二: 联合唯一约束
场景: 防止同一用户重复操作
数据库设计:
sql
-- 点赞记录表
CREATE TABLE t_like_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL COMMENT '用户ID',
target_type VARCHAR(20) NOT NULL COMMENT '目标类型: POST/COMMENT/VIDEO',
target_id BIGINT NOT NULL COMMENT '目标ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-- 联合唯一约束: 同一用户对同一目标只能点赞一次
UNIQUE KEY uk_user_target (user_id, target_type, target_id),
INDEX idx_target (target_type, target_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点赞记录表';
业务实现:
java
package com.example.idempotent.service;
import com.example.idempotent.entity.LikeRecord;
import com.example.idempotent.repository.LikeRecordRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class LikeService {
@Autowired
private LikeRecordRepository likeRecordRepository;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 点赞 - 联合唯一约束保证幂等
*
* @param userId 用户ID
* @param targetType 目标类型
* @param targetId 目标ID
* @return true-点赞成功, false-已点赞
*/
@Transactional(rollbackFor = Exception.class)
public boolean like(Long userId, String targetType, Long targetId) {
log.info("[like] 用户点赞: userId={}, targetType={}, targetId={}",
userId, targetType, targetId);
try {
// 1. 创建点赞记录
LikeRecord record = new LikeRecord();
record.setUserId(userId);
record.setTargetType(targetType);
record.setTargetId(targetId);
// 2. 保存(联合唯一约束保证幂等)
likeRecordRepository.save(record);
// 3. 增加点赞数(Redis计数)
String countKey = "like:count:" + targetType + ":" + targetId;
redisTemplate.opsForValue().increment(countKey);
log.info("[like] 点赞成功");
return true;
} catch (DuplicateKeyException e) {
// 已经点赞过,幂等返回成功
log.warn("[like] 已点赞,幂等返回: userId={}, targetId={}",
userId, targetId);
return true; // 幂等操作,返回成功
}
}
}
2.2 分布式锁方案
适用场景: 高并发、复杂业务逻辑
Redis分布式锁实现:
java
@Service
public class PaymentService {
@Autowired
private RedissonClient redissonClient;
@Transactional
public boolean payOrder(String orderNo, BigDecimal amount) {
String lockKey = "order:pay:" + orderNo;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待3秒,锁10秒后自动释放
if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 查询订单
Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
// 幂等判断
if ("PAID".equals(order.getStatus())) {
return true; // 已支付,幂等返回
}
// 执行支付
callPaymentGateway(orderNo, amount);
// 更新状态
order.setStatus("PAID");
orderRepository.save(order);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁失败");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
优点: ✅ 跨JVM ✅ 防死锁 ✅ 可重入
缺点: ❌ 依赖Redis ❌ 性能开销 ❌ 锁超时处理
2.3 Token令牌方案
适用场景: 前端提交、表单防重
流程:
1. 前端请求Token
2. 后端生成Token存Redis
3. 前端提交携带Token
4. 后端验证并删除Token(原子操作)
实现:
java
@Service
public class TokenService {
@Autowired
private StringRedisTemplate redisTemplate;
// 生成Token
public String generateToken(String bizType, String bizId) {
String token = UUID.randomUUID().toString().replace("-", "");
String key = "token:" + bizType + ":" + bizId + ":" + token;
redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
return token;
}
// 验证并消费Token(Lua脚本保证原子性)
public boolean verifyAndConsumeToken(String bizType, String bizId, String token) {
String key = "token:" + bizType + ":" + bizId + ":" + token;
String script =
"if redis.call('get', KEYS[1]) then " +
" redis.call('del', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
(RedisCallback<Long>) connection -> connection.eval(
script.getBytes(), ReturnType.INTEGER, 1, key.getBytes()
)
);
return result != null && result == 1;
}
}
Controller:
java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private TokenService tokenService;
// 获取Token
@GetMapping("/token")
public Result<String> getToken(@RequestParam Long userId) {
String token = tokenService.generateToken("CREATE_ORDER", userId.toString());
return Result.success(token);
}
// 创建订单(携带Token)
@PostMapping
public Result<Order> createOrder(
@RequestHeader("X-Idempotent-Token") String token,
@RequestBody OrderRequest request) {
// 验证并消费Token
boolean valid = tokenService.verifyAndConsumeToken(
"CREATE_ORDER", request.getUserId().toString(), token);
if (!valid) {
return Result.error("请勿重复提交");
}
Order order = orderService.createOrder(request);
return Result.success(order);
}
}
优点: ✅ 防前端重复提交 ✅ 一次性Token ✅ 可设置有效期
缺点: ❌ 两次请求 ❌ 依赖Redis
2.4 状态机方案
适用场景: 订单流程、工单流转
状态定义:
java
@Getter
public enum OrderStatus {
CREATED("已创建"),
PAID("已支付"),
SHIPPED("已发货"),
COMPLETED("已完成"),
CANCELLED("已取消");
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
// 判断是否可以转换
public boolean canTransitionTo(OrderStatus target) {
switch (this) {
case CREATED:
return target == PAID || target == CANCELLED;
case PAID:
return target == SHIPPED || target == CANCELLED;
case SHIPPED:
return target == COMPLETED;
case COMPLETED:
case CANCELLED:
return false; // 终态
default:
return false;
}
}
}
服务实现:
java
@Service
public class OrderStateMachineService {
@Transactional
public boolean transitionStatus(String orderNo, OrderStatus targetStatus) {
// 查询订单
Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
OrderStatus currentStatus = OrderStatus.valueOf(order.getStatus());
// 幂等判断: 已经是目标状态
if (currentStatus == targetStatus) {
return true;
}
// 状态机校验
if (!currentStatus.canTransitionTo(targetStatus)) {
throw new RuntimeException(
String.format("不允许从[%s]转换到[%s]",
currentStatus.getDesc(), targetStatus.getDesc())
);
}
// 更新状态
order.setStatus(targetStatus.name());
orderRepository.save(order);
// 记录历史
saveStatusHistory(order, currentStatus, targetStatus);
return true;
}
}
优点: ✅ 业务语义清晰 ✅ 可追溯 ✅ 天然幂等
缺点: ❌ 状态设计复杂 ❌ 需要详细规则
2.5 乐观锁版本号
适用场景: 库存扣减、余额变更
数据库设计:
sql
CREATE TABLE t_product_stock (
id BIGINT PRIMARY KEY,
product_id BIGINT UNIQUE,
stock INT NOT NULL,
version INT DEFAULT 0, -- 版本号
update_time DATETIME
);
实现:
java
@Service
public class StockService {
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
for (int i = 0; i < 3; i++) { // 最多重试3次
// 查询当前库存
ProductStock stock = stockRepository.findByProductId(productId)
.orElseThrow();
// 检查库存
if (stock.getStock() < quantity) {
return false;
}
// 乐观锁更新
int updated = stockRepository.deductStockWithVersion(
productId, quantity, stock.getVersion());
if (updated > 0) {
return true; // 更新成功
}
// 版本冲突,重试
Thread.sleep(50);
}
return false; // 超过重试次数
}
}
SQL:
xml
<update id="deductStockWithVersion">
UPDATE t_product_stock
SET stock = stock - #{quantity},
version = version + 1,
update_time = NOW()
WHERE product_id = #{productId}
AND stock >= #{quantity}
AND version = #{version}
</update>
优点: ✅ 无锁高并发 ✅ 数据库保证 ✅ 性能好
缺点: ❌ ABA问题 ❌ 需要重试机制
2.6 消息队列幂等
适用场景: 异步消息处理、事件驱动
方案一: 消息ID去重表
sql
CREATE TABLE t_message_record (
id BIGINT PRIMARY KEY,
message_id VARCHAR(100) UNIQUE NOT NULL,
topic VARCHAR(50),
content TEXT,
process_time DATETIME
);
java
@Component
public class OrderPaidConsumer {
@KafkaListener(topics = "order-paid")
@Transactional
public void handleOrderPaid(ConsumerRecord<String, String> record) {
String messageId = record.key();
try {
// 记录消息(唯一约束保证幂等)
MessageRecord mr = new MessageRecord();
mr.setMessageId(messageId);
mr.setTopic("order-paid");
mr.setContent(record.value());
messageRecordRepository.save(mr);
// 处理业务
processOrderPaid(record.value());
} catch (DuplicateKeyException e) {
// 消息已处理,幂等跳过
log.warn("消息已处理: {}", messageId);
}
}
}
方案二: Redis布隆过滤器
java
@Component
public class MessageIdempotentFilter {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
100_000_000, // 1亿元素
0.0001 // 误判率0.01%
);
}
public boolean isDuplicate(String messageId) {
if (bloomFilter.mightContain(messageId)) {
// 布隆过滤器判断可能存在,数据库二次确认
return messageRecordRepository.existsByMessageId(messageId);
}
return false;
}
public void markProcessed(String messageId) {
bloomFilter.put(messageId);
}
}
优点: ✅ 适合海量消息 ✅ 内存占用小
缺点: ❌ 存在误判 ❌ 需二次确认
三、综合实战案例
3.1 电商下单场景 (多重幂等保证)
业务流程:
创建订单 -> 扣库存 -> 锁优惠券 -> 调用支付 -> 发MQ
完整实现:
java
@Service
public class OrderCreateService {
@Autowired
private DistributedLockUtil lockUtil;
@Transactional
public Order createOrder(CreateOrderDTO dto) {
String orderNo = dto.getOrderNo();
// 1. 业务主键防重复创建
Order order = tryCreateOrderRecord(dto);
if (order != null && "COMPLETED".equals(order.getStatus())) {
return order; // 已完成,幂等返回
}
// 2. 分布式锁保证串行
String lockKey = "order:create:" + orderNo;
return lockUtil.executeWithLock(lockKey, 5, 30, () -> {
// 3. 扣减库存(乐观锁)
boolean stockOk = stockService.deductStock(
dto.getProductId(), dto.getQuantity());
if (!stockOk) {
throw new RuntimeException("库存不足");
}
// 4. 锁定优惠券(唯一约束)
if (dto.getCouponId() != null) {
boolean couponOk = couponService.lockCoupon(
dto.getUserId(), dto.getCouponId(), orderNo);
if (!couponOk) {
stockService.rollbackStock(dto.getProductId(), dto.getQuantity());
throw new RuntimeException("优惠券锁定失败");
}
}
// 5. 调用支付(幂等接口)
boolean payOk = paymentService.pay(orderNo, dto.getAmount());
if (!payOk) {
// 回滚优惠券和库存
rollbackAll(dto, orderNo);
throw new RuntimeException("支付失败");
}
// 6. 更新订单状态(状态机)
Order finalOrder = orderRepository.findByOrderNo(orderNo).orElseThrow();
finalOrder.setStatus("COMPLETED");
orderRepository.save(finalOrder);
// 7. 发送MQ(消息ID幂等)
sendOrderCreatedEvent(finalOrder);
return finalOrder;
});
}
private Order tryCreateOrderRecord(CreateOrderDTO dto) {
try {
Order order = new Order();
order.setOrderNo(dto.getOrderNo());
order.setUserId(dto.getUserId());
order.setStatus("CREATED");
return orderRepository.save(order);
} catch (DuplicateKeyException e) {
return orderRepository.findByOrderNo(dto.getOrderNo()).orElse(null);
}
}
}
3.2 支付回调幂等处理
场景: 支付网关可能多次回调
java
@RestController
@RequestMapping("/api/payment")
public class PaymentCallbackController {
@Autowired
private PaymentCallbackService callbackService;
@PostMapping("/callback")
public String handleCallback(@RequestBody PaymentCallbackDTO dto) {
// 1. 验签
if (!verifySign(dto)) {
return "FAIL";
}
// 2. 幂等处理(业务流水号+状态机)
try {
callbackService.processCallback(dto.getOrderNo(), dto.getPaymentNo());
return "SUCCESS";
} catch (DuplicateKeyException e) {
// 已处理过,幂等返回成功
return "SUCCESS";
} catch (Exception e) {
log.error("处理支付回调失败", e);
return "FAIL";
}
}
}
@Service
public class PaymentCallbackService {
@Transactional
public void processCallback(String orderNo, String paymentNo) {
// 1. 记录支付流水(唯一约束)
PaymentRecord record = new PaymentRecord();
record.setPaymentNo(paymentNo); // 唯一键
record.setOrderNo(orderNo);
record.setStatus("SUCCESS");
paymentRecordRepository.save(record); // 重复会抛异常
// 2. 更新订单状态(状态机)
Order order = orderRepository.findByOrderNo(orderNo).orElseThrow();
if ("PAID".equals(order.getStatus())) {
return; // 已支付,幂等返回
}
if (!"CREATED".equals(order.getStatus())) {
throw new RuntimeException("订单状态异常");
}
order.setStatus("PAID");
orderRepository.save(order);
// 3. 发送MQ通知
sendPaymentSuccessEvent(order);
}
}
3.3 定时任务幂等
场景: 定时任务可能重复执行
java
@Component
public class OrderAutoCompleteTask {
@Autowired
private RedissonClient redissonClient;
// 每小时执行一次
@Scheduled(cron = "0 0 * * * ?")
public void autoCompleteOrders() {
String lockKey = "task:auto-complete:" +
LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,获取不到说明任务正在执行
if (!lock.tryLock(0, 3600, TimeUnit.SECONDS)) {
log.warn("任务正在执行中,本次跳过");
return;
}
// 执行任务
List<Order> orders = findPendingOrders();
for (Order order : orders) {
completeOrder(order);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
四、方案选型对比
| 方案 | 适用场景 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
| 数据库唯一约束 | 创建操作 | 简单、强一致 | 依赖数据库 | ⭐⭐⭐⭐⭐ |
| 分布式锁 | 复杂业务 | 跨JVM、防并发 | 性能开销大 | ⭐⭐⭐⭐ |
| Token令牌 | 前端提交 | 防重复提交 | 两次请求 | ⭐⭐⭐⭐⭐ |
| 状态机 | 流程控制 | 业务清晰 | 设计复杂 | ⭐⭐⭐⭐⭐ |
| 乐观锁 | 库存扣减 | 高并发 | 需要重试 | ⭐⭐⭐⭐ |
| 消息去重 | MQ消费 | 异步处理 | 依赖存储 | ⭐⭐⭐⭐⭐ |
五、最佳实践建议
5.1 设计原则
- 识别幂等边界: 明确哪些操作需要幂等
- 选择合适方案: 根据场景选择最优方案
- 多重保障: 关键业务使用多种方案组合
- 日志追踪: 记录所有幂等处理过程
- 监控告警: 监控幂等失败率
5.2 常见陷阱
❌ 错误做法:
java
// 不幂等的扣库存
UPDATE t_stock SET stock = stock - 1 WHERE product_id = ?
✅ 正确做法:
java
// 幂等的扣库存(乐观锁)
UPDATE t_stock
SET stock = stock - 1, version = version + 1
WHERE product_id = ? AND version = ? AND stock >= 1
5.3 性能优化
- 减少锁粒度: 使用细粒度锁(订单号而非全局锁)
- 异步处理: 非核心流程异步化
- 缓存优化: Redis缓存Token和处理记录
- 批量操作: 消息批量去重
5.4 监控指标
java
// 幂等处理监控
public class IdempotentMetrics {
@Autowired
private MeterRegistry meterRegistry;
public void recordIdempotentSkip(String bizType) {
meterRegistry.counter("idempotent.skip", "type", bizType).increment();
}
public void recordIdempotentSuccess(String bizType) {
meterRegistry.counter("idempotent.success", "type", bizType).increment();
}
public void recordIdempotentFailure(String bizType) {
meterRegistry.counter("idempotent.failure", "type", bizType).increment();
}
}