接口限流是一种控制访问频率的技术

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 在高并发场景下,合理的接口限流、防重复提交及接口防抖机制对保障系统稳定性至关重要。本文介绍了如何利用AOP在不改变业务代码的前提下,灵活添加这些功能。具体包括:通过`@AccessLimit`注解实现接口限流,利用Redis进行计数与控制;通过`@RepeatSubmit`注解防止重复提交,确保数据一致性;通过`@AntiShake`注解实现接口防抖,提升用户体验。此外,提供了基于Redisson和Spring Cloud的实现示例。

最近上了一个新项目,考虑到一个问题,在高并发场景下,我们无法控制前端的请求频率和次数,这就可能导致服务器压力过大,响应速度变慢,甚至引发系统崩溃等严重问题。为了解决这些问题,我们需要在后端实现一些机制,如接口限流、防重复提交和接口防抖,而这些是保证接口安全、稳定提供服务,以及防止错误数据 和 脏数据产生的重要手段。

而AOP适合在在不改变业务代码的情况下,灵活地添加各种横切关注点,实现一些通用公共的业务场景,例如日志记录、事务管理、安全检查、性能监控、缓存管理、限流、防重复提交等功能。这样不仅提高了代码的可维护性,还使得业务逻辑更加清晰专注,关于AOP不理解的可以看这篇文章。

接口限流
接口限流是一种控制访问频率的技术,通过限制在一定时间内允许的最大请求数来保护系统免受过载。限流可以在应用的多个层面实现,比如在网关层、应用层甚至数据库层。常用的限流算法有漏桶算法(Leaky Bucket)、令牌桶算法(Token Bucket)等。限流不仅可以防止系统过载,还可以防止恶意用户的请求攻击。

限流框架大概有

spring cloud gateway集成redis限流,但属于网关层限流
阿里Sentinel,功能强大、带监控平台
srping cloud hystrix,属于接口层限流,提供线程池与信号量两种方式
其他:redission、redis手撸代码
本文主要是通过 Redission 的分布式计数来实现的 固定窗口 模式的限流,也可以通过 Redission 分布式限流方案(令牌桶)的的方式RRateLimiter。

在高并发场景下,合理地实施接口限流对于保障系统的稳定性和可用性至关重要。

自定义接口限流注解类 @AccessLimit
java
/**

  • 接口限流
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface AccessLimit {

    /**

    • 限制时间窗口间隔长度,默认10秒
      */
      int times() default 10;

      /**

    • 时间单位
      */
      TimeUnit timeUnit() default TimeUnit.SECONDS;

      /**

    • 上述时间窗口内允许的最大请求数量,默认为5次
      */
      int maxCount() default 5;

      /**

    • redis key 的前缀
      */
      String preKey();

      /**

    • 提示语
      /
      String msg() default "服务请求达到最大限制,请求被拒绝!";
      }
      利用AOP实现接口限流
      java
      /*
  • 通过AOP实现接口限流
    */
    @Component
    @Aspect
    @Slf4j
    @RequiredArgsConstructor
    public class AccessLimitAspect {

    private static final String ACCESS_LIMIT_LOCK_KEY = "ACCESS_LIMIT_LOCK_KEY";

    private final RedissonClient redissonClient;

    @Around("@annotation(accessLimit)")
    public Object around(ProceedingJoinPoint point, AccessLimit accessLimit) throws Throwable {

     String prefix = accessLimit.preKey();
     String key = generateRedisKey(point, prefix);
    
     //限制窗口时间
     int time = accessLimit.times();
     //获取注解中的令牌数
     int maxCount = accessLimit.maxCount();
     //获取注解中的时间单位
     TimeUnit timeUnit = accessLimit.timeUnit();
    
     //分布式计数器
     RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
    
     if (!atomicLong.isExists() || atomicLong.remainTimeToLive() <= 0) {
         atomicLong.expire(time, timeUnit);
     }
    
     long count = atomicLong.incrementAndGet();
     ;
     if (count > maxCount) {
         throw new LimitException(accessLimit.msg());
     }
    
     // 继续执行目标方法
     return point.proceed();
    

    }

    public String generateRedisKey(ProceedingJoinPoint point, String prefix) {

     //获取方法签名
     MethodSignature methodSignature = (MethodSignature) point.getSignature();
     //获取方法
     Method method = methodSignature.getMethod();
     //获取全类名
     String className = method.getDeclaringClass().getName();
    
     // 构建Redis中的key,加入类名、方法名以区分不同接口的限制
     return String.format("%s:%s:%s", ACCESS_LIMIT_LOCK_KEY, prefix, DigestUtil.md5Hex(String.format("%s-%s", className, method)));
    

    }
    }
    调用示例实现
    java
    @GetMapping("/getUser")
    @AccessLimit(times = 10, timeUnit = TimeUnit.SECONDS, maxCount = 5, preKey = "getUser", msg = "服务请求达到最大限制,请求被拒绝!")
    public Result getUser() {
    return Result.success("成功访问");
    }
    防重复提交
    在一些业务场景中,重复提交同一个请求可能会导致数据的不一致,甚至严重影响业务逻辑的正确性。例如,在提交订单的场景中,重复提交可能会导致用户被多次扣款。为了避免这种情况,可以使用防重复提交技术,这对于保护数据一致性、避免资源浪费非常重要

