导读:本期聚焦于孙悟空创作的《Spring Boot 接口幂等性如何设计?基于 Token 与 Redis 的实现方案详解》,敬请观看详情。接口被重复调用导致重复下单、重复扣款,是分布式系统中常见的数据一致性问题。本文围绕 Spring Boot 场景,详细讲解如何利用 Token 机制结合 Redis 实现接口幂等性控制。内容涵盖幂等性的基本概念与产生原因、整体设计思路、拦截器与注解的具体代码实现、并发场景下的原子性保障,以及方案优缺点与适用场景分析。同时对比了唯一索引、分布式锁、状态机等常见幂等手段,帮助开发者在实际项目中根据业务特点选择合适的防重方案,避免脏数据产生。

在电商下单、支付回调、表单提交等场景中,用户快速双击按钮、网络重试或消息队列重复投递,都可能导致同一个请求被服务端处理多次,进而产生重复订单、重复扣款等严重问题。幂等性设计就是为了让同一操作无论执行多少次,产生的效果都与执行一次相同。本文将以 Spring Boot 为技术底座,讲解一套基于 Token 机制与 Redis 的接口幂等性解决方案,并给出完整的代码实现。

Spring Boot 接口幂等性如何设计?基于 Token 与 Redis 的实现方案详解

什么是幂等性,为什么接口需要它

幂等性原本是数学中的概念,指一个操作执行一次和执行多次的结果完全一致。映射到 HTTP 接口层面,就是客户端对同一个接口发起多次请求,服务端的业务结果应当保持不变。需要特别注意,GET、PUT、DELETE 天然具有幂等语义,而 POST 通常不是幂等的,所以幂等性设计的重点往往集中在 POST 类型的写操作接口上。

导致接口被重复调用的原因有很多:前端没有做按钮防抖,用户在网络卡顿时连续点击;网关或负载均衡在超时后自动重试;客户端 App 的请求重试机制;MQ 消息在生产端未确认时重复投递;支付渠道的异步回调多次通知等。这些场景在开发阶段往往难以完全复现,只能靠服务端的幂等设计来兜底。

如果不做幂等控制,最直接的后果就是脏数据。例如用户购买一件商品,由于双击提交产生了两张订单,库存被扣减两次,后续的退款和对账流程都会变得混乱。因此在涉及资金、库存、订单等核心链路时,幂等性不是可选项,而是必须项。

Token 机制的设计思路

Token 幂等方案的核心流程分为两步:第一步,客户端在进入业务页面时,先调用一个获取 Token 的接口,服务端生成一个唯一 Token 并存入 Redis,同时把值返回给客户端;第二步,客户端提交业务请求时携带这个 Token,服务端在处理业务前先去 Redis 中删除该 Token,删除成功才继续执行业务逻辑,删除失败说明 Token 已被消费或不存在,直接拒绝请求并提示重复提交。

这个方案之所以有效,关键在于 Redis 的删除操作是原子的。第一次请求到达时删除 Token 成功,业务正常执行;后续重复请求到达时 Token 已经不存在,删除失败,请求被拦截。这样即使客户端在极短时间内发出多个并发请求,也只有一个请求能通过校验。

需要强调一个常见的错误实现:先调用 get 判断 Token 是否存在,再调用 del 删除。这种写法在并发下存在竞态条件,两个请求可能同时读到 Token 存在,然后都执行删除并都通过校验。正确的做法是使用 Lua 脚本把校验和删除合并成一个原子操作,或者直接依赖 del 命令的返回值判断是否删除成功。

代码实现:注解加拦截器的完整方案

下面在 Spring Boot 项目中落地这套方案。整体结构包括三个部分:自定义注解 @Idempotent 用于标记需要幂等控制的接口;一个拦截器负责提取 Token 并调用 Redis 完成原子校验;一个工具接口用于发放 Token。

首先定义注解:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // Token 在请求头中的键名,默认为 idempotentToken
    String headerName() default "idempotentToken";
}

接着编写拦截器,核心逻辑是使用 Lua 脚本保证校验与删除的原子性:

