Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【干货】如何防止接口重复提交?(中)

【干货】如何防止接口重复提交?(中)

作者头像
Java极客技术
发布于 2022-12-04 06:21:35
发布于 2022-12-04 06:21:35
1.7K00
代码可运行
举报
文章被收录于专栏:Java极客技术Java极客技术
运行总次数:0
代码可运行

一、摘要

在上一篇文章中,我们详细的介绍了对于下单流量不算高的系统,可以通过请求唯一ID+数据表增加唯一索引约束这种方案来实现防止接口重复提交

随着业务的快速增长,每一秒的下单请求次数,可能从几十上升到几百甚至几千。

面对这种下单流量越来越高的场景,此时数据库的访问压力会急剧上升,上面这套方案全靠数据库来解决,会特别吃力!

对于这样的场景,我们可以选择引入缓存中间件来解决,可选的组件有 redis、memcache 等。

下面,我们以引入redis缓存数据库服务器,向大家介绍具体的解决方案!

二、方案实践

我们先来看一张图,这张图就是本次方案的核心流程图。

实现的逻辑,流程如下:

  • 1.当用户进入订单提交界面的时候,调用后端获取请求唯一 ID,同时后端将请求唯一ID存储到redis中再返回给前端,前端将唯一 ID 值埋点在页面里面
  • 2.当用户点击提交按钮时,后端检查这个请求唯一 ID 是否存在,如果不存在,提示错误信息;如果存在,继续后续检查流程
  • 3.使用redis的分布式锁服务,对请求 ID 在限定的时间内进行加锁,如果加锁成功,继续后续流程;如果加锁失败,说明服务正在处理,请勿重复提交
  • 4.最后一步,如果加锁成功后,需要将锁手动释放掉,以免再次请求时,提示同样的信息;同时如果任务执行成功,需要将redis中的请求唯一 ID 清理掉
  • 5.至于数据库是否需要增加字段唯一索引,理论上可以不用加,如果加了更保险

引入缓存服务,防止重复提交的大体思路如上,实践代码如下!

2.1、引入 redis 组件

小编的项目是基于SpringBoot版本进行构建,添加相关的redis依赖环境如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- 引入springboot -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
</parent>

......

<!-- Redis相关依赖包,采用jedis作为客户端 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </exclusion>
        <exclusion>
            <artifactId>lettuce-core</artifactId>
            <groupId>io.lettuce</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

特别注意:由于每个项目环境不一样,具体的依赖包需要和工程版本号匹配

2.2、添加 redis 环境配置

在全局配置application.properties文件中,添加redis相关服务配置如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# Redis数据库索引(默认为0)
spring.redis.database=1
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# Redis服务器连接超时配置
spring.redis.timeout=1000

# 连接池配置
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=1000
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.time-between-eviction-runs=100

在使用redis之前,请确保redis服务器是启动状态,并且能正常访问!

2.3、编写获取请求唯一ID的接口,同时将唯一ID存入redis
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
@RequestMapping("api")
public class SubmitTokenController {

    /**
     * SubmitToken过期时间
     */
    private static final Integer EXPIRE_TIME = 60;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 获取getSubmitToken
     * @return
     */
    @RequestMapping("getSubmitToken")
    public ResResult getSubmitToken(){
        String uuid = UUID.randomUUID().toString();
        //存入redis
        stringRedisTemplate.opsForValue().set(uuid, uuid, EXPIRE_TIME, TimeUnit.SECONDS);
        return ResResult.getSuccess(uuid);
    }
}
2.4、编写服务验证逻辑,通过 aop 代理方式实现

首先创建一个@SubmitToken注解,通过这个注解来进行方法代理拦截!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SubmitToken {

}

编写方法代理服务,增加防止重复提交的验证,实现了逻辑如下!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Order(1)
@Aspect
@Component
public class SubmitTokenAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(SubmitTokenAspect.class);

    /**
     * 获取分布式锁等待时间,单位秒
     */
    private static final Long LOCK_REDIS_WAIT_TIME = 3L;

    /**
     * 分布式锁前缀
     */
    private static final String LOCK_KEY_PREFIX = "SUBMIT:TOKEN:LOCK";

    /**
     * 默认锁对应的值
     */
    private static final String DEFAULT_LOCK_VALUE = "DEFAULT_LOCK_VALUE";


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private RedisLockService redisLockService;

    /**
     * 方法调用环绕拦截
     */
    @Around(value = "@annotation(com.example.submittoken.config.annotation.SubmitToken)")
    public Object doAround(ProceedingJoinPoint joinPoint){
        HttpServletRequest request = getHttpServletRequest();
        if(Objects.isNull(request)){
            return ResResult.getSysError("请求参数不能为空!");
        }
        String submitToken = request.getHeader("submitToken");
        if(StringUtils.isEmpty(submitToken)){
            return ResResult.getSysError("submitToken不能为空!");
        }
        //检查submitToken是否存在
        String submitTokenValue = stringRedisTemplate.opsForValue().get(submitToken);
        if(StringUtils.isEmpty(submitTokenValue)){
            return ResResult.getSysError(ResResultEnum.SUBMIT_ERROR_MESSAGE);
        }
        //尝试加锁
        String lockKey = LOCK_KEY_PREFIX + submitToken;
        boolean lock = redisLockService.tryLock(lockKey, DEFAULT_LOCK_VALUE, Duration.ofSeconds(LOCK_REDIS_WAIT_TIME));
        if(!lock){
            return ResResult.getSysError("服务正在处理,请勿重复提交!");
        }
        try {
            //继续执行后续流程
            Object result = joinPoint.proceed();
            //任务执行成功,清除submitToken缓存
            stringRedisTemplate.delete(submitToken);
            return result;
        } catch (CommonException e) {
            return ResResult.getSysError(e.getMessage());
        } catch (Throwable e) {
            LOGGER.error("业务处理发生异常,错误信息:",e);
            return ResResult.getSysError(ResResultEnum.DEFAULT_ERROR_MESSAGE);
        } finally {
            //执行完毕之后,手动将锁释放
            redisLockService.releaseLock(lockKey, DEFAULT_LOCK_VALUE);
        }
    }

    /**
     * 获取请求对象
     * @return
     */
    private HttpServletRequest getHttpServletRequest(){
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes)ra;
        HttpServletRequest request = sra.getRequest();
        return request;
    }
}

