首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

如何实现一个通用的接口限流、防重、防抖机制

编程知识
2024年09月01日 12:16

介绍

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

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

接口限流

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

限流框架大概有

  1. spring cloud gateway集成redis限流,但属于网关层限流
  2. 阿里Sentinel,功能强大、带监控平台
  3. srping cloud hystrix,属于接口层限流,提供线程池与信号量两种方式
  4. 其他:redission、redis手撸代码

本文主要是通过 Redission 的分布式计数来实现的 固定窗口 模式的限流,也可以通过 Redission 分布式限流方案(令牌桶)的的方式RRateLimiter。

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

  • 自定义接口限流注解类 @AccessLimit
/**
 * 接口限流
 */
@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实现接口限流
/**
 * 通过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)));
    }
}
  • 调用示例实现
@GetMapping("/getUser")
@AccessLimit(times = 10, timeUnit = TimeUnit.SECONDS, maxCount = 5, preKey = "getUser", msg = "服务请求达到最大限制,请求被拒绝!")
public Result getUser() {
    return Result.success("成功访问");
}

防重复提交

在一些业务场景中,重复提交同一个请求可能会导致数据的不一致,甚至严重影响业务逻辑的正确性。例如,在提交订单的场景中,重复提交可能会导致用户被多次扣款。为了避免这种情况,可以使用防重复提交技术,这对于保护数据一致性、避免资源浪费非常重要

  • 自定义接口防重注解类 @RepeatSubmit
/**
* 自定义接口防重注解类
*/
@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实现接口防重处理
/**
 * 利用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)));
    }
}
  • 调用示例
@PostMapping("/saveUser")
@RepeatSubmit(limitType = RepeatSubmit.Type.PARAM,lockTime = 5,timeUnit = TimeUnit.SECONDS,preKey = "saveUser",msg = "请求重复提交")
public Result saveUser() {
    return Result.success("成功保存");
}

接口防抖

接口防抖是一种优化用户操作体验的技术,主要用于减少短时间内高频率触发的操作。例如,当用户快速点击按钮时,我们可以通过防抖机制,只处理最后一次触发的操作,而忽略前面短时间内的多次操作。防抖技术常用于输入框文本变化事件、按钮点击事件等场景,以提高系统的性能和用户体验。

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

  • 定义自定义注解 @AntiShake
// 该注解只能用于方法
@Target(ElementType.METHOD) 
// 运行时保留,这样才能在AOP中被检测到
@Retention(RetentionPolicy.RUNTIME) 
public @interface AntiShake {
    // 默认防抖时间1秒,单位秒
    long value() default 1000L; 
}
  • 实现AOP切面处理防抖
@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()) {
            // 如果距离上次调用时间小于指定的防抖时间,则直接返回,不执行方法
            return null; // 或者根据业务需要返回特定值
        }
        
        lastInvokeTime.set(currentTime);
        return joinPoint.proceed(); // 执行原方法
    }
}
  • 调用示例代码
@PostMapping("/clickButton")
@AntiShake(value = 1000)
public Result clickButton() {
    return Result.success("成功点击按钮");
}

关于作者

来自一线程序员Seven的探索与实践,持续学习迭代中~

本文已收录于我的个人博客:https://www.seven97.top

公众号:seven97,欢迎关注~

From:https://www.cnblogs.com/seven97-top/p/18391209
本文地址: http://shuzixingkong.net/article/1629
0评论
提交 加载更多评论
其他文章 一种优雅的方式整合限流、幂等、防盗刷
大家在工作中肯定遇到过接口被人狂刷的经历,就算没有经历过,在接口开发的过程中,我们也需要对那些容易被刷的接口或者和会消耗公司金钱相关的接口增加防盗刷功能。例如,发送短信接口以及发送邮件等接口,我看了国内很多产品的短信登录接口,基本上都是做了防盗刷,如果不做的话,一夜之间,也许公司都赔完了┭┮﹏┭┮。
一种优雅的方式整合限流、幂等、防盗刷 一种优雅的方式整合限流、幂等、防盗刷 一种优雅的方式整合限流、幂等、防盗刷
gcc/g++编译
编译工具链 我们写程序的时候用的都是集成开发环境 (IDE: Integrated Development Environment),集成开发环境可以极大地方便我们程序员编写程序,但是配置起来也相对麻烦。在 Linux 环境下,我们用的是编译工具链,又叫软件开发工具包(SDK:Software De
gcc/g++编译 gcc/g++编译 gcc/g++编译
Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤
Dify简介 Dify是一个开源的大语言模型(Large Language Model, LLM)应用开发平台。它融合了后端即服务(Backend as a Service, BaaS)和LLMOps的理念,旨在帮助开发者,甚至是非技术人员,能够快速搭建和部署生成式AI应用程序。 Dify的主要特点
Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤 Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤 Dify大语言模型应用开发平台新手必备:安装注册与私有服务器部署全步骤
一个操作系统的设计与实现——第23章 快速系统调用
23.1 什么是快速系统调用 系统调用是操作系统为3特权级任务提供服务的一种手段。在32位操作系统中,我们通过中断实现了系统调用。由于系统调用是一个使用非常频繁的机制,且中断也不是专门为系统调用设计的,因此,64位CPU提供了系统调用的专用机制:快速系统调用。 快速系统调用由专用的syscall指令
使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析
这篇文章将用三个版本的归并排序,为大家分析使用协程排序的时间开销(被排序的切片长度由128到1000w)
使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析 使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析 使用Golang的协程竟然变慢了|100万个协程的归并排序耗时分析
Python自动复制Excel数据:将各行分别重复指定次数
本文介绍基于Python语言,读取Excel表格文件数据,并将其中符合我们特定要求的那一行加以复制指定的次数,而不符合要求的那一行则不复制;并将所得结果保存为新的Excel表格文件的方法~
Python自动复制Excel数据:将各行分别重复指定次数 Python自动复制Excel数据:将各行分别重复指定次数 Python自动复制Excel数据:将各行分别重复指定次数
iptables 工作过程整理
转载注明出处: 1.概念和工作原理 iptables是Linux系统中用来配置防火墙的命令。iptables是工作在TCP/IP的二、三、四层,当主机收到一个数据包后,数据包先在内核空间处理,若发现目标地址是自身,则传到用户空间中交给对应的应用程序处理,若发现目标不是自身,则会将包丢弃或进行转发。
iptables 工作过程整理 iptables 工作过程整理
Go plan9 汇编:手写汇编
原创文章,欢迎转载,转载请注明出处,谢谢。 0. 前言 在 Go plan9 汇编: 打通应用到底层的任督二脉 一文中介绍了从应用程序到汇编指令的转换。本文将结合汇编和 Go 程序实现手写基本的汇编指令,以加深对 Go plan9 汇编的了解。 1. 手写汇编 1.1 全局变量 首先写一个打印整型变
Go plan9 汇编:手写汇编 Go plan9 汇编:手写汇编 Go plan9 汇编:手写汇编