别让用户“剁手”把你搞破产:接口幂等性与防重的终极防线

别让用户"剁手"把你搞破产:接口幂等性与防重的终极防线 🛡️

各位"背锅侠"们,大家好。👋

咱们做后端的,最怕的不是代码写不出来,而是用户手太快。💸

尤其是做支付、下单、抽奖这类业务。用户因为网络卡顿,手指在屏幕上疯狂输出连点两下,结果你这边没防住,给他发了两份奖品,或者扣了两次钱。第二天财务找上门,你就准备收拾工位吧。🧳

今天咱们就聊聊怎么给用户这双"快手"戴上镣铐:接口幂等性 与请求防重。


1. 概念纠偏:防重 ≠ 幂等 🔄

很多同学喜欢把这俩混为一谈,其实差别大了去了:

  • 防重(Anti-Duplication) :针对请求。这个请求我只收一次,第二次直接扔掉。不管你后面怎么样,我不处理。
  • 幂等(Idempotency) :针对结果 。你发我一百次同样的请求,我处理一百次,但结果 和副作用跟处理一次是一样的(钱只扣一次)。

举个例子:

  • 防重:你重复提交入职申请,HR 系统直接提示"请勿重复提交"。
  • 幂等:你重复发起转账 100 元,系统只扣你 100 元,不会因为发了两次请求就扣 200。

2. 落地实战:防重方案(拒绝"连点怪") 🚫

防重的核心在于:识别唯一请求。

方案一:前端节流(第一道防线)

这是给小白看的,但必须做。按钮点击后置灰,或者 loading状态。

缺点:浏览器刷新、脚本模拟请求,直接绕过。

方案二:Redis SetNX(分布式锁思路)

这是最常用、最有效的手段。

核心逻辑:利用 Redis 的单线程特性,在接口处理前,先去占个坑。

Key 设计 :req:{userId}:{action}:{uniqueToken}

typescript 复制代码
// 伪代码:防重拦截器
@Component
public class IdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从 Header 或 Token 中获取用户唯一标识
        String userId = request.getHeader("X-User-Id");
        
        // 2. 获取客户端生成的唯一请求 ID (非常重要!)
        String requestId = request.getHeader("X-Request-Id");
        
        // 如果没有,说明是非法请求或爬虫,直接拦截
        if (StringUtils.isBlank(requestId)) {
            throw new RuntimeException("缺少请求指纹!🚨");
        }

        String key = "req:lock:" + userId + ":" + requestId;

        // 3. SetNX:如果 Key 不存在才设置,存在则返回 false
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.SECONDS);
        
        if (Boolean.FALSE.equals(success)) {
            // 请求重复了,直接返回,别进 Controller 了
            throw new RuntimeException("您点得太快了,慢点儿~ 🐢");
        }
        return true;
    }
}

专家提示 :X-Request-Id最好由前端生成(UUID),这样能覆盖用户在不同设备上的重复操作。


3. 落地实战:幂等方案(结果不变) 🔑

防重是挡在门外,幂等是进门后的保险箱。

方案一:数据库唯一约束(最硬核)

这是金融级方案。

表设计:

sql 复制代码
CREATE TABLE `order` (
  `id` bigint NOT NULL,
  `order_no` varchar(64) NOT NULL,
  `biz_id` varchar(64) NOT NULL COMMENT '业务唯一ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_biz_id` (`biz_id`) -- 核心在这里!
);

逻辑:

业务方传入一个 biz_id(比如支付流水号)。即使请求过来两次,数据库会因为唯一键冲突插入失败。你的代码捕获 DuplicateKeyException,直接返回"操作成功"即可。

方案二:状态机幂等(适合订单流转)

订单状态从 CREATED-> PAID。

typescript 复制代码
// 伪代码:更新订单状态
@Transactional
public void payOrder(String orderNo) {
    int rows = orderMapper.updateStatus(
        "PAID", 
        orderNo, 
        Arrays.asList("CREATED") // 只有 CREATED 状态才能更新
    );
    
    if (rows == 0) {
        // 说明要么订单不存在,要么已经不是 CREATED 了(已经付过款了)
        // 此时直接返回成功,这就是幂等
        log.info("订单已处理或状态异常,直接返回成功");
        return;
    }
    // 扣减库存...
}

核心 :UPDATE table SET status = 'NEW' WHERE status = 'OLD' AND id = ?。利用 SQL 的行锁保证原子性。


4. 高级玩法:基于 Token 的预校验机制 🎫

这是很多大厂(如阿里、京东)用的方案。

  1. 申请 Token:在用户进入页面时,后端生成一个唯一 Token,存入 Redis,并返回给前端。

  2. 提交携带:用户提交请求时,带上这个 Token。

  3. 校验删除 :后端使用 Lua 脚本​ 原子性地检查并删除 Redis 中的 Token。

    • 删成功 = 第一次请求,放行。
    • 删失败 = 重复请求,拦截。

为什么用 Lua?

因为"查"和"删"必须是原子操作,否则在高并发下,两个线程同时查到 Token 存在,都会去处理。

vbnet 复制代码
-- Lua 脚本:原子校验
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

5. 总结:资深工程师的 CheckList ✅

为了体现你的专业度,下次评审时拿出这张表:

场景 推荐方案 核心原理 适用业务
表单提交/点赞​ Redis SetNX​ 抢占锁,过期失效 防止用户连点
支付/转账​ 唯一索引 (UK) ​ 数据库层的最终裁决 金融交易,绝对防重
订单状态变更​ 乐观锁/状态机​ Where status = ? 订单流转
复杂流程​ Token 机制​ 一次性验证码 秒杀、高并发写

最后的碎碎念 💭

千万不要依赖前端! ​ 永远不要相信用户的网络环境。哪怕前端做了防重,后端也必须有一道 Redis 或 DB 级别的防线。

毕竟,代码写得好,老板回家早。要是真因为没做幂等赔了钱,那可真是"一顿操作猛如虎,一看工资二千五"了。😂

大家如果有更好的方案,或者在生产环境中踩过什么"幂等"的大坑,欢迎在评论区交流,咱们一起填坑!👇

相关推荐
IT_陈寒1 小时前
Java中equals方法比了个寂寞?原来这才是正确的重写姿势
前端·人工智能·后端
Thneonl1 小时前
Celery 生产踩坑:1000 任务积压与 acks_late 双重执行
后端·python
卷福同学1 小时前
第一次当面试官有感
后端·面试
苏三说技术1 小时前
为什么越来越多人用 OnlyOffice?
后端
知守观1 小时前
@Transactional 事务失效排查,try-catch 吞异常导致回滚失败(附源码分析)
后端·spring
羑悻1 小时前
Codex + Seed-2.1-pro 实测:多模态理解 + Coding Agent 能扛住真实仓库吗?
后端
颜进强1 小时前
14 · NestJS ExecutionContext 执行上下文:守卫、拦截器、过滤器拿到的"同一个 context",为什么能力不一样?
前端·后端·ai编程
小小张说故事1 小时前
Python 多线程为什么跑不快?asyncio 入门指南:异步并发从零上手
后端·python
明月_清风1 小时前
数据进入平台后怎么处理?一文搞懂 ETL
大数据·后端·数据分析
初学AI的小高1 小时前
LangGraph断点恢复与幂等执行实战
后端·架构