@Component
public class IdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // Lua 脚本:存在则删除并返回1,不存在返回0
    private static final DefaultRedisScript<Long> SCRIPT =
            new DefaultRedisScript<>(
                    "if redis.call('get', KEYS[1]) == ARGV[1] " +
                    "then return redis.call('del', KEYS[1]) " +
                    "else return 0 end",
                    Long.class);

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod method = (HandlerMethod) handler;
        Idempotent annotation = method.getMethodAnnotation(Idempotent.class);
        if (annotation == null) {
            return true;
        }
        String token = request.getHeader(annotation.headerName());
        if (token == null || token.isEmpty()) {
            writeError(response, "缺少幂等Token");
            return false;
        }
        String key = "idempotent:token:" + token;
        Long result = redisTemplate.execute(SCRIPT,
                Collections.singletonList(key), token);
        if (result == null || result == 0) {
            writeError(response, "请勿重复提交");
            return false;
        }
        return true;
    }

    private void writeError(HttpServletResponse response, String msg)
            throws IOException {
        response.setStatus(409);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":409,\"message\":\"" + msg + "\"}");
    }
}

然后注册拦截器,并提供 Token 发放接口:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private IdempotentInterceptor idempotentInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor)
                .addPathPatterns("/**");
    }
}

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/generate")
    public String generateToken() {
        String token = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(
                "idempotent:token:" + token, token, 5, TimeUnit.MINUTES);
        return token;
    }
}

最后在业务接口上使用注解即可:

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

    @PostMapping("/create")
    @Idempotent
    public Result createOrder(@RequestBody OrderRequest req) {
        // 走到这里说明Token校验通过,业务只会执行一次
        return orderService.createOrder(req);
    }
}

使用流程非常清晰:前端进入下单页时先请求 /token/generate 拿到 Token,提交订单时把 Token 放在请求头 idempotentToken 中一起发送。拦截器完成原子校验后放行,重复请求则直接返回 409 状态码。

方案细节与常见坑点

第一个需要注意的点是 Token 的过期时间。设置过长会占用 Redis 内存,设置过短则用户在页面停留较久后提交会失败,一般建议 5 到 10 分钟。第二个点是 Token 必须一次性消费,即用即删,绝不能在校验通过后保留 Token 供再次使用,否则幂等性就失效了。

第三个坑是业务执行失败后的处理。如果 Token 已被删除但业务代码抛出异常,用户修正后再次提交会被拦截。针对这种情况有两种策略:一是捕获业务异常后在拦截器的 afterCompletion 阶段回写 Token(实现较复杂);二是业务失败时由前端重新调用生成接口获取新 Token,这种方式实现简单,推荐优先采用。

第四个点是企业级落地时的 Token 与用户绑定。仅靠 UUID 无法防止 Token 被他人窃取后伪造请求,更严谨的做法是在生成 Token 时记录当前登录用户 ID,校验时验证请求者身份与 Token 归属一致,可以通过把用户 ID 拼入 Redis 的 value 中并在 Lua 脚本里一并比对来实现。

与其他幂等方案的对比

Token 加 Redis 并不是唯一的幂等手段,实际项目中经常多种方案配合使用:

方案实现方式优点缺点
Token + Redis前置发放Token,请求时原子删除通用性强,不依赖数据库需要前端配合,多一次请求
唯一索引数据库对业务唯一键建唯一约束绝对可靠,兜底能力强只适合插入场景,冲突会抛异常
分布式锁Redis或ZooKeeper对业务键加锁可控制并发执行锁过期与业务超时需要权衡
状态机订单状态只能单向流转天然幂等,语义清晰仅适合有状态流转的业务
乐观锁版本号更新时携带版本号比对解决并发更新覆盖需要重试机制配合

从表中可以看出,Token 方案的优势在于对业务侵入小、通用性好,特别适合表单提交、下单这类由用户主动触发的操作。而唯一索引适合作为最终兜底,即使前面的校验全部失效,数据库约束也能保证不产生重复数据。支付回调场景则更适合状态机加唯一索引的组合,因为回调方通常不会先来获取 Token。

总结来说,幂等性设计没有银弹,Token 加 Redis 的方案在 Spring Boot 中实现成本低、效果可靠,是防重复提交的首选方案。建议在核心写接口上叠加使用 Token 前置校验与数据库唯一索引兜底,两层防护结合,才能在复杂的生产环境中真正保障数据一致性。

Spring Boot幂等性Token机制Redis防重复提交修改时间:2026-09-01 17:04:44

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。