部分校验逻辑用到了redis分布式锁,具体实现逻辑如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * redis分布式锁服务类
 * 采用LUA脚本实现,保证加锁、解锁操作原子性
 *
 */
@Component
public class RedisLockService {

    /**
     * 分布式锁过期时间,单位秒
     */
    private static final Long DEFAULT_LOCK_EXPIRE_TIME = 60L;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 尝试在指定时间内加锁
     * @param key
     * @param value
     * @param timeout 锁等待时间
     * @return
     */
    public boolean tryLock(String key,String value, Duration timeout){
        long waitMills = timeout.toMillis();
        long currentTimeMillis = System.currentTimeMillis();
        do {
            boolean lock = lock(key, value, DEFAULT_LOCK_EXPIRE_TIME);
            if (lock) {
                return true;
            }
            try {
                Thread.sleep(1L);
            } catch (InterruptedException e) {
                Thread.interrupted();
            }
        } while (System.currentTimeMillis() < currentTimeMillis + waitMills);
        return false;
    }

    /**
     * 直接加锁
     * @param key
     * @param value
     * @param expire
     * @return
     */
    public boolean lock(String key,String value, Long expire){
        String luaScript = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire));
        return result.equals(Long.valueOf(1));
    }


    /**
     * 释放锁
     * @param key
     * @param value
     * @return
     */
    public boolean releaseLock(String key,String value){
        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key),value);
        return result.equals(Long.valueOf(1));
    }
}

2.5、在相关的业务接口上,增加SubmitToken注解即可
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 下单
     * @param request
     * @return
     */
    @SubmitToken
    @PostMapping(value = "confirm")
    public ResResult confirm(@RequestBody OrderConfirmRequest request){
        //调用订单下单相关逻辑
        orderService.confirm(request);
        return ResResult.getSuccess();
    }
}

整套方案完全基于redis来实现,同时结合redis的分布式锁来实现请求限流,之所以选择redis,是因为它是一个内存数据库,性能比关系型数据库强太多,即使每秒的下单请求量在几千,也能很好的应对,为关系型数据库起到降压作用

特别注意的地方:使用redis的分布式锁,推荐单机环境,如果redis是集群环境,可能会导致锁短暂无效

三、小结

随着下单流量逐渐上升,通过查询数据库来检查当前服务请求是否重复提交这种方式,可能会让数据库的请求查询频率变得非常高,数据库的压力会倍增。

