大数跨境
0
0

SpringBoot接口幂等性方案:4种策略+代码实战,告别重复提交

SpringBoot接口幂等性方案:4种策略+代码实战,告别重复提交 跨境Emily
2025-11-05
13

来源:程序语言

👉 欢迎加入小哈的星球,你将获得: 专属的项目实战(多个项目) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《Spring AI 项目实战》正在更新中..., 基于 Spring AI + Spring Boot 3.x + JDK 21;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍演示地址:http://116.62.199.48:7070/
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;
  • 专栏阅读地址:https://www.quanxiaoha.com/column

截止目前,累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有4000+小伙伴加入



  • 背景
  • 1.Token 令牌 —— 最经典、最稳
  • 2.数据库唯一索引 —— 低成本、强一致
  • 3.分布式锁 —— 高并发大杀器
  • 4.请求内容摘要 —— 最透明、最通用

图片

背景

网络抖动、用户手抖、MQ 重试……任何一次“重复请求”都可能让钱 扣钱、商品多发 。

今天一次讲透 4 种常用幂等性实现,附可 copy 的代码 。

1.Token 令牌 —— 最经典、最稳

思路: 先拿令牌 → 再执行业务 → 用完即焚。

核心关键字: 预生成、一次性、Redis 原子删除。

精简代码(含注解)

@RestController
@RequestMapping("/order")
publicclassOrderController {

    @Autowired
    private StringRedisTemplate redis;

    // ① 预生成 Token,给前端
    @GetMapping("/token")
    public String getToken() {
        Stringtoken= UUID.randomUUID().toString();
        // 10 分钟有效期,足够前端完成下单
        redis.opsForValue().set("tk:" + token, "1", Duration.ofMinutes(10));
        return token;
    }

    // ② 下单接口,Header 中带令牌
    @PostMapping
    public Result create(@RequestHeader("Idempotent-Token") String token,
                         @RequestBody OrderReq req) {
        Stringkey="tk:" + token;
        // 原子删除:成功返回 true 表示第一次使用
        Booleanfirst= redis.delete(key);
        if (Boolean.FALSE.equals(first)) {
            return Result.fail("**请勿重复下单**");
        }
        // 真正创建订单
        Orderorder= orderService.create(req);
        return Result.ok(order);
    }
}

注解:

  • UUID 保证全局唯一,Redis TTL 防呆。
  • delete 是原子操作,天然并发安全。
  • 用 Header 传递令牌,保持接口语义纯净。

2.数据库唯一索引 —— 低成本、强一致

思路: 把“业务唯一键”做成唯一索引,重复写直接抛异常。

核心关键字: 天然幂等、异常即幂等。

代码示例

@Entity
@Table(name = "t_payment",
       uniqueConstraints = @UniqueConstraint(columnNames = "transaction_id"))
publicclassPayment {
    @Id
    private Long id;

    // 支付平台返回的流水号
    @Column(name = "transaction_id")
    private String txId;

    private BigDecimal amount;
    private String status;
}

@Service
publicclassPayService {
    @Autowired
    private PaymentRepo repo;

    public Result pay(PayReq req) {
        try {
            Paymentp=newPayment();
            p.setTxId(req.getTxId());
            p.setAmount(req.getAmount());
            p.setStatus("SUCCESS");
            repo.save(p);   // 重复就抛 DataIntegrityViolationException
            return Result.ok("**支付成功**");
        } catch (DataIntegrityViolationException e) {
            // 异常即查询结果,避免重复扣款
            Paymentexist= repo.findByTxId(req.getTxId());
            return Result.ok("**已支付**", exist.getId());
        }
    }
}

注解:

  • 唯一索引兜底,数据库层面 100 % 防重。
  • try-catch 把异常转成正常响应,用户体验丝滑。
  • 无外部依赖,适配老旧系统也毫无压力。

3.分布式锁 —— 高并发大杀器

思路: 对“订单号 / 用户ID”加分布式锁,抢到锁再干活。

核心关键字: 互斥、超时、可重入。

Redisson 简洁版

