副标题:从理论到实践,彻底搞懂幂等性!🎯
🎬 开场:可怕的重复执行
真实故障案例
某支付系统的灾难:
用户操作:
10:00:00 用户点击"支付"按钮
服务器处理中...
10:00:05 用户等不及,再次点击"支付"
服务器又收到一次请求...
结果:
订单ID:20231212001
应扣款:100元
实际扣款:200元!❌
用户投诉:
"我只买了100块钱的东西,
为什么扣了我200?!"
原因:
支付接口没有做幂等性处理!
幂等性的定义
幂等性(Idempotence):
同一个操作执行多次,结果和执行一次相同
数学定义:
f(f(x)) = f(x)
例子:
幂等操作 ✅:
- 查询操作:SELECT * FROM users
- 删除操作:DELETE FROM users WHERE id = 1
- 设置操作:UPDATE users SET status = 1 WHERE id = 1
非幂等操作 ❌:
- 增加操作:UPDATE users SET balance = balance + 100
- 创建操作:INSERT INTO orders VALUES (...)
- 扣减操作:UPDATE products SET stock = stock - 1
📚 为什么需要幂等性?
场景1:网络重试
客户端 → 服务器:创建订单请求
↓
网络超时(实际已创建)
↓
客户端 → 服务器:重试,再次创建订单
↓
创建了两个订单!❌
场景2:消息队列重复消费
生产者 → MQ → 消费者:处理订单
↓
消费者处理完成
但ACK消息丢失
↓
MQ → 消费者:重新投递
↓
订单被重复处理!❌
场景3:用户重复操作
用户快速点击"提交"按钮3次
↓
3个请求到达服务器
↓
创建了3条相同数据!❌
🛠️ 幂等性实现方案
方案1:数据库唯一约束 ⭐⭐⭐⭐⭐
最简单可靠的方案!
-- 订单表添加唯一约束
CREATE TABLE `orders` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL UNIQUE, -- 订单号唯一约束
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`status` TINYINT NOT NULL,
`create_time` DATETIME NOT NULL,
UNIQUE KEY `uk_order_no` (`order_no`) -- 唯一索引
);
-- 支付流水表
CREATE TABLE `payment_record` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`order_no` VARCHAR(64) NOT NULL UNIQUE, -- 订单号唯一约束
`transaction_id` VARCHAR(64) NOT NULL UNIQUE, -- 交易流水号唯一约束
`amount` DECIMAL(10,2) NOT NULL,
`status` TINYINT NOT NULL,
`create_time` DATETIME NOT NULL,
UNIQUE KEY `uk_order_no` (`order_no`),
UNIQUE KEY `uk_transaction_id` (`transaction_id`)
);
代码实现:
/**
* 使用数据库唯一约束保证幂等性
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单(幂等)
*/
public Order createOrder(CreateOrderRequest request) {
String orderNo = request.getOrderNo(); // 客户端生成唯一订单号
// 1. 先查询订单是否已存在
Order existingOrder = orderMapper.selectByOrderNo(orderNo);
if (existingOrder != null) {
log.info("订单已存在,返回已有订单: {}", orderNo);
return existingOrder; // 幂等:返回已有订单
}
// 2. 创建新订单
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.CREATED);
order.setCreateTime(new Date());
try {
orderMapper.insert(order);
log.info("订单创建成功: {}", orderNo);
return order;
} catch (DuplicateKeyException e) {
// 3. 唯一约束冲突,说明并发创建了
log.warn("订单号重复,返回已有订单: {}", orderNo);
return orderMapper.selectByOrderNo(orderNo);
}
}
}
优缺点:
优点 ✅:
- 实现简单
- 性能好
- 可靠性高
- 数据库层保证
缺点 ❌:
- 依赖数据库
- 需要业务唯一标识
适用场景:
- 订单创建
- 支付请求
- 用户注册
- 推荐方案 ⭐⭐⭐⭐⭐
方案2:全局唯一ID(Token机制)⭐⭐⭐⭐⭐
防止重复提交的标准方案!
原理
流程:
1. 客户端请求获取Token
2. 服务器生成Token并存入Redis
3. 客户端提交业务请求时携带Token
4. 服务器验证Token并删除(保证只能用一次)
5. 执行业务逻辑
┌──────────┐
│ 客户端 │
└────┬─────┘
│ ① 获取Token
↓
┌────────────┐ ┌─────────┐
│ 服务器 │─────▶│ Redis │
│ │ ② │ Token │
└────┬───────┘◀─────└─────────┘
│ ③ 返回Token
↓
┌──────────┐
│ 客户端 │
│ 保存Token│
└────┬─────┘
│ ④ 提交请求 + Token
↓
┌────────────┐ ┌─────────┐
│ 服务器 │ │ Redis │
│ 验证Token │─────▶│检查删除 │
│ │ ⑤ │ │
└────┬───────┘ └─────────┘
│ ⑥ 执行业务
↓
完成
代码实现
/**
* Token幂等性服务
*/
@Service
public class IdempotentTokenService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String TOKEN_PREFIX = "idempotent:token:";
private static final long TOKEN_EXPIRE_TIME = 5 * 60; // 5分钟
/**
* 生成Token
*/
public String generateToken(Long userId) {
// 生成唯一Token
String token = UUID.randomUUID().toString().replace("-", "");
// 存入Redis
String key = TOKEN_PREFIX + token;
redisTemplate.opsForValue().set(key, String.valueOf(userId),
TOKEN_EXPIRE_TIME, TimeUnit.SECONDS);
log.info("生成幂等Token: userId={}, token={}", userId, token);
return token;
}
/**
* 验证并消费Token(原子操作)
*/
public boolean validateAndConsumeToken(String token, Long userId) {
if (token == null || token.isEmpty()) {
return false;
}
String key = TOKEN_PREFIX + token;
// 使用Lua脚本保证原子性:检查 + 删除
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
" return redis.call('del', KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(userId)
);
boolean success = result != null && result == 1;
if (success) {
log.info("Token验证成功: token={}", token);
} else {
log.warn("Token验证失败(重复或过期): token={}", token);
}
return success;
}
}
/**
* 订单Controller
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private IdempotentTokenService tokenService;
@Autowired
private OrderService orderService;
/**
* 获取幂等Token
*/
@GetMapping("/token")
public Result<String> getToken(@RequestParam Long userId) {
String token = tokenService.generateToken(userId);
return Result.success(token);
}
/**
* 创建订单(幂等)
*/
@PostMapping("/create")
public Result<Order> createOrder(
@RequestBody CreateOrderRequest request,
@RequestHeader("X-Idempotent-Token") String token) {
Long userId = request.getUserId();
// 验证并消费Token
if (!tokenService.validateAndConsumeToken(token, userId)) {
return Result.fail("请勿重复提交");
}
// 执行业务逻辑
Order order = orderService.createOrder(request);
return Result.success(order);
}
}
前端使用:
/**
* 前端Token使用
*/
class OrderForm {
constructor() {
this.token = null;
}
/**
* 页面加载时获取Token
*/
async onPageLoad() {
const userId = this.getUserId();
const response = await fetch(`/order/token?userId=${userId}`);
const result = await response.json();
if (result.code === 200) {
this.token = result.data;
console.log('获取Token成功:', this.token);
}
}
/**
* 提交订单
*/
async submitOrder() {
if (!this.token) {
alert('Token未获取,请刷新页面');
return;
}
const orderData = this.getOrderData();
const response = await fetch('/order/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Idempotent-Token': this.token // 携带Token
},
body: JSON.stringify(orderData)
});
const result = await response.json();
if (result.code === 200) {
alert('订单创建成功');
// Token已消费,重新获取
await this.onPageLoad();
} else {
alert(result.message);
}
}
}
方案3:状态机(State Machine)⭐⭐⭐⭐
适合有明确状态流转的场景!
/**
* 订单状态机
*/
public enum OrderStatus {
CREATED(1, "已创建"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消");
private final int code;
private final String desc;
OrderStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
}
/**
* 订单服务(使用状态机保证幂等性)
*/
@Service
public class OrderStateMachineService {
@Autowired
private OrderMapper orderMapper;
/**
* 支付订单(幂等)
*/
@Transactional
public boolean payOrder(String orderNo) {
// 1. 查询订单
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 2. 检查当前状态
if (order.getStatus() == OrderStatus.PAID) {
log.info("订单已支付,幂等返回成功: {}", orderNo);
return true; // 幂等:已支付,直接返回成功
}
if (order.getStatus() != OrderStatus.CREATED) {
throw new BusinessException("订单状态不允许支付: " + order.getStatus());
}
// 3. 更新订单状态(使用乐观锁)
int rows = orderMapper.updateStatusWithVersion(
orderNo,
OrderStatus.CREATED, // 期望当前状态
OrderStatus.PAID, // 目标状态
order.getVersion() // 版本号
);
if (rows == 0) {
// 更新失败,可能是并发修改
log.warn("订单状态更新失败,可能已被处理: {}", orderNo);
// 重新查询确认状态
order = orderMapper.selectByOrderNo(orderNo);
return order.getStatus() == OrderStatus.PAID;
}
log.info("订单支付成功: {}", orderNo);
return true;
}
}
SQL实现(乐观锁):
<!-- OrderMapper.xml -->
<update id="updateStatusWithVersion">
UPDATE orders
SET status = #{newStatus},
version = version + 1,
update_time = NOW()
WHERE order_no = #{orderNo}
AND status = #{oldStatus}
AND version = #{version}
</update>
状态流转规则:
订单状态流转:
CREATED(已创建)
↓ pay()
PAID(已支付)
↓ ship()
SHIPPED(已发货)
↓ complete()
COMPLETED(已完成)
不允许的流转:
PAID → CREATED ❌
SHIPPED → PAID ❌
COMPLETED → * ❌
幂等性:
当前状态已是目标状态,直接返回成功
方案4:去重表 ⭐⭐⭐⭐
专门用于记录已处理的请求!
-- 幂等去重表
CREATE TABLE `idempotent_record` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`business_type` VARCHAR(32) NOT NULL COMMENT '业务类型',
`business_key` VARCHAR(128) NOT NULL COMMENT '业务唯一标识',
`request_id` VARCHAR(64) NOT NULL COMMENT '请求ID',
`request_data` TEXT COMMENT '请求数据',
`response_data` TEXT COMMENT '响应数据',
`status` TINYINT NOT NULL COMMENT '状态:1-处理中,2-成功,3-失败',
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
UNIQUE KEY `uk_business` (`business_type`, `business_key`)
);
代码实现:
/**
* 幂等记录服务
*/
@Service
public class IdempotentRecordService {
@Autowired
private IdempotentRecordMapper recordMapper;
/**
* 检查并记录请求
*/
public IdempotentResult checkAndRecord(
String businessType,
String businessKey,
String requestData) {
// 1. 查询是否已处理
IdempotentRecord record = recordMapper.selectByBusinessKey(
businessType, businessKey
);
if (record != null) {
// 已处理过
if (record.getStatus() == IdempotentStatus.SUCCESS) {
log.info("请求已处理成功,幂等返回: {}-{}", businessType, businessKey);
return IdempotentResult.alreadyProcessed(record.getResponseData());
} else if (record.getStatus() == IdempotentStatus.PROCESSING) {
log.warn("请求正在处理中: {}-{}", businessType, businessKey);
return IdempotentResult.processing();
} else {
log.warn("请求之前处理失败: {}-{}", businessType, businessKey);
// 允许重试
}
}
// 2. 记录请求(状态:处理中)
record = new IdempotentRecord();
record.setBusinessType(businessType);
record.setBusinessKey(businessKey);
record.setRequestId(UUID.randomUUID().toString());
record.setRequestData(requestData);
record.setStatus(IdempotentStatus.PROCESSING);
record.setCreateTime(new Date());
record.setUpdateTime(new Date());
try {
recordMapper.insert(record);
return IdempotentResult.newRequest(record.getId());
} catch (DuplicateKeyException e) {
// 并发插入,说明正在处理
log.warn("并发请求,已有其他线程处理: {}-{}", businessType, businessKey);
return IdempotentResult.processing();
}
}
/**
* 更新处理结果
*/
public void updateResult(Long recordId, boolean success, String responseData) {
IdempotentStatus status = success ?
IdempotentStatus.SUCCESS : IdempotentStatus.FAILED;
recordMapper.updateStatus(recordId, status, responseData);
}
}
/**
* 订单服务(使用去重表)
*/
@Service
public class OrderIdempotentService {
@Autowired
private IdempotentRecordService recordService;
@Autowired
private OrderService orderService;
/**
* 创建订单(幂等)
*/
public Order createOrderIdempotent(CreateOrderRequest request) {
String businessType = "ORDER_CREATE";
String businessKey = request.getOrderNo();
String requestData = JSON.toJSONString(request);
// 1. 检查并记录
IdempotentResult result = recordService.checkAndRecord(
businessType, businessKey, requestData
);
if (result.isAlreadyProcessed()) {
// 已处理,返回之前的结果
return JSON.parseObject(result.getResponseData(), Order.class);
}
if (result.isProcessing()) {
// 正在处理,返回提示
throw new BusinessException("请求正在处理中,请稍后查询");
}
// 2. 执行业务逻辑
Order order = null;
try {
order = orderService.createOrder(request);
// 3. 记录成功结果
recordService.updateResult(
result.getRecordId(),
true,
JSON.toJSONString(order)
);
return order;
} catch (Exception e) {
// 4. 记录失败结果
recordService.updateResult(
result.getRecordId(),
false,
e.getMessage()
);
throw e;
}
}
}
方案5:分布式锁 ⭐⭐⭐
适合高并发场景!
/**
* 使用分布式锁保证幂等性
*/
@Service
public class OrderLockService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderService orderService;
/**
* 创建订单(使用分布式锁)
*/
public Order createOrderWithLock(CreateOrderRequest request) {
String orderNo = request.getOrderNo();
String lockKey = "order:create:lock:" + orderNo;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待10秒,锁30秒后自动释放
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后重试");
}
// 双重检查:订单是否已存在
Order existingOrder = orderService.getByOrderNo(orderNo);
if (existingOrder != null) {
log.info("订单已存在,幂等返回: {}", orderNo);
return existingOrder;
}
// 创建订单
Order order = orderService.createOrder(request);
log.info("订单创建成功: {}", orderNo);
return order;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("创建订单失败", e);
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
🎯 实战场景
场景1:支付回调幂等
/**
* 支付回调处理(幂等)
*/
@RestController
@RequestMapping("/payment/callback")
public class PaymentCallbackController {
@Autowired
private IdempotentRecordService recordService;
@Autowired
private PaymentService paymentService;
/**
* 支付宝回调
*/
@PostMapping("/alipay")
public String alipayCallback(@RequestBody String requestBody) {
// 解析回调参数
Map<String, String> params = parseParams(requestBody);
String outTradeNo = params.get("out_trade_no"); // 商户订单号
String tradeNo = params.get("trade_no"); // 支付宝交易号
// 幂等处理
String businessType = "ALIPAY_CALLBACK";
String businessKey = tradeNo; // 使用支付宝交易号作为唯一标识
IdempotentResult result = recordService.checkAndRecord(
businessType, businessKey, requestBody
);
if (result.isAlreadyProcessed()) {
log.info("支付回调已处理: {}", tradeNo);
return "success"; // 返回成功,避免支付宝重复回调
}
if (result.isProcessing()) {
log.warn("支付回调正在处理: {}", tradeNo);
return "processing";
}
try {
// 验证签名
boolean signVerified = verifySign(params);
if (!signVerified) {
throw new BusinessException("签名验证失败");
}
// 处理支付结果
paymentService.handlePaymentCallback(outTradeNo, tradeNo, params);
// 记录成功
recordService.updateResult(result.getRecordId(), true, "success");
return "success";
} catch (Exception e) {
log.error("处理支付回调失败: {}", tradeNo, e);
recordService.updateResult(result.getRecordId(), false, e.getMessage());
return "fail";
}
}
}
场景2:MQ消息幂等消费
/**
* 订单消息消费者(幂等)
*/
@Component
public class OrderMessageConsumer {
@Autowired
private IdempotentRecordService recordService;
@Autowired
private OrderService orderService;
/**
* 消费订单创建消息
*/
@RabbitListener(queues = "order.create.queue")
public void handleOrderCreateMessage(Message message) {
String messageId = message.getMessageProperties().getMessageId();
String body = new String(message.getBody());
// 幂等检查
String businessType = "ORDER_CREATE_MQ";
String businessKey = messageId; // 使用消息ID作为唯一标识
IdempotentResult result = recordService.checkAndRecord(
businessType, businessKey, body
);
if (result.isAlreadyProcessed()) {
log.info("消息已处理: {}", messageId);
return; // 幂等:直接返回,不重复处理
}
if (result.isProcessing()) {
log.warn("消息正在处理: {}", messageId);
// 暂时不ACK,等待下次重试
throw new AmqpRejectAndDontRequeueException("消息正在处理中");
}
try {
// 解析消息
CreateOrderRequest request = JSON.parseObject(body, CreateOrderRequest.class);
// 创建订单
Order order = orderService.createOrder(request);
// 记录成功
recordService.updateResult(
result.getRecordId(),
true,
JSON.toJSONString(order)
);
log.info("订单创建成功: messageId={}, orderNo={}",
messageId, order.getOrderNo());
} catch (Exception e) {
log.error("处理订单消息失败: {}", messageId, e);
recordService.updateResult(result.getRecordId(), false, e.getMessage());
throw e;
}
}
}
📊 方案对比
| 方案 | 实现难度 | 性能 | 可靠性 | 适用场景 | 推荐度 |
|---|---|---|---|---|---|
| 数据库唯一约束 | ⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 有业务唯一标识 | ⭐⭐⭐⭐⭐ |
| Token机制 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 表单提交 | ⭐⭐⭐⭐⭐ |
| 状态机 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 状态流转 | ⭐⭐⭐⭐ |
| 去重表 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 通用 | ⭐⭐⭐⭐ |
| 分布式锁 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | 高并发 | ⭐⭐⭐ |
🎉 总结
选择建议
场景1:订单创建
方案:数据库唯一约束(订单号)
理由:简单可靠
场景2:表单提交
方案:Token机制
理由:防止重复提交
场景3:订单支付
方案:状态机 + 乐观锁
理由:明确的状态流转
场景4:支付回调
方案:去重表(交易流水号)
理由:需要记录处理结果
场景5:MQ消费
方案:去重表(消息ID)
理由:消息可能重复投递
场景6:秒杀扣库存
方案:分布式锁 + Redis
理由:高并发场景
记忆口诀
幂等设计很重要,
重复执行不出错。
五种方案要记牢,
场景不同方案选。
数据库唯一约束强,
订单创建最常用。
业务标识要唯一,
简单可靠又高效。
Token机制防重提,
获取令牌再操作。
一次使用就删除,
表单提交必须用。
状态机思想好,
有序流转不混乱。
乐观锁来保证,
并发更新不出错。
去重表专门用,
记录请求和结果。
支付回调MQ消费,
都可以用去重表。
分布式锁要谨慎,
性能开销要考虑。
高并发短事务,
才适合用分布式锁!
愿你的系统幂等性完美,重复请求从此无忧! 🔄✨