** 🏡浩泽学编程:个人主页
** 🔥 推荐专栏:《深入浅出SpringBoot》《java对AI的调用开发》
《RabbitMQ》《Spring》《SpringMVC》《项目实战》🛸学无止境,不骄不躁,知行合一
文章目录
前言
限流是秒杀业务最常用的手段。限流是从用户访问压力的角度来考虑如何应对系统故障。这里我是用限制访问接口次数(Redis+拦截器+自定义注解)和验证码的方式实现简单限流。
一、接口限流
- 接口限流是为了对服务端的接口接收请求的频率进行限制,防止服务挂掉。
- 栗子:假设我们的秒杀接口一秒只能处理12w个请求,结果秒杀活动刚开始就一下来了20w个请求。这肯定是不行的,我们可以通过接口限流将这8w个请求给拦截住,不然系统直接就整挂掉。
- 实现方案: - Sentiel等开源流量控制组件(Sentiel主要以流量为切入点,提供流量控制、熔断降级、系统自适应保护等功能的稳定性和可用性)- 秒杀请求之前进行验证码输入或答题等- 限制同一用户、ip单位时间内请求次数- 提前预约- 等等
这里我使用的是Redis+Lua脚本+拦截器实现同一用户单位时间内请求次数限制。
自定义注解
含义:限制xx秒内最多请求xx次
importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**
* @Version: 1.0.0
* @Author: Dragon_王
* @ClassName: AccessLimit
* @Description: 通用接口限流,限制xx秒内最多请求次数
* @Date: 2024/3/3 17:09
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceAccessLimit{//时间,单位秒intsecond();//限制最大请求次数intmaxCount();//是否需要登录booleanneedLogin()defaulttrue;}
Redis+Lua脚本+拦截器
主要关心业务逻辑:
@ComponentpublicclassAccessLimitInterceptorimplementsHandlerInterceptor{@AutowiredprivateIUserService userService;@AutowiredprivateRedisTemplate redisTemplate;//加载lua脚本privatestaticfinalDefaultRedisScript<Boolean>SCRIPT;static{SCRIPT=newDefaultRedisScript<>();SCRIPT.setLocation(newClassPathResource("script.lua"));SCRIPT.setResultType(Boolean.class);}@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{if(handler instanceofHandlerMethod){//获取登录用户User user =getUser(request, response);HandlerMethod hm =(HandlerMethod) handler;//获取自定义注解内的属性值AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);if(accessLimit ==null){returntrue;}int second = accessLimit.second();int maxCount = accessLimit.maxCount();boolean needLogin = accessLimit.needLogin();//获取当前请求地址作为keyString key = request.getRequestURI();//如果needLogin=true,是必须登录,进行用户状态验证if(needLogin){if(user ==null){render(response,RespBeanEnum.SESSION_ERROR);returnfalse;}
key +=":"+ user.getId();}//使用lua脚本Object result = redisTemplate.execute(SCRIPT,Collections.singletonList(key),newString[]{String.valueOf(maxCount),String.valueOf(second)});if(result.equals(false)){//render函数就是一个让我返回报错的函数,这里的RespBeanEnum是我封装好的报错的枚举类型,无需关注,render函数你也无需管,只要关心return false拦截render(response,RespBeanEnum.ACCESS_LIMIT_REACHED);//拦截returnfalse;}}returntrue;}privatevoidrender(HttpServletResponse response,RespBeanEnum respBeanEnum)throwsIOException{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");PrintWriter printWriter = response.getWriter();RespBean bean =RespBean.error(respBeanEnum);
printWriter.write(newObjectMapper().writeValueAsString(bean));
printWriter.flush();
printWriter.close();}/**
* @Description: 获取当前登录用户
* @param request
* @param response
* @methodName: getUser
* @return: com.example.seckill.pojo.User
* @Author: dragon_王
* @Date: 2024-03-03 17:20:51
*/privateUsergetUser(HttpServletRequest request,HttpServletResponse response){String userTicket =CookieUtil.getCookieValue(request,"userTicker");if(StringUtils.isEmpty(userTicket)){returnnull;}return userService.getUserByCookie(userTicket, request, response);}}
lua脚本,如果第一次访问就存入计数器,每次访问+1,如果计数器大于5返回false
local key = KEYS[1]local maxCount =tonumber(ARGV[1])local second =tonumber(ARGV[2])local count = redis.call('GET', key)if count then
count =tonumber(count)if count < maxCount then
count = count +1
redis.call('SET', key, count)
redis.call('EXPIRE', key, second)elsereturnfalseendelse
redis.call('SET', key,1)
redis.call('EXPIRE', key, second)endreturntrue
二、验证码
引入验证码依赖(这是个开源的图形验证码,直接拿过来用):
<!--验证码依赖--><dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version></dependency><dependency><groupId>org.openjdk.nashorn</groupId><artifactId>nashorn-core</artifactId><version>15.3</version></dependency>
/**
* @Description: 获取验证码
* @param user
* @param goodsId
* @param response
* @methodName: verifyCode
* @return: void
* @Author: dragon_王
* @Date: 2024-03-03 12:38:14
*/@ApiOperation("获取验证码")@GetMapping(value ="/captcha")publicvoidverifyCode(User user,Long goodsId,HttpServletResponse response){if(user ==null|| goodsId <0){thrownewGlobalException(RespBeanEnum.REQUEST_ILLEGAL);}//设置请求头为输出图片的类型
response.setContentType("image/jpg");
response.setHeader("Pargam","No-cache");
response.setHeader("Cache-Control","no-cache");
response.setDateHeader("Expires",0);//生成验证码ArithmeticCaptcha captcha =newArithmeticCaptcha(130,32,3);//奖验证码结果存入redis
redisTemplate.opsForValue().set("captcha:"+ user.getId()+":"+ goodsId, captcha.text(),300,TimeUnit.SECONDS);try{
captcha.out(response.getOutputStream());}catch(IOException e){
log.error("验证码生成失败", e.getMessage());}}
这里用的是bootstrap写的简单前端:
<divclass="row"><divclass="form-inline"><imgid="captchaImg"width="130"height="32"style="display: none"onclick="refreshCaptcha()"/><inputid="captcha"class="form-control"style="display: none"/><buttonclass="btn btn-primary"type="button"id="buyButton"onclick="getSeckillPath()">立即秒杀
</button></div></div><script>
校验验证码逻辑也很简单 (从redis中取出存入的图形结果和输入框中比对):
/**
* @Description: 校验验证码
* @param user
* @param goodsId
* @param captcha
* @methodName: checkCaptcha
* @return: boolean
* @Author: dragon_王
* @Date: 2024-03-03 15:48:13
*/publicbooleancheckCaptcha(User user,Long goodsId,String captcha){if(user ==null|| goodsId <0||StringUtils.isEmpty(captcha)){returnfalse;}String redisCaptcha =(String) redisTemplate.opsForValue().get("captcha:"+ user.getId()+":"+ goodsId);return captcha.equals(redisCaptcha);}
总结
以上就是用redis+自定义注解+Lua脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。
版权归原作者 浩泽学编程 所有, 如有侵权,请联系我们删除。