自定义接口防重注解类 @RepeatSubmit
java
/**

  • 自定义接口防重注解类
    /
    @Documented
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RepeatSubmit {
    /*

    • 定义了两种防止重复提交的方式,PARAM 表示基于方法参数来防止重复,TOKEN 则可能涉及生成和验证token的机制
      /
      enum Type { PARAM, TOKEN }
      /*
    • 设置默认的防重提交方式为基于方法参数。开发者可以不指定此参数,使用默认值。
    • @return Type
      */
      Type limitType() default Type.PARAM;

      /**

    • 允许设置加锁的过期时间,默认为5秒。这意味着在第一次请求之后的5秒内,相同的请求将被视为重复并被阻止
      */
      long lockTime() default 5;

      //提供了一个可选的服务ID参数,通过token时用作KEY计算
      String serviceId() default "";

      /**

    • 提示语
      /
      String msg() default "请求重复提交!";
      }
      利用AOP实现接口防重处理
      java
      /*

      • 利用AOP实现接口防重处理
        */
        @Aspect
        @Slf4j
        @RequiredArgsConstructor
        @Component
        public class RepeatSubmitAspect {

      private final String REPEAT_SUBMIT_LOCK_KEY_PARAM = "REPEAT_SUBMIT_LOCK_KEY_PARAM";

      private final String REPEAT_SUBMIT_LOCK_KEY_TOKEN = "REPEAT_SUBMIT_LOCK_KEY_TOKEN";

      private final RedissonClient redissonClient;

      private final RedisRepository redisRepository;

      @Pointcut("@annotation(repeatSubmit)")
      public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {

      }

      /**

    • 环绕通知, 围绕着方法执行
    • 两种方式
    • 方式一:加锁 固定时间内不能重复提交
    • 方式二:先请求获取token,再删除token,删除成功则是第一次提交
      */
      @Around("pointCutNoRepeatSubmit(repeatSubmit)")
      public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

      //用于记录成功或者失败
      boolean res = false;

      //获取防重提交类型
      String type = repeatSubmit.limitType().name();
      if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {

       //方式一,参数形式防重提交
       //通过 redissonClient 获取分布式锁,基于IP地址、类名、方法名生成唯一key
       String ipAddr = IPUtil.getIpAddr(request);
       String preKey = repeatSubmit.preKey();
       String key = generateTokenRedisKey(joinPoint, ipAddr, preKey);
      
       //获取注解中的锁时间
       long lockTime = repeatSubmit.lockTime();
       //获取注解中的时间单位
       TimeUnit timeUnit = repeatSubmit.timeUnit();
      
       //使用 tryLock 尝试获取锁,如果无法获取(即锁已被其他请求持有),则认为是重复提交,直接返回null
       RLock lock = redissonClient.getLock(key);
       //锁自动过期时间为 lockTime 秒,确保即使程序异常也不会永久锁定资源,尝试加锁,最多等待0秒,上锁以后 lockTime 秒自动解锁 [lockTime默认为5s, 可以自定义]
       res = lock.tryLock(0, lockTime, timeUnit);
      

      } else {

       //方式二,令牌形式防重提交
       //从请求头中获取 request-token,如果不存在,则抛出异常
       String requestToken = request.getHeader("request-token");
       if (StringUtils.isBlank(requestToken)) {
           throw new LimitException("请求未包含令牌");
       }
       //使用 request-token 和 serviceId 构造Redis的key,尝试从Redis中删除这个键。如果删除成功,说明是首次提交;否则认为是重复提交
       String key = String.format("%s:%s:%s", REPEAT_SUBMIT_LOCK_KEY_TOKEN, repeatSubmit.serviceId(), requestToken);
       res = redisRepository.del(key);
      

      }

      if (!res) {

       log.error("请求重复提交");
       throw new LimitException(repeatSubmit.msg());
      

      }

      return joinPoint.proceed();
      }

      private String generateTokenRedisKey(ProceedingJoinPoint joinPoint, String ipAddr, String preKey) {
      //根据ip地址、用户id、类名方法名、生成唯一的key
      MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
      Method method = methodSignature.getMethod();
      String className = method.getDeclaringClass().getName();
      String userId = "seven";
      return String.format("%s:%s:%s", REPEAT_SUBMIT_LOCK_KEY_PARAM, preKey, DigestUtil.md5Hex(String.format("%s-%s-%s-%s", ipAddr, className, method, userId)));
      }
      }
      调用示例
      java
      @PostMapping("/saveUser")
      @RepeatSubmit(limitType = RepeatSubmit.Type.PARAM,lockTime = 5,timeUnit = TimeUnit.SECONDS,preKey = "saveUser",msg = "请求重复提交")
      public Result saveUser() {
      return Result.success("成功保存");
      }
      接口防抖
      接口防抖是一种优化用户操作体验的技术,主要用于减少短时间内高频率触发的操作。例如,当用户快速点击按钮时,我们可以通过防抖机制,只处理最后一次触发的操作,而忽略前面短时间内的多次操作。防抖技术常用于输入框文本变化事件、按钮点击事件等场景,以提高系统的性能和用户体验。

