幂等模式:让系统经得起重复调用
各位同学,今天我们来聊一个非常接地气的话题——幂等。说白了,幂等就是同一个操作,你调用一次和调用一百次,结果是一样的。你想想看,在分布式系统里,网络超时、重试、消息重复消费,这些场景太常见了。如果没有幂等保护,用户点一次支付按钮,结果扣了两次钱,那可就出大事了。
我在做支付系统的时候,就遇到过这种问题。有一次线上排查,发现某个订单被重复扣款了,查了半天,原来是回调接口被重复调用了两次。从那以后,我对幂等的重视程度直接拉满。今天我就把幂等模式的几种核心实现方式,以及支付和订单系统里的实战经验,一次性讲清楚。
什么是幂等?先看一个简单例子
幂等(Idempotency)这个概念来自数学,但在工程里,它的含义更具体:一个操作,无论执行多少次,产生的副作用都是一样的。
举个例子:
- 查询订单状态:天然幂等,查一万次也不会改数据
- 创建订单:不幂等,调两次就创建两个订单
- 更新订单状态为“已支付”:如果加状态判断,可以做到幂等
嗯,这里要注意:幂等不是天然存在的,是需要设计的。你写接口的时候,默认是不幂等的,除非你刻意去实现它。
幂等模式的三种核心实现
我个人习惯把幂等实现分为三类:幂等键、去重表、状态机。下面我一个一个讲。
1. 幂等键(Idempotency Key)
这是最常用的一种方式。客户端在请求时,带一个全局唯一的幂等键(比如 UUID)。服务端收到请求后,先查一下这个幂等键有没有处理过。如果处理过,直接返回上次的结果;如果没有,就正常处理,并把结果缓存起来。
我在项目中遇到过这样一个场景:支付网关的回调接口,第三方会重试多次。我们让每次回调都带上一个唯一的幂等键(比如支付流水号),服务端用 Redis 做缓存,key 就是幂等键,value 是处理结果。这样重复回调来了,直接返回缓存结果,不会重复处理。
代码示例(Java 伪代码):
public Response handlePayment(PaymentRequest request) {
String idempotencyKey = request.getIdempotencyKey();
// 1. 查缓存,看是否处理过
Response cached = cache.get(idempotencyKey);
if (cached != null) {
return cached; // 直接返回上次结果
}
// 2. 加分布式锁,防止并发
boolean locked = redisLock.tryLock(idempotencyKey, 5, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("请稍后重试");
}
try {
// 3. 再次检查(双重检测)
cached = cache.get(idempotencyKey);
if (cached != null) {
return cached;
}
// 4. 执行业务逻辑
Response result = doPayment(request);
// 5. 缓存结果
cache.set(idempotencyKey, result, 24, TimeUnit.HOURS);
return result;
} finally {
redisLock.unlock(idempotencyKey);
}
}
2. 去重表(Deduplication Table)
去重表本质上和幂等键类似,但它是用数据库的唯一索引来保证的。你建一张去重表,里面放幂等键字段,并加上唯一索引。插入时如果冲突,说明已经处理过了。
这种方式适合对数据一致性要求极高的场景。比如订单系统里,可以用订单号作为去重键。我第一次做订单系统时,就用了这种方式,简单粗暴,但非常有效。
表结构示例:
CREATE TABLE idempotency_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
idempotency_key VARCHAR(64) NOT NULL UNIQUE, -- 唯一索引
result TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Java 代码示例:
@Transactional
public Response createOrder(CreateOrderRequest request) {
String key = request.getOrderNo();
// 尝试插入去重记录
try {
idempotencyRecordDao.insert(new IdempotencyRecord(key, null));
} catch (DuplicateKeyException e) {
// 已存在,说明重复请求
return idempotencyRecordDao.getResultByKey(key);
}
// 执行业务逻辑
Order order = doCreateOrder(request);
// 更新结果
idempotencyRecordDao.updateResultByKey(key, order);
return Response.success(order);
}
3. 状态机(State Machine)
状态机是另一种优雅的幂等实现。它通过限制状态的流转方向,来保证同一个操作不会重复执行。比如订单状态:待支付 → 已支付 → 已发货 → 已完成。每个状态只能向前流转,不能回退。
你想想看,如果订单已经是“已支付”状态,你再调用“支付”接口,状态机发现当前状态不允许再执行“支付”操作,就会直接返回成功,而不会重复扣款。
状态机实现的核心是:每个操作都定义允许的当前状态。比如:
- 支付操作:只允许在“待支付”状态执行
- 发货操作:只允许在“已支付”状态执行
- 确认收货:只允许在“已发货”状态执行
代码示例(Java 状态机):
public enum OrderStatus {
PENDING_PAYMENT,
PAID,
SHIPPED,
COMPLETED,
CANCELLED;
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new HashMap<>();
static {
TRANSITIONS.put(PENDING_PAYMENT, Set.of(PAID, CANCELLED));
TRANSITIONS.put(PAID, Set.of(SHIPPED, CANCELLED));
TRANSITIONS.put(SHIPPED, Set.of(COMPLETED));
TRANSITIONS.put(COMPLETED, Set.of());
TRANSITIONS.put(CANCELLED, Set.of());
}
public boolean canTransitionTo(OrderStatus target) {
return TRANSITIONS.getOrDefault(this, Collections.emptySet()).contains(target);
}
}
// 使用
public Response payOrder(Long orderId) {
Order order = orderDao.getById(orderId);
if (!order.getStatus().canTransitionTo(OrderStatus.PAID)) {
// 状态不允许,直接返回成功(幂等)
return Response.success("订单已支付");
}
// 执行支付逻辑
doPay(order);
order.setStatus(OrderStatus.PAID);
orderDao.update(order);
return Response.success("支付成功");
}
支付与订单系统的幂等设计实战
好了,理论讲完了,我们来看看实际系统里怎么用。支付和订单系统是幂等设计的重灾区,我直接说几个关键点。
支付系统的幂等设计
支付系统里,最怕的就是重复扣款。我总结了几条铁律:
- 支付请求必须带幂等键:通常用支付流水号或订单号。服务端用幂等键去重。
- 支付回调必须幂等:第三方支付回调可能重复,服务端要用支付流水号做去重。
- 退款操作也要幂等:退款单号作为幂等键,防止重复退款。
- 状态机保护:支付状态只能从“待支付”变为“已支付”,不能从“已支付”再变一次。
我曾经遇到过一个线上事故:支付回调重复了三次,第一次成功了,第二次和第三次因为没做幂等,又执行了两次入账逻辑,导致用户账户余额多了三倍。嗯,那次事故之后,我们团队把幂等检查加到了所有支付相关的接口上。
订单系统的幂等设计
订单系统里,幂等主要体现在这几个地方:
- 创建订单:用订单号或幂等键去重,防止重复下单
- 取消订单:状态机限制,只有“待支付”或“已支付”的订单才能取消
- 修改订单:用版本号或乐观锁,防止并发修改
这里我特别想提一下乐观锁。它虽然不是严格意义上的幂等模式,但能很好地解决并发问题。比如:
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = ? AND version = ?
这样即使两个请求同时来,也只有一个能更新成功。另一个会失败,然后重试时发现状态已经变了,自然就幂等了。
一张图看懂幂等模式
下面我用一张 SVG 图,把幂等模式的三种实现和它们在支付订单系统中的应用串起来。
总结一下
幂等模式不是什么高深的技术,但它能救你的命。我做了这么多年系统,见过太多因为没做幂等而出的事故。说白了,你只要记住三点:
- 幂等键:适合接口级别的去重,用缓存实现,性能好
- 去重表:适合数据一致性要求高的场景,用数据库唯一索引
- 状态机:适合业务流程固定的场景,从设计上杜绝重复操作
在实际项目中,这三种方式经常组合使用。比如支付系统里,幂等键做第一道防线,去重表做第二道,状态机做兜底。这样即使某一层出了问题,其他层也能拦住。
嗯,今天就讲到这里。幂等模式看起来简单,但真正用好它,需要你对业务场景有深刻的理解。希望今天的分享能帮到你。
公众号:蓝海资料掘金营,微信 deep3321