@Service
publicclassStockService {

    @Autowired
    private RedissonClient redisson;

    public Result deduct(DeductCmd cmd) {
        StringlockKey="lock:stock:" + cmd.getProductId();
        RLocklock= redisson.getLock(lockKey);
        try {
            // 最多等 3 秒,持锁 5 秒自动释放
            if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                return Result.fail("**处理中,请稍后**");
            }
            // 业务幂等检查:根据请求 ID 查记录
            if (repo.existsByRequestId(cmd.getRequestId())) {
                return Result.ok("**已扣减**");
            }
            // 真正扣库存
            repo.deductStock(cmd);
            return Result.ok("**扣减成功**");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.fail("**系统繁忙**");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

注解:

  • Redisson 自带 watchdog,自动续期,不怕死锁。
  • requestId 做幂等表,锁+唯一索引 双保险。
  • lock.isHeldByCurrent线程 防止误删别人的锁。

4.请求内容摘要 —— 最透明、最通用

思路: 把请求体做 MD5/SHA256,作为幂等键。

核心关键字: 零额外交互、客户端无感。

自定义注解 + AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface Idempotent {
    intexpire()default3600;   // 秒
}

@Aspect
@Component
publicclassIdempotentAspect {

    @Autowired
    private StringRedisTemplate redis;

    @Around("@annotation(idem)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idem)throws Throwable {
        HttpServletRequestreq= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // ① 计算请求体摘要
        Stringbody= IOUtils.toString(req.getReader());
        Stringdigest= DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8));
        Stringkey="idem:digest:" + digest;

        // ② 第一次:setIfAbsent 返回 true
        Booleanabsent= redis.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(idem.expire()));
        if (Boolean.FALSE.equals(absent)) {
            return Result.fail("**重复请求**");
        }
        try {
            return pjp.proceed();
        } catch (Exception e) {
            redis.delete(key);  // 异常时释放,允许重试
            throw e;
        }
    }
}

// 使用
@RestController
publicclassTransferApi {
    @PostMapping("/transfer")
    @Idempotent(expire = 7200)
    public Result transfer(@RequestBody TransferCmd cmd) {
        return Result.ok(transferSvc.doTransfer(cmd));
    }
}

注解:

  • MD5 把任意长度报文压缩成 32 位,冲突概率极低。
  • setIfAbsent 保证原子性,异常回删避免误杀。
  • 注解 + AOP 零侵入,老接口 1 行代码即可拥有幂等。

小结

方案
延迟
复杂度
外部依赖
适用场景
Token
Redis
有预生成环节:下单、支付
唯一索引
支付、注册
分布式锁
Redis/ZK
高并发抢券、秒杀
内容摘要
Redis
无预生成:转账、回调

实施清单

  • 检查你的核心接口有没有唯一业务键。
  • 优先使用数据库唯一索引,成本最低。
  • 并发量高再上分布式锁或Token,不要过度设计。
  • 给关键接口加上监控告警,幂等失败时第一时间知道。

👉 欢迎加入小哈的星球,你将获得: 专属的项目实战(多个项目) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《Spring AI 项目实战》正在更新中..., 基于 Spring AI + Spring Boot 3.x + JDK 21;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍演示地址:http://116.62.199.48:7070/
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/;
  • 专栏阅读地址:https://www.quanxiaoha.com/column

截止目前,累计输出 100w+ 字,讲解图 4013+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有4000+小伙伴加入


    
       

1. 我的私密学习小圈子,从0到1手撸企业实战项目~

2. 全网最全的 Jenkins + Maven + Git 自动化部署指南!

3. 聊聊防御式编程

4. 得物面试:Redis 内存碎片是什么?如何清理?

最近面试BAT,整理一份面试资料Java面试BATJ通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。

获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。

PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下在看,加个星标,这样每次新文章推送才会第一时间出现在你的订阅列表里。

“在看”支持小哈呀,谢谢啦

【声明】内容源于网络
0
0
跨境Emily
跨境分享录 | 持续输出实用内容
内容 44655
粉丝 3
跨境Emily 跨境分享录 | 持续输出实用内容
总阅读245.4k
粉丝3
内容44.7k