别再让接口乱跑!SpringBoot实现接口幂等性的四大实战方案,彻底告别重复提交!

本文将带你深入拆解 Spring Boot 实现接口幂等性的 4 种主流方案, 覆盖从“轻量级本地防重”到“分布式高并发控制”,并结合 实战级代码 展示落地细节。

在分布式系统中,重复请求是最隐蔽的业务炸弹。 用户手抖、网络抖动、支付回调、消息队列重试…… 任意一次“重复操作”,都有可能导致 重复扣款、重复发货、数据异常。

本文将带你深入拆解 Spring Boot 实现接口幂等性的 4 种主流方案, 覆盖从“轻量级本地防重”到“分布式高并发控制”,并结合 实战级代码 展示落地细节。

接下来,我们将逐步拆解四大方案:

  1. Token令牌机制 —— 经典且稳
  2. 数据库唯一索引 —— 简洁又强一致
  3. 分布式锁机制 —— 并发场景的核心武器
  4. 请求内容摘要 —— 最通用、最透明
Token 令牌机制:最经典的防重手段

核心思想

“先拿令牌 → 再执行业务 → 用完即焚”

通过在请求前生成一次性令牌(Token),在执行接口时验证并原子删除,保证每个请求只被处理一次。

代码示例

路径:/src/main/java/com/icoderoad/order/OrderController.java

package com.icoderoad.order;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.UUID;


@RestController
@RequestMapping("/order")
public class OrderController {


    @Autowired
    private StringRedisTemplate redis;


    // ① 预生成 Token,供前端使用
    @GetMapping("/token")
    public String getToken() {
        String token = UUID.randomUUID().toString();
        redis.opsForValue().set("tk:" + token, "1", Duration.ofMinutes(10));
        return token;
    }


    // ② 下单接口,Header 中携带令牌
    @PostMapping
    public Result create(@RequestHeader("Idempotent-Token") String token,
                         @RequestBody OrderReq req) {
        String key = "tk:" + token;
        Boolean first = redis.delete(key);
        if (Boolean.FALSE.equals(first)) {
            return Result.fail("请勿重复下单");
        }
        Order order = orderService.create(req);
        return Result.ok(order);
    }
}

要点解析:

  • UUID 生成全局唯一 Token;
  • Redis 设置 TTL(10分钟)避免缓存堆积;
  • delete() 是原子操作,可安全防重;
  • Header 传递令牌,保持接口语义清晰。

数据库唯一索引:最低成本的幂等保证

核心思想:

“唯一键 + 异常即幂等”

通过数据库层面的 唯一索引,让重复请求在插入时直接报错,天然具备幂等特性。

代码示例

路径:/src/main/java/com/icoderoad/payment/PayService.java

package com.icoderoad.payment;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;


import javax.persistence.*;
import java.math.BigDecimal;


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


    @Column(name = "transaction_id")
    private String txId;


    private BigDecimal amount;
    private String status;
}


@Service
public class PayService {


    @Autowired
    private PaymentRepo repo;


    public Result pay(PayReq req) {
        try {
            Payment p = new Payment();
            p.setTxId(req.getTxId());
            p.setAmount(req.getAmount());
            p.setStatus("SUCCESS");
            repo.save(p);
            return Result.ok("支付成功");
        } catch (DataIntegrityViolationException e) {
            Payment exist = repo.findByTxId(req.getTxId());
            return Result.ok("已支付", exist.getId());
        }
    }
}

要点解析:

  • uniqueConstraints 确保事务级防重;
  • 异常捕获后直接返回幂等响应;
  • 无需外部依赖,兼容老旧系统。

分布式锁机制:高并发下的“互斥利器”

核心思想:

“对关键资源加锁,谁抢到谁执行”

在并发操作中通过 Redisson 或 Zookeeper 实现互斥访问,保障同一用户或订单只被处理一次。

代码示例

路径:/src/main/java/com/icoderoad/stock/StockService.java

package com.icoderoad.stock;


import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


import java.util.concurrent.TimeUnit;


@Service
public class StockService {


    @Autowired
    private RedissonClient redisson;
    @Autowired
    private StockRepo repo;


    public Result deduct(DeductCmd cmd) {
        String lockKey = "lock:stock:" + cmd.getProductId();
        RLock lock = redisson.getLock(lockKey);


        try {
            if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                return Result.fail("处理中,请稍后");
            }
            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();
            }
        }
    }
}

要点解析:

  • tryLock 避免线程永久阻塞;
  • Redisson 自动续期机制防止死锁;
  • requestId 与唯一索引配合,形成“双保险”;
  • 适合秒杀、库存、并发下单等高频场景。

请求内容摘要:最透明的零侵入方案

核心思想:

“以请求内容为幂等标识,天然适配所有接口”

将请求体生成 MD5/SHA256摘要 作为幂等键,通过 Redis 进行原子性验证,真正做到“客户端无感”。

代码示例

路径:/src/main/java/com/icoderoad/common/aop/IdempotentAspect.java

package com.icoderoad.common.aop;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.apache.commons.io.IOUtils;


import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.time.Duration;


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    int expire() default 3600; // 秒
}


@Aspect
@Component
public class IdempotentAspect {


    @Autowired
    private StringRedisTemplate redis;


    @Around("@annotation(idem)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idem) throws Throwable {
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String body = IOUtils.toString(req.getReader());
        String digest = DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8));
        String key = "idem:digest:" + digest;


        Boolean absent = 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;
        }
    }
}

    使用示例:

    要点解析:

    • 使用 MD5 压缩请求体,确保唯一性;
    • setIfAbsent 保证 Redis 原子操作;
    • 异常回滚防止误判;
    • 注解 + AOP 实现零侵入式幂等控制。
    方案对比与落地建议

    方案类型

    实现复杂度

    外部依赖

    典型场景

    Token令牌

    中等

    Redis

    下单、支付、表单提交

    唯一索引

    注册、支付回调

    分布式锁

    中高

    Redis/ZK

    秒杀、库存扣减

    内容摘要

    Redis

    转账、接口回调

    结语:幂等性不是装饰,而是底线

    幂等控制是后端架构中防止业务灾难的安全阀。 选择方案时请遵循以下三条原则:

    1. 先业务分析,再加锁 —— 能靠唯一键解决的,不必上分布式锁;
    2. 核心路径必防重 —— 特别是支付、库存、转账等资金相关接口;
    3. 幂等监控要同步上线 —— 及时发现、告警、自动恢复。

    记住:幂等性不是性能开销,而是系统稳定的基石。

    从 Token 到摘要,每一种方案都有其价值, 真正的架构师,懂得“用最小的代价,守住最大的安全”。

    AI大模型学习福利

    作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

    一、全套AGI大模型学习路线

    AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

    因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

    二、640套AI大模型报告合集

    这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

    因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

    三、AI大模型经典PDF籍

    随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


    因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

    四、AI大模型商业化落地方案

    因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

    作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值