百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

SpringBoot + Redis + Token 解决接口幂等性问题,挑选最佳方案!

mhr18 2025-05-26 17:26 37 浏览 0 评论

前言

SpringBoot实现接口幂等性的方案有很多,其中最常用的一种就是 token + redis 方式来实现。

下面我就通过一个案例代码,帮大家理解这种实现逻辑。

原理

前端获取服务端getToken() -> 前端发起请求 -> header中带上token -> 服务端校验前端传来的token和redis中的token是否一致 -> 一致则删除token -> 执行业务逻辑

案例

1、利用Token + Redis

核心代码如下:

java@RestController
public class IdempotentController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 提交接口,需要携带有效的token参数
     */
    @PostMapping("/submit")
    public String submit(@RequestParam("token") String token) {
        // 检查Token是否有效
        if (!isValidToken(token)) {
            return "Invalid token";
        }

        // 具体的接口处理逻辑,在这里实现你的业务逻辑

        // 使用完毕后删除Token
        deleteToken(token);

        return "Success";
    }

    /**
     * 检查Token是否有效
     */
    private boolean isValidToken(String token) {
        // 检查Token是否存在于Redis中
        return redisTemplate.hasKey(token);
    }

    /**
     * 删除Token
     */
    private void deleteToken(String token) {
        // 从Redis中删除Token
        redisTemplate.delete(token);
    }

    /**
     * 生成Token接口,用于获取一个唯一的Token
     */
    @GetMapping("/generateToken")
    public String generateToken() {
        // 生成唯一的Token
        String token = UUID.randomUUID().toString();

        // 将Token保存到Redis中,并设置过期时间(例如10分钟)
        redisTemplate.opsForValue().set(token, "true", Duration.ofMinutes(10));

        return token;
    }
}

上述代码和前面描述的原理一致,但实际上存在问题,那就是在高并发场景下依然会有幂等性问题,这是因为没有充分利用redis的原子性

2、利用Redis原子性

接下来,使用Redis的原子性操作,比如SETNXEXPIRE来实现更可靠的幂等性控制。

我们优化一下代码,如下:

java@RestController
public class IdempotentController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 提交接口,需要携带有效的token参数
     */
    @PostMapping("/submit")
    public String submit(@RequestParam("token") String token) {
        // 使用SETNX命令尝试将Token保存到Redis中,如果返回1表示设置成功,说明是第一次提交;否则返回0,表示重复提交
        Boolean success = redisTemplate.opsForValue().setIfAbsent(token, "true", Duration.ofMinutes(10));
        if (success == null || !success) {
            return "Duplicate submission";
        }

        try {
            // 具体的接口处理逻辑,在这里实现你的业务逻辑

            return "Success";
        } finally {
            // 使用DEL命令删除Token
            redisTemplate.delete(token);
        }
    }
}

可以看到,我们使用了setIfAbsent方法来尝试将Token保存到Redis中,并设置过期时间(例如10分钟)。如果设置成功,则执行具体的接口处理逻辑,处理完成后会自动删除Token。如果设置失败,说明该Token已存在,即重复提交,直接返回错误信息。

注意,上述代码中删除Token的操作在finally块中执行,无论接口处理逻辑成功与否都会确保删除Token,以免出现异常导致未能正确删除Token的情况。

通过使用Redis的原子性操作,我们可以更可靠地实现接口的幂等性,并在高并发情况下提供更好的性能和准确性。

但是,在高并发场景下,这样其实依然有问题,依然有概率出现幂等性问题。

这是因为,高并发场景下,可能会出现同时两个请求都从redis中获取到token,在服务端都能校验成功,最终破坏幂等性。

所以,还有优化的空间。

3、结合Lua脚本

可以使用Lua脚本配合Redis的原子性操作来实现更可靠的幂等性控制。

优化后的完整代码如下:

java@RestController
public class IdempotentController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 提交接口,需要携带有效的token参数
     */
    @PostMapping("/submit")
    public String submit(@RequestHeader("token") String token) {
        if (StringUtils.isBlank(token)) {
            return "Missing token";
        }

        DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(LUA_SCRIPT, Boolean.class);

        // 使用Lua脚本执行原子性操作
        Boolean success = redisTemplate.execute(script, Collections.singletonList(token), "true", "600");
        if (success == null || !success) {
            return "Duplicate submission";
        }

        try {
            // 具体的接口处理逻辑,在这里实现你的业务逻辑

            return "Success";
        } finally {
            // 使用DEL命令删除Token
            redisTemplate.delete(token);
        }
    }

    /**
     * 生成Token接口,用于获取一个唯一的Token
     */
    @GetMapping("/generateToken")
    public String generateToken() {
        // 生成唯一的Token
        String token = UUID.randomUUID().toString();

        // 将Token保存到Redis中,并设置过期时间(例如10分钟)
        redisTemplate.opsForValue().set(token, "true", Duration.ofMinutes(10));

        return token;
    }

    // Lua脚本
    private final String LUA_SCRIPT = "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then\n" +
            "    redis.call('EXPIRE', KEYS[1], ARGV[2])\n" +
            "    return true\n" +
            "else\n" +
            "    return false\n" +
            "end";
}

其中,这段Lua脚本的含义如下:

首先定义了一个私有 final 字符串变量 LUA_SCRIPT,用于存储Lua脚本的内容。


在Lua脚本中使用了Redis的命令,以及参数引用。下面是逐行解释:

