当前位置:   article > 正文

SpringBoot 接口访问频率限制(一)_springboot限制ip访问次数

springboot限制ip访问次数

前言
限流,是服务或者应用对自身保护的一种手段,通过限制或者拒绝调用方的流量,来保证自身的负载。限流是保护高并发系统的三把利器之一,另外两个是缓存和降级。限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等
功能说明
在Spring Boot应用程序中实施接口访问频率限制(也称为速率限制或节流)是非常重要的,原因主要有以下几点:

  1. 防止服务滥用:如果没有频率限制,恶意用户或系统可能会发送大量的请求到服务接口,导致服务过载或崩溃。通过限制单个用户或IP地址的请求频率,可以有效地防止服务被滥用。
  2. 保护系统资源:限制请求频率可以确保系统资源(如CPU、内存、数据库连接等)得到合理分配,防止某个接口或功能消耗过多资源,影响其他正常用户的体验。
  3. 提高系统稳定性:当接口请求量激增时,如果没有合理的频率限制,系统可能会因为处理大量请求而变得不稳定。通过限制请求频率,可以确保系统在高并发场景下依然能够稳定运行。
  4. 维护业务安全:一些关键的业务接口,如用户注册、密码重置等,如果不加以限制,可能会被恶意用户利用,通过大量请求尝试破解或攻击。频率限制可以有效地提高这些接口的安全性。
  5. 提升用户体验:当某个接口被大量请求占用时,其他正常用户的请求可能会受到阻塞或延迟。通过实施频率限制,可以确保每个用户都能获得稳定且响应迅速的服务。

为了更灵活地应对不同的场景和需求,我们可以采用策略模式来实现接口访问频率限制。策略模式是一种行为设计模式,它使你能在运行时改变对象的行为。在频率限制的实现中,策略模式允许我们定义一系列的算法,并将每一个算法封装起来,使它们可以互相替换。这样,我们就可以根据不同的业务场景,动态地选择并应用适合的频率限制策略。

在策略模式中,我们通常定义一个策略接口,其中包含实现频率限制算法所需的方法。
限流策略定义:

public class FrequencyControlDTO {
    /**
     * 代表频控的Key 如果target为Key的话 这里要传值用于构建redis的Key target为Ip或者UID的话会从上下文取值 Key字段无需传值
     */
    private String key;
    /**
     * 频控时间范围,默认单位秒
     *
     * @return 时间范围
     */
    private Integer time;

    /**
     * 频控时间单位,默认秒
     *
     * @return 单位
     */
    private TimeUnit unit;

    /**
     * 单位时间内最大访问次数
     *
     * @return 次数
     */
    private Integer count;
    //时间换算毫秒
    public long getTimeInMillis() {
        return unit.toMillis(time);
    }
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

限流策略接口定义:
然后,我们为每个具体的算法实现一个策略类,这些类都实现了相同的策略接口。在Spring Boot应用中,我们可以将这些策略类作为Spring的Bean进行注册,以便在需要时通过依赖注入的方式使用它们。

public interface FrequencyControl {

    /**
     * 单限流策略的调用方法-编程式调用
     *
     * @param frequencyControl 单个频控对象
     * @param supplier         服务提供着
     * @return 业务方法执行结果
     * @throws Throwable
     */
    public <T> T executeWithFrequencyControl(FrequencyControlDTO frequencyControl, Supplier<T> supplier) throws Throwable;

    /**
     * 多限流策略的编程式调用方法 无参的调用方法
     *
     * @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
     * @param supplier             函数式入参-代表每个频控方法执行的不同的业务逻辑
     * @return 业务方法执行的返回值
     * @throws Throwable 被限流或者限流策略定义错误
     */
    public <T> T executeWithFrequencyControlList(List<FrequencyControlDTO> frequencyControlList, Supplier<T> supplier) throws Throwable;


    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

当接口请求到达时,我们根据预设的规则或配置选择相应的策略类,并执行其定义的频率限制算法。如果请求符合频率限制的要求,则允许其继续处理;否则,可以拒绝该请求或采取其他相应的措施。
策略实现
固定频率的限流算法伪代码实现思路主要依赖于记录请求到达的时间戳,并计算在给定的时间窗口内请求的数量。如果请求数量超过了设定的阈值,则触发限流机制。

/**
 * 固定限流类
 */
@Slf4j
@Service
public class TotalCountWithInFixTimeFrequencyController implements FrequencyControl{
    @Override
    public <T> T executeWithFrequencyControl(FrequencyControlDTO frequencyControl, Supplier<T> supplier) throws Throwable {
       return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier);
    }