后端接口防抖处理主要是为了避免在短时间内接收到大量相同的请求,特别是由于前端操作(如快速点击按钮)、网络重试或异常情况导致的重复请求。后端接口防抖通常涉及记录最近的请求信息,并在特定时间窗口内拒绝处理相同或相似的请求。

定义自定义注解 @AntiShake
java
// 该注解只能用于方法
@Target(ElementType.METHOD)
// 运行时保留,这样才能在AOP中被检测到
@Retention(RetentionPolicy.RUNTIME)
public @interface AntiShake {
// 默认防抖时间1秒,单位秒
long value() default 1000L;
}
实现AOP切面处理防抖
java
@Aspect // 标记为切面类
@Component // 让Spring管理这个Bean
public class AntiShakeAspect {

private ThreadLocal<Long> lastInvokeTime = new ThreadLocal<>();

@Around("@annotation(antiShake)") // 拦截所有标记了@AntiShake的方法
public Object aroundAdvice(ProceedingJoinPoint joinPoint, AntiShake antiShake) throws Throwable {
    long currentTime = System.currentTimeMillis();
    long lastTime = lastInvokeTime.get() != null ? lastInvokeTime.get() : 0;

    if (currentTime - lastTime < antiShake.value()) {
        // 如果距离上次调用时间小于指定的防抖时间,则直接返回,不执行方法

//代码效果参考:https://www.xx-ph.com/sitemap/post.xml
return null; // 或者根据业务需要返回特定值
}

    lastInvokeTime.set(currentTime);
    return joinPoint.proceed(); // 执行原方法
}

}
调用示例代码
java
@PostMapping("/clickButton")
@AntiShake(value = 1000)
public Result clickButton() {
return Result.success("成功点击按钮");
}

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
网络协议 Ubuntu 网络安全
使用VScode SSH公网远程连接本地服务器开发【无公网IP内网穿透】
使用VScode SSH公网远程连接本地服务器开发【无公网IP内网穿透】
|
10月前
|
缓存 资源调度 JavaScript
Vue集成Excalidraw实现在线画板功能
Excalidraw是一款开源在线绘图工具,适用于白板、思维导图、原型设计等场景。支持手绘风格、多种图形元素、导出功能及多人协作,深受开发者喜爱。本文档介绍了如何在Vue项目中集成Excalidraw,包括安装依赖、配置文件修改、页面添加等步骤,帮助开发者快速上手。
1286 0
Vue集成Excalidraw实现在线画板功能
资源块|带你读《5G空口特性与关键技术》之九
3GPP TS38.211 中对 Point A 进行了定义。需要说明的是,2018/6 版本的TS38.211-f20 中的定义在 2018/9 版本中没有变化,不过在2018/12 版本 TS38.211-f40 中,基于 RAN1#94b 会议的决议进行了修改,有关信息请参看 RAN1#94b会议报告以及提案 R1-1811817 和 R11810834。
12098 1
资源块|带你读《5G空口特性与关键技术》之九
|
Java Spring 容器
spring之HttpInvoker
  一、HttpInvoker是常用的Java同构系统之间方法调用实现方案,是众多Spring项目中的一个子项目。顾名思义,它通过HTTP通信即可实现两个Java系统之间的远程方法调用,使得系统之间的通信如同调用本地方法一般。
