相关推荐recommended
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
作者:mmseoamin日期:2024-03-20

【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流,在这里插入图片描述,第1张


🏡浩泽学编程:个人主页

 🔥 推荐专栏:《深入浅出SpringBoot》《java对AI的调用开发》
              《RabbitMQ》《Spring》《SpringMVC》《项目实战》
🛸学无止境,不骄不躁,知行合一

文章目录

  • 前言
  • 一、接口限流
    • 自定义注解
    • Redis+Lua脚本+拦截器
    • 二、验证码
    • 总结

      前言

      限流是秒杀业务最常用的手段。限流是从用户访问压力的角度来考虑如何应对系统故障。这里我是用限制访问接口次数(Redis+拦截器+自定义注解)和验证码的方式实现简单限流。


      一、接口限流

      • 接口限流是为了对服务端的接口接收请求的频率进行限制,防止服务挂掉。
      • 栗子:假设我们的秒杀接口一秒只能处理12w个请求,结果秒杀活动刚开始就一下来了20w个请求。这肯定是不行的,我们可以通过接口限流将这8w个请求给拦截住,不然系统直接就整挂掉。
      • 实现方案:
        • Sentiel等开源流量控制组件(Sentiel主要以流量为切入点,提供流量控制、熔断降级、系统自适应保护等功能的稳定性和可用性)
        • 秒杀请求之前进行验证码输入或答题等
        • 限制同一用户、ip单位时间内请求次数
        • 提前预约
        • 等等

      这里我使用的是Redis+Lua脚本+拦截器实现同一用户单位时间内请求次数限制。

      自定义注解

      含义:限制xx秒内最多请求xx次

      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.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 @interface AccessLimit {
      	//时间,单位秒
          int second();
      	//限制最大请求次数
          int maxCount();
      	//是否需要登录
          boolean needLogin() default true;
      }
      

      Redis+Lua脚本+拦截器

      主要关心业务逻辑:

      @Component
      public class AccessLimitInterceptor implements HandlerInterceptor{
          @Autowired
          private IUserService userService;
          @Autowired
          private RedisTemplate redisTemplate;
      	
      	//加载lua脚本
      	private static final DefaultRedisScript SCRIPT;
          static {
              SCRIPT = new DefaultRedisScript<>();
              SCRIPT.setLocation(new ClassPathResource("script.lua"));
              SCRIPT.setResultType(Boolean.class);
          }
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {		
          	
              if (handler instanceof HandlerMethod) {
              	//获取登录用户
                  User user = getUser(request, response);
                  HandlerMethod hm = (HandlerMethod) handler;
                  //获取自定义注解内的属性值
                  AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
                  if (accessLimit == null) {
                      return true;
                  }
                  int second = accessLimit.second();
                  int maxCount = accessLimit.maxCount();
                  boolean needLogin = accessLimit.needLogin();
      			
      			//获取当前请求地址作为key
                  String key = request.getRequestURI();
                  //如果needLogin=true,是必须登录,进行用户状态验证
                  if (needLogin) {
                      if (user == null) {
                          render(response, RespBeanEnum.SESSION_ERROR);
                          return false;
                      }
                      key += ":" + user.getId();
                  }
                  //使用lua脚本
                  Object result = redisTemplate.execute(SCRIPT, Collections.singletonList(key),new String[]{String.valueOf(maxCount), String.valueOf(second)});
                  if (result.equals(false)){
                  	//render函数就是一个让我返回报错的函数,这里的RespBeanEnum是我封装好的报错的枚举类型,无需关注,render函数你也无需管,只要关心return false拦截
                      render(response,RespBeanEnum.ACCESS_LIMIT_REACHED);
                      //拦截
                      return false;
                  }
              }
              return true;
          }
          private void render(HttpServletResponse response, RespBeanEnum respBeanEnum) throws IOException {
              response.setCharacterEncoding("UTF-8");
              response.setContentType("application/json");
              PrintWriter printWriter = response.getWriter();
              RespBean bean = RespBean.error(respBeanEnum);
              printWriter.write(new ObjectMapper().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
           */
          private User getUser(HttpServletRequest request, HttpServletResponse response) {
              String userTicket = CookieUtil.getCookieValue(request, "userTicker");
              if (StringUtils.isEmpty(userTicket)) {
                  return null;
              }
              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)
          else
              return false
          end
      else
          redis.call('SET', key, 1)
          redis.call('EXPIRE', key, second)
      end
      return true
      

      【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流,在这里插入图片描述,第2张

      二、验证码

      引入验证码依赖(这是个开源的图形验证码,直接拿过来用):

      
      		
      			com.github.whvcse
      			easy-captcha
      			1.6.2
      		
      		
      			org.openjdk.nashorn
      			nashorn-core
      			15.3
      		
      
          /**
           * @Description: 获取验证码
           * @param user
           * @param goodsId
           * @param response
           * @methodName: verifyCode
           * @return: void
           * @Author: dragon_王
           * @Date: 2024-03-03 12:38:14
           */
          @ApiOperation("获取验证码")
          @GetMapping(value = "/captcha")
          public void verifyCode(User user, Long goodsId, HttpServletResponse response) {
              if (user == null || goodsId < 0) {
                  throw new GlobalException(RespBeanEnum.REQUEST_ILLEGAL);
              }
              //设置请求头为输出图片的类型
              response.setContentType("image/jpg");
              response.setHeader("Pargam", "No-cache");
              response.setHeader("Cache-Control", "no-cache");
              response.setDateHeader("Expires", 0);
              //生成验证码
              ArithmeticCaptcha captcha = new ArithmeticCaptcha(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写的简单前端:

       
      【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流,第3张