    @Override
    public <T> T executeWithFrequencyControlList(List<FrequencyControlDTO> frequencyControlList, Supplier<T> supplier) throws Throwable {
        boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
        if (existsFrequencyControlHasNullKey) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "限流策略的Key字段不允许出现空值");
        }
        Map<String, FrequencyControlDTO> frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(FrequencyControlDTO::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
        return executeWithFrequencyControlMap((Map<String, FrequencyControlDTO>) frequencyControlDTOMap, supplier);
    }

    /**
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     * @param supplier            函数式入参-代表每个频控方法执行的不同的业务逻辑
     * @return 业务方法执行的返回值
     * @throws Throwable
     */

    private <T> T executeWithFrequencyControlMap(Map<String, FrequencyControlDTO> frequencyControlMap, Supplier<T> supplier) throws Throwable {
        boolean reachRateLimit = false;
        // 批量获取Redis中统计的值
        List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
        List<Integer> countList = RedisUtils.mget(frequencyKeys, Integer.class);
        // 遍历检查每个操作的频率是否超过限制
        for (int i = 0; i < frequencyKeys.size(); i++) {
            String key = frequencyKeys.get(i);
            Integer count = countList.get(i);
            int frequencyControlCount = frequencyControlMap.get(key).getCount();

            if (Objects.nonNull(count) && count >= frequencyControlCount) {
                // 如果频率超过限制,记录警告并设置达到频率限制的标志
                log.warn("frequencyControl limit key:{},count:{}", key, count);
                reachRateLimit = true;
            }
        }
        if (reachRateLimit) {
            // 如果达到频率限制,抛出操作频繁的异常
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作过于频繁,请稍后再试");
        }
        try {
            // 执行供应函数并返回结果
            return supplier.get();
        } finally {
            // 无论成功或失败,都增加对应操作的计数
            frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit()));
        }
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

在这里插入图片描述
对于不同的频率限制算法都需要executeWithFrequencyControlMap方法,因此可以抽出一个抽象类将这个方法变成所有策略下的公共方法,每种算法的第一块和第三块是不同的,可以将executeWithFrequencyControlMap抽出三个部分。
在这里插入图片描述
将不同策略的公共部分抽象出来

public abstract class AbstractFrequencyControlService implements FrequencyControl{
    @Override
    public <T> T executeWithFrequencyControl(FrequencyControlDTO frequencyControl, Supplier<T> supplier)  throws Throwable {
        return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier);
    }

    @Override
    public <T> T executeWithFrequencyControlList(List<FrequencyControlDTO> frequencyControlList, Supplier<T> supplier) throws Throwable {
        boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
        if(existsFrequencyControlHasNullKey){
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "限流策略的Key字段不允许出现空值");
        }
        Map<String, FrequencyControlDTO> frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(FrequencyControlDTO::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
        return executeWithFrequencyControlMap((Map<String, FrequencyControlDTO>) frequencyControlDTOMap, supplier);
    }

    /**
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     * @param supplier            函数式入参-代表每个频控方法执行的不同的业务逻辑
     * @return 业务方法执行的返回值
     * @throws Throwable
     */
    private <T> T executeWithFrequencyControlMap(Map<String, FrequencyControlDTO> frequencyControlMap, Supplier<T> supplier) throws Throwable {
        if (reachRateLimit(frequencyControlMap)) {
            throw new BusinessException(ErrorCode.OPERATION_ERROR, "操作过于频繁,请稍后再试");
        }
        try {
            return supplier.get();
        } finally {
            //不管成功还是失败,都增加次数
            addFrequencyControlStatisticsCount(frequencyControlMap);
        }
    }

    /**
     * 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
     *
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     * @return true-方法被限流 false-方法没有被限流
     */
    protected abstract boolean reachRateLimit(Map<String, FrequencyControlDTO> frequencyControlMap);
    /**
     * 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
     *
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     */
    protected abstract void addFrequencyControlStatisticsCount(Map<String, FrequencyControlDTO> frequencyControlMap);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

每个策略只需要实现reachRateLimit和addFrequencyControlStatisticsCount就可以了

 /**
 * 固定限流类
 */
@Slf4j
@Service
public class TotalCountWithInFixTimeFrequencyController extends AbstractFrequencyControlService{

    /**
     * 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
     *
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     * @return true-方法被限流 false-方法没有被限流
     */
    protected boolean reachRateLimit(Map<String, FrequencyControlDTO> frequencyControlMap) {
        //批量获取redis统计的值
        List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
        List<Integer> countList = RedisUtils.mget(frequencyKeys, Integer.class);
        // 遍历检查每个操作的频率是否超过限制
        for (int i = 0; i < frequencyKeys.size(); i++) {
            String key = frequencyKeys.get(i);
            Integer count = countList.get(i);
            int frequencyControlCount = frequencyControlMap.get(key).getCount();

            if (Objects.nonNull(count) && count >= frequencyControlCount) {
                // 如果频率超过限制,记录警告并设置达到频率限制的标志
                log.warn("frequencyControl limit key:{},count:{}", key, count);
                return true;
            }
        }
        return false;
    }

    /**
     * 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
     *
     * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
     */
    protected void addFrequencyControlStatisticsCount(Map<String, FrequencyControlDTO> frequencyControlMap) {
        frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit()));
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

滑动窗口算法是对固定频率算法的一种改进,它允许统计的时间窗口动态滑动,而不是固定的时间段。
伪代码实现思路:

  1. 为每个限流键维护一个时间窗口,记录每个时间窗口内的请求次数。
  2. 当请求到来时,更新对应时间窗口的请求次数。
  3. 如果当前时间窗口的请求次数超过阈值,则限流。
  4. 随着时间的推移,旧的时间窗口会逐渐被新的时间窗口替代。
/**
 * 滑动窗口限流
 */
@Slf4j
@Service
public class SimpleSlidingWindowFrequencyController extends AbstractFrequencyControlService{


    /**
     * 检查当前请求是否超过速率限制。
     *
     * @param frequencyControlMap 用于存储频率控制信息的映射,键为标识符,值为频率控制DTO。
     * @return 如果当前请求超过速率限制,则返回true;否则返回false。
     */
    @Override
    protected boolean reachRateLimit(Map<String, FrequencyControlDTO> frequencyControlMap) {
        long currentTimeMillis = Instant.now().toEpochMilli();

        for (Map.Entry<String, FrequencyControlDTO> entry : frequencyControlMap.entrySet()) {
            String key = entry.getKey();
            // 移除滑动窗口之外的时间戳
            long windowStartMillis = currentTimeMillis - entry.getValue().getTimeInMillis();
            RedisUtils.zRemoveRange(key, 0, windowStartMillis);
            // 计算当前窗口内的请求数
            long requestCount = RedisUtils.zCount(key, windowStartMillis, currentTimeMillis);
            if (requestCount >= entry.getValue().getCount()) {
                // 如果当前窗口内的请求数达到或超过最大请求数,则认为超过了速率限制
                return true;
            }
        }

        return false;
    }

    /**
     * 将当前请求的统计信息添加到频率控制映射中。
     *
     * @param frequencyControlMap 用于存储频率控制信息的映射,键为标识符,值为频率控制DTO。
     */
    @Override
    protected void addFrequencyControlStatisticsCount(Map<String, FrequencyControlDTO> frequencyControlMap) {
        long currentTimeMillis = Instant.now().toEpochMilli();

        for (String key : frequencyControlMap.keySet()) {
            // 将当前请求的时间戳添加到Redis的有序集合中
            RedisUtils.zAdd(key, currentTimeMillis, currentTimeMillis);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

通过使用策略模式,我们能够便捷地切换不同的频率限制算法,并根据实际需求对算法进行灵活的扩展和优化。此处,我们仅展示了两种限流算法的实现作为示例,以展示策略模式的强大应用潜力。
策略工厂
依托于spring可以很方便的实现策略工厂

/**
 * 限流策略工厂
 *
 */
@Component
public class FrequencyControlStrategyFactory {
    /**
     * 滑动窗口限流
     */
    public static final String SIMPLE_SLIDING_WINDOW = "simpleSlidingWindow";
    /**
     * 固定限流类
     */
    public static final String TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER = "TotalCountWithInFixTime";

    @Autowired
    private SimpleSlidingWindowFrequencyController simpleSlidingWindowFrequencyController;
    @Autowired
    private TotalCountWithInFixTimeFrequencyController totalCountWithInFixTimeFrequencyController;
    /**
     * 限流策略集合
     */
    static Map<String, AbstractFrequencyControlService> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8);

    @PostConstruct
    public void init() {
        frequencyControlServiceStrategyMap.put(SIMPLE_SLIDING_WINDOW,simpleSlidingWindowFrequencyController);
        frequencyControlServiceStrategyMap.put(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER,totalCountWithInFixTimeFrequencyController);
    }

    /**
     * 根据名称获取策略类
     *
     * @param strategyName 策略名称
     * @return 对应的限流策略类
     */
    public   AbstractFrequencyControlService getFrequencyControllerByName(String strategyName) {
        return  frequencyControlServiceStrategyMap.get(strategyName);
    }


}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

然而,在增加新的算法时,比如新增TokenBucketFrequencyController令牌桶算法,不仅需要创建新的类,还需要对策略工厂类进行相应的修改。虽然这样的操作并不是特别繁琐,但仍然存在一种更符合开闭原则的方式来实现策略工厂。
不妨深入思考一下,为什么目前新增策略时需要手动修改策略工厂呢?如果能够在Spring框架中注册策略时自动通知工厂,岂不是更为便捷?

为了实现这一目标,可以在一个公共的抽象类AbstractFrequencyControlService中新增一个registerMyselfToFactory方法。这样,每种限流算法在继承这个抽象类时,都可以自动调用这个方法将自己注册到策略工厂中。这样一来,每当有新的限流算法被创建时,策略工厂就能自动感知并更新其策略列表,从而无需手动修改工厂类,更加符合开闭原则的要求。

    /**
     * 注册到工厂中
     */
    @PostConstruct
    protected void registerMyselfToFactory() {
        FrequencyControlStrategyFactory.registerFrequencyController(getStrategyName(), this);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

新的策略工厂如下:

public class FrequencyControlStrategyFactory {


    /**
     * 限流策略集合
     */
    static Map<String, AbstractFrequencyControlService> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8);


    /**
     * 将策略类放入工厂
     *
     * @param strategyName                    策略名称
     * @param abstractFrequencyControlService 策略类
     */
    public static void registerFrequencyController(String strategyName, AbstractFrequencyControlService abstractFrequencyControlService) {
        frequencyControlServiceStrategyMap.put(strategyName, abstractFrequencyControlService);
    }

    /**
     * 根据名称获取策略类
     *
     * @param strategyName 策略名称
     * @return 对应的限流策略类
     */
    public   AbstractFrequencyControlService getFrequencyControllerByName(String strategyName) {
        return  frequencyControlServiceStrategyMap.get(strategyName);
    }


}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

每个算法只需要实现getStrategyName就可以自动注册到工厂中了。通过这种方式,我们不仅可以满足当前的需求,还可以轻松应对未来的变化,使系统更加健壮和可扩展。
调用示例:

总结
这篇文本首先介绍了限流的重要性和应用场景,它是服务自我保护的一种方式,通过限制流量以确保系统负载可控,特别是在高并发场景如秒杀抢购中能有效防止系统因流量过大而崩溃。

  1. 在本文中,我们设计并实现了策略模式(Strategy Pattern),定义了一个FrequencyControl接口,提供了两种执行频率限制的方法,分别针对单个限流策略和多个限流策略的场景。通过定义一组可互换的算法或策略,并使它们可以相互替换,从而让程序能够根据需要灵活选择具体的行为实现。
  2. 同时,本文还实现了模板方法模式(Template Method Pattern),抽象类AbstractFrequencyControlService,其中抽象方法reachRateLimitaddFrequencyControlStatisticsCount由各个策略类根据各自的限流逻辑进行实现。将一些步骤延迟到子类中实现,使得子类可以在不改变结构的基础上重写部分方法以适应不同的情况。
  3. 对于策略模式的优化方面,我们进一步强化了开闭原则(Open-Closed Principle)的应用。各个策略实现registerMyselfToFactory在不修改原有代码的基础上,通过新增策略类的方式,使得系统能够在不改动原有代码的前提下增加新的行为策略,从而提升了系统的可维护性和可扩展性。
    SpringBoot 接口访问频率限制(二)
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/874243
推荐阅读
相关标签
  

闽ICP备14008679号