使用Redis控制表单重复提交和控制接口访问频率方式

 更新时间:2025年06月20日 11:16:23   作者:pbxs  
这篇文章主要介绍了使用Redis控制表单重复提交和控制接口访问频率方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

场景一:控制表单重复提交

防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、Lock锁、借助Redis语法实现简单锁、Redis+Lua分布式锁、Redisson分布式锁,再到DB的悲观锁、乐观锁、借助表唯一索引等等都可以实现防重提交,以保证数据的安全性。

这篇文章我们介绍其中一种方案–借助Redis语法实现简单锁,最终实现防重提交。

背景

我们项目中,为了控制表单重复提交问题,会在点击页面按钮(向后端发起业务请求)后就会置灰按钮,直到后端响应后解除按钮置灰。通过按钮置灰来防止重启提交问题。但Postman、Jmeter和其他服务调用(绕过前端页面)呢?所以后端接口也要根据控制表单重复提交的问题。

后端代码可以在2个位置做控制:

一是放在gateway网关做:

  • 好处是只在一个地方加上控制代码,就可以控制所有接口的重复提交问题。
  • 坏处是控制的范围太广(比如查询接口无需控制,控制了反而多余)、定义重复提交的时间段不能灵活调整。

二是放在AOP切面做:

  • 好处是只有需要的地方才会被控制(哪里需要引用一下自定义注解即可),另外也能灵活调整定义重复提交的时间段(自定义注解里定义时间字段开放给使用者填写)。
  • 坏处是每个需要控制的地方都要加注解,会有侵入性和一定的工作量。

实现代码

1、添加自定义注解

package com.xxx.annotations;

import java.lang.annotation.*;

/**
 * 自定义注解防止表单重复提交
 *
 * @Author WANGLINGQIANG
 * @Date 2023/9/6 10:11
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 过期时间,单位毫秒
     */
    long expireTime() default 500L;

}

2、添加AOP切面

package com.xxx.aop;

import com.xxx.annotations.RepeatSubmit;
import com.xxx.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * 防止表单重复提交切面
 *
 * @Author WANGLINGQIANG
 * @Date 2023/9/6 10:13
 */
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
    private static final String KEY_PREFIX = "repeat_submit:";
    @Resource
    private RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.xxx.annotations.RepeatSubmit)")
    public void repeatSubmit() {}

    @Around("repeatSubmit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    	//joinPoint获取方法对象
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取方法上的@RepeatSubmit注解
        RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
        //获取HttpServletRequest对象,以获取请求uri
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String uri = request.getRequestURI();
        //拼接Redis的key,这里只是简单根据uri来判断是否重复提交。可以根据自己业务调整,比如根据用户id或者请求token等
        String cacheKey = KEY_PREFIX.concat(uri);
        Boolean flag = null;
        try {
            //借助setIfAbsent(),key不存在才能设值成功
            flag = redisTemplate.opsForValue().setIfAbsent(cacheKey, "", annotation.expireTime(), TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            //如果Redis不可用,则打印日志记录,但依然对请求放行
            log.error("", e);
            return joinPoint.proceed();
        }
        //Redis可用的情况,如果flag=true说明单位时间内这是第一次请求,放行
        if (flag) {
            return joinPoint.proceed();
        } else {
            //进入else说明单位时间内进行了多次请求,则拦截请求并提示稍后重试
            throw new ServiceException("系统繁忙,请稍后重试");
        }
    }
}

这里利用redisTemplate的setIfAbsent()实现的,如果存在就不能set成功,set的同时设置过期时间,可以是用使用默认,也可以自己根据业务调整。

另外,cacheKey的定义,也可以根据自己的需要去调整,比如根据当前登录用户的userId、当前登录的token等。

3、使用

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

	@RepeatSubmit
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysUser user) {
    	//....
    }

场景二:控制接口调用频率

背景

忘记密码后通过发送手机验证码找回密码的场景。因为每发一条短信都需要收费,所以要控制发短信的频率。

比如,同一个手机号在3分钟内只能发送3次短信,超过3次后则提示用户“短信发送过于频繁,请10分钟后再试”。

实现代码

@Slf4j
@RestController
@RequestMapping("/sms")
public class SmsController {
    @Resource
    private ISmsService smsService;
    @Resource
    public RedisTemplate redisTemplate;