1、if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then:使用 Redis 的 SETNX 命令,在键 KEYS[1] 中设置值为 ARGV[1](ARGV 是一个参数数组)。如果 SETNX 返回值为 1(表示设置成功),则执行以下代码块。


2、redis.call('EXPIRE', KEYS[1], ARGV[2]):使用 Redis 的 EXPIRE 命令,在键 KEYS[1] 设置过期时间为 ARGV[2] 秒。


3、return true:返回布尔值 true 给调用方,表示设置和过期时间设置都成功。

else:如果 SETNX 返回值不为 1,则执行以下代码块。


4、return false:返回布尔值 false 给调用方,表示设置失败。

所以,这段Lua脚本的目的是在 Redis 中设置一个键值对,并为该键设置过期时间。


如果键已存在,脚本将返回 false 表示设置失败;如果键不存在,脚本将返回 true 表示设置和过期时间设置都成功。

总结

在处理接口幂等性的问题中,token机制使用最广泛,也是性能比较好的方案。

其实,还有一种比较简单的方案,就是使用Redission分布式锁。

这种方案的编码非常少,效果也能达到,但上锁必有损耗,所以综合性能是不如本文方案的,但因为封装的好,编码简单,也是企业中很受欢迎的方式。

我的过往文章中有关于Redisson配合自定义注解实现防重的文章《Springboot+Redisson自定义注解一次解决重复提交问题(含源码)》,有兴趣的可以去看一下。

Redisson虽然实现简单,但本身不利于学习,在学习阶段,我不推荐直接上手Redisson。

好了,今天的知识学会了吗?


福利

私信回复【面试】获取【Java面试八股文10万字总结】【Java进阶架构核心手册】

相关推荐

Java面试题及答案总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Redis、Linux、SpringBoot、Spring、MySQ...

Java面试题及答案最全总结(2025春招版)

大家好,我是Java面试分享最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Spring...

Java面试题及答案最全总结(2025版持续更新)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

蚂蚁金服面试题(附答案)建议收藏:经典面试题解析

前言最近编程讨论群有位小伙伴去蚂蚁金服面试了,以下是面试的真题,跟大家一起来讨论怎么回答。点击上方“捡田螺的小男孩”,选择“设为星标”,干货不断满满1.用到分布式事务嘛?为什么用这种方案,有其他方案...

测试工程师面试必问的十道题目!全答上来的直接免试

最近参加运维工程师岗位的面试,笔者把自己遇到的和网友分享的一些常见的面试问答收集整理出来了,希望能对自己和对正在准备面试的同学提供一些参考。一、Mongodb熟悉吗,一般部署几台?部署过,没有深入研究...

10次面试9次被刷?吃透这500道大厂Java高频面试题后,怒斩offer

很多Java工程师的技术不错,但是一面试就头疼,10次面试9次都是被刷,过的那次还是去了家不知名的小公司。问题就在于:面试有技巧,而你不会把自己的能力表达给面试官。应届生:你该如何准备简历,面试项目和...

java高频面试题整理

【高频常见问题】1、事务的特性原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。一致性或可串性:事务的执行使得数据库从一种正确状态转换成另一种正确状态隔离性:在事务正确提交之前,不允许把该...

2025 年最全 Java 面试题,京东后端面试面经合集,答案整理

最近京东搞了个TGT计划,针对顶尖青年技术天才,直接宣布不设薪资上限。TGT计划面向范围包括2023年10月1日到2026年9月30日毕业的海内外本硕博毕业生。时间范围还...

idGenerator测评

工作中遇到需要生成随机数的需求,看了一个个人开发的基于雪花算法的工具,今天进行了一下测评(测试)。idGenerator项目地址见:https://github.com/yitter/IdGenera...

2024年开发者必备:MacBook Pro M1 Max深度体验与高效工作流

工作机器我使用的是一台16英寸的MacBookProM1Max。这台电脑的表现堪称惊人!它是我用过的最好的MacBook,短期内我不打算更换它。性能依然出色,即使在执行任务时也几乎听不到风扇的...

StackOverflow 2022 年度调查报告

一个月前,StackOverflow开启了2022年度开发者调查,历时一个半月,在6月22日,StackOverflow正式发布了2022年度开发者调查报告。本次报告StackO...

这可能是最全面的SpringDataMongoDB开发笔记

MongoDB数据库,在最近使用越来越广泛,在这里和Java的开发者一起分享一下在Java中使用Mongodb的相关笔记。希望大家喜欢。关于MongoDB查询指令,请看我的上一篇文章。SpringD...

Mac M2 本地部署ragflow

修改配置文件Dockerfile文件ARGNEED_MIRROR=1//开启国内镜像代理docker/.envREDIS_PORT=6380//本地redis端口冲突RAGFLOW_IMA...

别再傻傻分不清!localhost、127.0.0.1、本机IP,原来大有讲究!

调试接口死活连不上?部署服务队友访问不了?八成是localhost、127.0.0.1、本机IP用混了!这三个看似都指向“自己”的东西,差之毫厘谬以千里。搞不清它们,轻则调试抓狂,重则服务裸奔。loc...

我把 Mac mini 托管到机房了:一套打败云服务器的终极方案

我把我积灰的Macmini托管到机房了,有图有真相。没想到吧?一台在家吃灰的苹果电脑,帮我省了大钱!对,就是控制了自己的服务器,省了租用云服务器的钱,重要数据还全捏在自己手里,这感觉真爽。你可...

取消回复欢迎 发表评论: