Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis控制表单重复提交和控制接口访问频率

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

作者: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);
        }
    }

}

总结

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

您可能感兴趣的文章:
阅读全文