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

小明 2025-05-04 00:41:21 11

🏡���泽学编程:个人主页

 🔥 推荐专栏:《深入浅出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  
          

          二、验证码

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

          		
          			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  
          

          这里用的是bootstrap写的简单前端:

           
              
                  
                  
                  立即秒杀
                  
              
           
           
          

          校验验证码逻辑也很简单 (从redis中取出存入的图形结果和输入框中比对):

          /**
               * @Description: 校验验证码
               * @param user
               * @param goodsId
               * @param captcha
               * @methodName: checkCaptcha
               * @return: boolean
               * @Author: dragon_王
               * @Date: 2024-03-03 15:48:13
               */
              public boolean checkCaptcha(User user, Long goodsId, String captcha) {
                  if (user == null || goodsId  
          


          总结

          以上就是用redis+自定义注解+Lua脚本+拦截器限制访问接口次数和验证码的方式实现简单限流。

The End
微信