    @PostMapping("/sendValidCode")
    public Result sendValidCode(@RequestBody @Valid SmsDTO smsDTO) {
        //验证手机号格式
        checkPhoneNumber(smsDTO.getPhoneNumber());
        
        //...其他验证
        
		//拼接Redis的key(key为手机号,以控制一个手机号有限时间内容发送的次数)
        String cacheKey = "sms:code:resetPwd:"+smsDTO.getPhoneNumber();
        //验证发送短信次数,超过则拦截(阈值是3次,超时时间是3分钟,重试时间是10分钟)
        checkSendCount(cacheKey, THRESHOLD, TIMEOUT, RETRY_TIME);
        return smsService.sendMsg(smsDTO);
    }
    
    /**
     * 验证发送短信次数,超过则拦截
     * 该方法用lua脚本替换实现更好
     */
    private void checkSendCount(String cacheKey, Long threshold, Long timeout, String retryTime) {
   		//首先进方法就先+1
        Long count = redisTemplate.opsForValue().increment(cacheKey);
        //然后比较次数,是否超过阈值
        if (count > threshold) {
            //超过则设置过期时间为10分钟,并提示10分钟后重试
            redisTemplate.expire(cacheKey, 10L, TimeUnit.MINUTES);
            throw new ServiceException("短信发送过于频繁,请" + retryTime + "分钟后再试");
        } else {
            //没超过3次,则累加上这一次
            redisTemplate.expire(cacheKey, timeout, TimeUnit.MINUTES);
        }
    }

}

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 使用Jedis线程池returnResource异常注意事项

    使用Jedis线程池returnResource异常注意事项

    这篇文章主要介绍了使用Jedis线程池returnResource异常注意事项,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03
  • redis实现多级缓存同步方案详解

    redis实现多级缓存同步方案详解

    这篇文章主要介绍了redis实现多级缓存同步方案详解,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-12-12
  • Redis+Caffeine如何构建高性能二级缓存

    Redis+Caffeine如何构建高性能二级缓存

    这篇文章主要介绍了Redis+Caffeine如何构建高性能二级缓存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • Redis连接超时异常的处理方法

    Redis连接超时异常的处理方法

    这篇文章主要给大家介绍了关于Redis连接超时异常的处理方法,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-07-07
  • 详解Redis单线程架构的优势与不足

    详解Redis单线程架构的优势与不足

    很多人都遇到过这么一道面试题:Redis是单线程还是多线程?这个问题既简单又复杂,说他简单是因为大多数人都知道Redis是单线程,说复杂是因为这个答案其实并不准确,本文就给大家讲讲Redis单线程架构的优势与不足,需要的朋友可以参考下
    2024-02-02
  • Redis内存碎片处理实例详解

    Redis内存碎片处理实例详解

    内存碎片是redis服务中分配器分配存储对象内存的时产生的,下面这篇文章主要给大家介绍了关于Redis内存碎片处理的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-05-05
  • 推荐一款神仙颜值的 Redis 客户端工具(速收藏)

    推荐一款神仙颜值的 Redis 客户端工具(速收藏)

    这篇文章主要给大家推荐一款神仙颜值的 Redis 客户端工具(速收藏),非常好用的redis桌面管理工具,可以运行于Linux、Windows、Mac三大平台,并且当加载大数量的key不会crash,感兴趣的朋友跟随小编一起看看吧
    2020-12-12
  • Redis Stream秒杀系统实现

    Redis Stream秒杀系统实现

    文章浏览阅读345次,点赞7次,收藏3次。高性能: Lua脚本原子操作,毫秒级响应高并发: 异步处理,支持10万+ QPS数据一致性: 库存不会超卖可靠性: 消息队列确保订单不丢失用户体验: 立即返回结果,无需等待。
    2025-11-11
  • Redis的常见四种部署方案

    Redis的常见四种部署方案

    这篇文章介绍Reids最为常见的四种部署模式,其实Reids和数据库的集群模式差不多,可以分为 Redis单机模式部署、Redis主从模式部署、Redis哨兵模式部署、Cluster集群模式部署,其他的部署方式基本都是围绕以下几种方式在进行调整到适应的生产环境,感兴趣的朋友一起看看吧
    2023-11-11
  • 浅谈Redis安全策略

    浅谈Redis安全策略

    Redis 提供了诸多安全策略,比如为了保证数据安全,提供了设置密码的功能。本文就详细的介绍了一下Redis安全策略,具有一定的参考价值,感兴趣的可以了解一下
    2021-11-11

最新评论