2617 0
|
11月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
1786 0
|
10月前
|
算法 容器
令牌桶算法原理及实现,图文详解
本文介绍令牌桶算法,一种常用的限流策略,通过恒定速率放入令牌,控制高并发场景下的流量,确保系统稳定运行。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
令牌桶算法原理及实现,图文详解
|
SQL 存储 关系型数据库
6本值得推荐的MySQL学习书籍
本文是关于MySQL学习书籍的推荐,作者在DotNetGuide技术社区和微信公众号收到读者请求后,精选了6本值得阅读的MySQL书籍,包括《SQL学习指南(第3版)》、《MySQL是怎样使用的:快速入门MySQL》、《MySQL是怎样运行的:从根儿上理解MySQL》、《深入浅出MySQL:数据库开发、优化与管理维护(第3版)》以及《高性能MySQL(第4版)》和《MySQL技术内幕InnoDB存储引擎(第2版)》。此外,还有12本免费书籍的赠送活动,涵盖《SQL学习指南》、《MySQL是怎样使用的》等,赠书活动有效期至2024年4月9日。
3723 0
|
12月前
|
C# 数据安全/隐私保护
C# 一分钟浅谈:类与对象的概念理解
【9月更文挑战第2天】本文从零开始详细介绍了C#中的类与对象概念。类作为一种自定义数据类型,定义了对象的属性和方法;对象则是类的实例,拥有独立的状态。通过具体代码示例,如定义 `Person` 类及其实例化过程,帮助读者更好地理解和应用这两个核心概念。此外,还总结了常见的问题及解决方法,为编写高质量的面向对象程序奠定基础。
141 3
|
异构计算
Cesium中用到的图形技术——Horizon Culling
Cesium中用到的图形技术——Horizon Culling
257 0
|
XML Java 开发者
“掌握Spring IoC和AOP:30道面试必备问题解析!“
“掌握Spring IoC和AOP:30道面试必备问题解析!“
415 0