使用Redis控制表单重复提交和控制接口访问频率方式
目录
- 场景一:控制表单重复提交
- 背景
- 实现代码
- 场景二:控制接口调用频率
- 背景
- 实现代码
- 总结
场景一:控制表单重复提交
防重提交有很多方案,从前端的按钮置灰,到后端synchronize锁、Lock锁、借助Redis语法实现简单锁、Redis+Lua分布式锁、Redisson分布式锁,再到DB的悲观锁、乐观锁、借助表唯一索引等等都可以实现防重提交,以保证数据的安全性。
这篇文章我们介绍其中一aJdNU种方案–借助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.sl编程客栈f4j.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.RedisTemplataJdNUe; 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 Throwablwww.devze.come { http://www.devze.com //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); } } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论