小工具知识网

当前位置: 首页 > 行业知识 > 【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流

行业知识

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

2026-11-09 23:01:52 来源:互联网转载
���泽学编程:个人主页

  推荐专栏:《深入浅出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脚本+拦截器

          主要关心业务逻辑:

          @Componentpublic 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.whvcseeasy-captcha1.6.2org.openjdk.nashornnashorn-core15.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   

          总结

上一篇:狩猎使命新服务器怎么玩?

下一篇:饥荒洞穴服务器怎么加?