此时我们可以引入redis缓存,将通过查询数据库来检查当前请求是否重复提交这种方式,转移到通过查询缓存来检查当前请求是否重复提交,可以很好的给数据库降压!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java极客技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【干货】如何防止接口重复提交?(下)
在上一篇文章中,我们详细的介绍了随着下单流量逐渐上升,为了降低数据库的访问压力,通过请求唯一ID+redis分布式锁来防止接口重复提交,流程图如下!
Java极客技术
2022/12/04
1.1K0
【干货】如何防止接口重复提交?(下)
SpringBoot整合Redis实现分布式缓存、分布式锁等,实战分享!
在前几篇文章中,我们详细介绍了 redis 的一些功能特性以及主流的 java 客户端 api 使用方法。
Java极客技术
2023/02/23
3.4K0
SpringBoot整合Redis实现分布式缓存、分布式锁等,实战分享!
【SpringBoot】SpringBoot中防止接口重复提交(单机环境和分布式环境)
本文将从SpringBoot应用的角度出发,探讨在单机环境和分布式环境下如何有效防止接口重复提交。单机环境虽然相对简单,但基本的防护策略同样适用于分布式环境的部署。 接下来,我们将首先分析接口重复提交的原因和危害,然后详细介绍在SpringBoot应用中可以采取的防护策略,包括前端控制、后端校验、使用令牌机制(如Token)、利用数据库的唯一约束等。对于分布式环境,我们还将探讨如何使用分布式锁、Redis等中间件来确保数据的一致性和防止接口被重复调用。
哈__
2024/05/24
1.6K0
【SpringBoot】SpringBoot中防止接口重复提交(单机环境和分布式环境)
利用Redis实现防止接口重复提交功能
在划水摸鱼之际,突然听到有的用户反映增加了多条一样的数据,这用户立马就不干了,让我们要马上修复,不然就要投诉我们。
秃头哥编程
2022/04/27
1.3K0
面试必会的重复提交 8 种解决方案!
重复提交看似是一个小儿科的问题,但却存在好几种变种用法。在面试中回答的好,说不定会有意想不到的收获!现把这 8 种解决方案分享给大家!
业余草
2019/09/18
6810
面试必会的重复提交 8 种解决方案!
纠正误区:这才是 SpringBoot Redis 分布式锁的正确实现方式
在单机部署的时候,我们可以使用 Java 中提供的 JUC 锁机制避免多线程同时操作一个共享变量产生的安全问题。JUC 锁机制只能保证同一个 JVM 进程中的同一时刻只有一个线程操作共享资源。
码哥字节
2024/01/29
1.4K0
纠正误区:这才是 SpringBoot Redis 分布式锁的正确实现方式
Redis分布式锁的正确实现方式
数据库乐观锁 基于Redis的分布式锁 基于Zookeeper的分布式锁 本文介绍的是基于Redis的分布式锁;
stys35
2020/04/02
1.1K0
SpringBoot接口防抖(防重复提交)的一些实现方案
作为一名老码农,在开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。
程序员皮皮林
2024/09/12
2590
SpringBoot接口防抖(防重复提交)的一些实现方案
一口气说出8种幂等性解决重复提交的方案,面试官懵了!(附代码)
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。
java进阶架构师
2020/05/25
1.6K0
一口气说出8种幂等性解决重复提交的方案,面试官懵了!(附代码)
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的代码,可重入性就就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行。
码哥字节
2024/01/30
4240
字节二面:Spring Boot Redis 可重入分布式锁实现原理?
一文读懂分布式锁——使用SpringBoot+Redis实现分布式锁
随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁。很多小伙伴对于分布式锁还不是特别了解,所以特地总结了一篇文章,让大家一文读懂分布式锁的前世今生。
章为忠学架构
2023/03/23
6.7K0
一文读懂分布式锁——使用SpringBoot+Redis实现分布式锁
分布式接口防抖终极解决方案,如何避免重复提交!
防抖(Debouncing)是一种编程技术,用于控制事件处理函数的执行频率。在用户与界面交互频繁的场景中,比如连续滚动、连续输入等,如果每次交互都触发事件处理函数,可能会导致性能问题或不必要的数据库操作。
Tinywan
2024/07/05
5520
分布式接口防抖终极解决方案,如何避免重复提交!
Redis实现分布式锁的正确方式
封面为好友拍摄的照片,想查看更多微信公众号搜索:JavaBoy王皓或csdn博客搜索:TenaciousD
胖虎
2019/06/26
8760
Redis实现分布式锁的正确方式
用户重复注册分析-多线程事务中加锁引发的bug
线上客户端用户使用微信扫码登陆时需要再绑定一个手机号,在绑定手机后,用户购买客户端商品下线再登录,发现用户账号ID被变更,已经不是用户刚绑定手机号时自动登录的用户账号ID,查询线上数据库,发现同一个手机生成了多个账号id,至此问题复现
wayn
2022/12/10
1.8K1
用户重复注册分析-多线程事务中加锁引发的bug
老大吩咐的可重入分布式锁,终于完美的实现了!!!
最近在做一个项目,将一个其他公司的实现系统(下文称作旧系统),完整的整合到自己公司的系统(下文称作新系统)中,这其中需要将对方实现的功能完整在自己系统也实现一遍。
andyxh
2020/06/16
7350
redis分布式锁的实现(setNx命令和Lua脚本)
本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结
全栈程序员站长
2022/08/31
2.5K0
1.缓存Redis实战操作记录
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
全栈程序员站长
2022/06/30
4980
1.缓存Redis实战操作记录
如何保证接口幂等性?高并发下的接口幂等性如何实现?
接口幂等性这一概念源于数学,原意是指一个操作如果连续执行多次所产生的结果与仅执行一次的效果相同,那么我们就称这个操作是幂等的。在互联网领域,特别是在Web服务、API设计和分布式系统中,接口幂等性具有非常重要的意义。
用户11397231
2025/01/24
1170
如何保证接口幂等性?高并发下的接口幂等性如何实现?
Spring Boot 接口幂等性实现的 4 种方案!
幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同。在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
用户1516716
2021/03/23
5.7K2
Redis常见坑及解决方案
以上使用Redis的setNx()命令和expire命令实现了加锁,但是本方案是分成了两步完成的加锁操作,并不是原子操作,可能会出现未给该key设置过期时间的问题,因此该问题的解决方案推荐使用Redisson的分布式锁。
关忆北.
2023/10/11
2820
推荐阅读
相关推荐
【干货】如何防止接口重复提交?(下)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验