大数跨境
0
0

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

别再让接口乱跑!SpringBoot实现接口幂等性的4大实战方案,彻底告别重复提交! Owen跨境
2025-10-24
6
导读:别再让接口乱跑!SpringBoot实现接口幂等性的4大实战方案,彻底告别重复提交!在分布式系统中,重复请求是最隐蔽的业务炸弹。


别再让接口乱跑!SpringBoot实现接口幂等性的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;}
@Servicepublic 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;
@Servicepublic 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(35, 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@Componentpublic 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;        }    }}

使用示例:

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

要点解析:

  • 使用 MD5 压缩请求体,确保唯一性;

  • setIfAbsent 保证 Redis 原子操作;

  • 异常回滚防止误判;

  • 注解 + AOP 实现零侵入式幂等控制。

方案对比与落地建议

方案类型 实现复杂度 外部依赖 典型场景
Token令牌 中等 Redis 下单、支付、表单提交
唯一索引 注册、支付回调
分布式锁 中高 Redis/ZK 秒杀、库存扣减
内容摘要 Redis 转账、接口回调

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

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

  1. 先业务分析,再加锁 —— 能靠唯一键解决的,不必上分布式锁;

  2. 核心路径必防重 —— 特别是支付、库存、转账等资金相关接口;

  3. 幂等监控要同步上线 —— 及时发现、告警、自动恢复。

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

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


今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。


快速搭建属于您的专属官网,就上 TechWisdom(www.techwisdom.cn)
提供 100+ 精美模板,支持二级域名和独立域名配置,可根据需求进行 个性化定制开发。首次上线还有专业团队协助 上传内容,轻松打造高效、专业、吸睛的官网!立即访问网站,选择您心仪的模板,开启建站新体验吧!


作者:路条编程(转载请获本公众号授权,并注明作者与出处)

【声明】内容源于网络
0
0
Owen跨境
跨境分享汇 | 持续提供优质内容
内容 44793
粉丝 1
Owen跨境 跨境分享汇 | 持续提供优质内容
总阅读229.9k
粉丝1
内容44.8k