幂等性设计原则:如何保证服务中任务不重复执行?

当你疯狂点击“购买”按钮,却发现自己下了 5 个相同订单;或者因为服务器延迟,你的支付重复进行了好几次…… 是不是一不小心就可能亏大了?别怕,咱们今天就来聊聊如何用幂等性策略,让你在分布式系统或高并发场景下,稳稳地“只执行一次”!


1. 什么是幂等性

幂等性(Idempotency) 是指一个操作无论执行多少次,产生的结果都是相同的,即多次执行不会对系统状态造成额外影响。

数学定义:幂等操作满足: f(f(x))=f(x),即,无论 f(x) 被调用多少次,结果都不变。

举个生活中的例子:

  • 刷牙这件事。你今天早上刷过牙,10 分钟后又去刷一次,牙应该还是干净的(前提是你没有再吃薯片!),这就是“刷了第二次并不会让牙变得更干净”——幂等!
  • 你告诉同事 “门没锁,去锁一下”;他去锁了一次,再去锁第二次,其实门状态依然是“锁着”,不会因为被锁两次就更多一把锁,这也是幂等。

在编程世界也是同理:无论是后端接口、数据库操作还是消息处理,一旦你设计好 幂等,就不用担心重复执行带来的尴尬场面。

2. 为什么我们需要幂等性?

来看看如果没有幂等,可能会发生什么糟心事:

  1. 支付重复扣费
    小明买了一张演唱会门票,点击提交订单后发现页面卡住了,就又点了一次——很可能就扣费两次!多花冤枉钱,会被气哭。
  2. 库存超卖
    在双十一大促中,库存只有 10 件的爆款商品,结果因为高并发请求超卖到了 15 件,这就非常尴尬,商家得给消费者发致歉信,还可能影响店铺评分。
  3. 重复任务调度
    某个定时任务因为服务宕机被重启,又重复把数据给跑了一遍,数据库里多了成堆的垃圾数据,老板看了直拍桌子:“怎么又重复了!”

结论:没有幂等,系统就像个“爱吃回头草”的“小马”,老是踩到同一个坑里。为了让它少撞墙,需要在关键环节加上幂等保护。

3. 幂等性如何落地?咱们来点实战!

3.1. 数据库唯一约束——给重复提交当头一棒

核心思路:利用数据库的唯一索引或主键,来保证数据只能插入一次。

场景示例:订单号
在创建订单时,设置 订单号 (orderId) 为唯一主键。

  • 第一次插入成功:数据库保存这笔订单
  • 第二次再插入同样的 orderId 时,数据库会报“重复主键错误”,从而杜绝重复订单。
  • 当应用层捕获到这个错误,可以友好地提示用户:“订单已存在,请勿重复提交”。
CREATE TABLE orders (
    order_id VARCHAR(50) PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10,2)
);

适用场景:

  1. 订单、支付记录等具有 唯一业务标识 的场景。
  2. 各种需要确保只写一次的记录,如用户注册等。

3.2. Token 防重——给每个请求发一张“唯一门票”

核心思路:客户端先请求一个唯一 Token(就像一张唯一门票),随后在操作请求时带上这张门票,服务端每看到一张门票就“作废”它,拒绝使用第二次。

幂等性设计原则:如何保证服务中任务不重复执行?_第1张图片

具体步骤:

  1. 客户端先请求 Token
  2. 客户端带 Token 调用正式接口
  3. 服务端检查并“消耗”该 Token 

             3.1 通过 Redis 的 setIfAbsent(token, "used") 来判断此 Token 是否已被用过 

             3.2 如果已被用过,说明请求重复了,直接拦截

适用场景: 表单重复提交、支付重复操作等短时间内易发生重复的业务请求。

3.3. 幂等键 (Idempotency Key) ——记住你的来路

核心思路

  • 每次请求自带一个 Idempotency-Key(可以是 UUID)。
  • 服务器记录这个 Key,一旦再次碰到相同 Key,就判定这是重复请求,直接返回第一次的处理结果。
String requestId = request.getHeader("Idempotency-Key");
if (redis.exists(requestId)) {
    // 重复请求,直接返回之前的处理结果
    return redis.get(requestId);
} else {
    // 首次处理,逻辑执行完后,存储 result 到 redis
    redis.set(requestId, result, 10, TimeUnit.MINUTES);
}

适用场景:对外提供的 API,需要保证相同请求只处理一次。比如第三方支付系统常用这个方法,API 保证幂等

3.4. MQ 幂等消费——消息来了不要慌,再检查一下

在消息队列 (Kafka、RabbitMQ、RocketMQ) 中,消息重复投递是一种常见现象。消费者需要自行去重:

  1. 基于业务主键(比如订单号)检查是否处理过。
  2. 将已处理过的消息 ID 缓存或写进数据库。
String messageId = msg.getMessageId();
if (redis.exists(messageId)) {
    // 该消息已消费过,直接丢弃
    return;
}
processMessage(msg);  // 执行业务逻辑
redis.set(messageId, "done", 10, TimeUnit.MINUTES);

适用场景:

  1. 支付扣款消息:一旦扣款成功,不能再扣一次。
  2. 订单状态变更:不能把一个订单变更几回。
  3. 库存变动:不能重复扣减库存。

3.5. 分布式锁——同一时间“只准一个人做事”

当多个线程或微服务都要操作同一份资源(比如扣减库存),我们可以使用分布式锁来避免在执行前就发生重叠操作。

boolean lock = redis.setIfAbsent("lock:inventory:123", "1", 5, TimeUnit.SECONDS);
if (!lock) {
    throw new RuntimeException("有其他进程正在处理,请稍后重试!");
}
// 如果获取到了锁,执行业务逻辑
updateInventory("123", 10);
redis.delete("lock:inventory:123");

适用场景: 高并发的场景,比如 抢购秒杀、秒杀订单,防止库存被瞬间减多次。

4. 分布式环境下的坑:稳住阵脚,小心这几招

  • 跨服务调用要全链路幂等

      比如订单服务 A -> 库存服务 B -> 支付服务 C,如果 A 重试了两次,就有可能让 B、C 也执行两次。必须在每个服务之间都带上一个幂等标识,哪怕调用链路中途失败,也能保障只执行一次。

  • 异步消息一定要去重

       使用消息队列时,尤其要做好 消息幂等消费,否则就会出现再发一次消息就会再次扣钱或者再次更新库存的严重问题。

  • 数据库操作要注意原子性

     在做幂等判定时,如果需要同时更新多张表,要保证它们要么同时成功,要么同时失败。可以借助事务或分布式事务框架(比如 Seata)去解决。

5. 结语

幂等性就像给系统装了一把“时光机”:它让我们在面对重复请求、重复消息时,能回到正确的状态,避免二次伤害。

  • 用 数据库唯一索引、Token 防重、幂等键、MQ 去重、分布式锁等手段,根据你的业务场景来选。
  • 别忘了在分布式调用、消息消费等场景,都要把幂等设计纳入考量。

只要设计得好,重复请求不再是大魔王!你的用户和老板会爱上这种“只扣一次费、只生成一次订单、只更新一次状态”的安全感!

祝各位在幂等之路上一路披荆斩棘,构建更可靠、更稳定的系统。下次见啦!

如果觉得这篇分享对你有帮助,欢迎点赞、留言或者分享给你的同事和朋友~

你可能感兴趣的:(项目相关,oracle,数据库)