使用Java内存级方式 和 Redis Lua 脚本方式实现滑动窗口限流

日常业务系统中,限流一般分为两种限流场景:

  • 1、基于服务自身保护的应用自身限流。比如每一个启动的 springboot 实例服务都可以受理最大每秒150个请求的量,服务需要保护自身不被击垮对启动的每一个服务实例都进行限流处理。
  • 2、基于业务系统入口的总限流。比如某业务对外提供了一个api接口,系统经过实测日常可以承载最大10000每秒的请求频率,但是该接口是提供给很多供应商同时使用的,因业务规则实际需要,要求对某个具体渠道的调用最大限流是50,这种就是在总入口处进行限流)。

其中第1种场景,使用 Java 内存级的限流即可实现。

对于第2种场景,需要使用例如 Redis 这样高性能的共享存储的方式来实现。

基于 Java 代码的限流

本文使用 Java JUC 包中的 ConcurrentSkipListMapConcurrentLinkedQueue 集合来实现滑动窗口限流。

示例一,使用 ConcurrentSkipListMap

java 复制代码
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;

public class SlidingWindowRateLimiter {
    private final long windowSizeMs;
    private final int maxRequests;
    private final ConcurrentSkipListMap<Long, Integer> requestTimestamps;

    public SlidingWindowRateLimiter(long windowSizeMs, int maxRequests) {
        this.windowSizeMs = windowSizeMs;
        this.maxRequests = maxRequests;
        this.requestTimestamps = new ConcurrentSkipListMap<>();
    }

    public boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        long startTime = currentTime - windowSizeMs;

        // 移除超过时间窗口的请求
        requestTimestamps.headMap(startTime, false).clear();

        // 统计当前时间窗口内的请求数量
        int currentRequests = requestTimestamps.size();

        if (currentRequests < maxRequests) {
            requestTimestamps.put(currentTime, 1);
            return true;
        } else {
            return false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(1000, 5);

        for (int i = 0; i < 10; i++) {
            if (limiter.allowRequest()) {
                System.out.println("Request " + i + " allowed at " + System.currentTimeMillis());
            } else {
                System.out.println("Request " + i + " denied at " + System.currentTimeMillis());
            }
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }
}

示例二,使用 ConcurrentLinkedQueue

java 复制代码
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;

public class SlidingWindowRateLimiter2 {
    private final long windowSizeMs;
    private final int maxRequests;
    private final ConcurrentLinkedQueue<Long> requestTimestamps;

    public SlidingWindowRateLimiter2(long windowSizeMs, int maxRequests) {
        this.windowSizeMs = windowSizeMs;
        this.maxRequests = maxRequests;
        this.requestTimestamps = new ConcurrentLinkedQueue<>();
    }

    public boolean allowRequest() {
        long currentTime = System.currentTimeMillis();
        long startTime = currentTime - windowSizeMs;

        // 移除超过时间窗口的请求
        while (!requestTimestamps.isEmpty() && requestTimestamps.peek() < startTime) {
            requestTimestamps.poll();
        }

        // 统计当前时间窗口内的请求数量
        int currentRequests = requestTimestamps.size();

        if (currentRequests < maxRequests) {
            requestTimestamps.add(currentTime);
            return true;
        } else {
            return false;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(1000, 5);

        for (int i = 0; i < 10; i++) {
            if (limiter.allowRequest()) {
                System.out.println("Request " + i + " allowed at " + System.currentTimeMillis());
            } else {
                System.out.println("Request " + i + " denied at " + System.currentTimeMillis());
            }
            TimeUnit.MILLISECONDS.sleep(100);
        }
    }
}

对比总结:

ConcurrentSkipListMap 的主要特性就是进行查找、插入和删除操作时更高效,内部是基于跳表结构实现的。可以保证Key的顺序。

ConcurrentLinkedQueue 的顾名思义就是队列,查找效率相对较低,但是内存占用比 ConcurrentSkipListMap 少一点。顺序严格安装入队的顺序。

  • 如果时间窗口内的请求数量较大,并且你需要高效的查找和移除操作,推荐使用 ConcurrentSkipListMap。它提供了有序性和高效的 O(log n) 操作,适合大规模数据的处理。
  • 如果时间窗口内的请求数量较小,并且你更关心内存开销和插入/删除的效率,推荐使用 ConcurrentLinkedQueue。它提供了 O(1) 的插入和删除操作,适合小规模数据的处理。

绝大部分的应用,其实不比太纠结,两者随便选用。

基于 Redis 的限流脚本

在实际项目应用中,我们的服务实例是多个的,在内存中使用有序集合来实现限流就不可行了,下面是 Redis 使用 lua 脚本进行限流的脚本,可以参考使用:

bash 复制代码
--KEYS[1]: 限流 key
--ARGV[1]: 限流窗口,毫秒
--ARGV[2]: 当前时间戳(作为score)
--ARGV[3]: 阈值
--ARGV[4]: score 对应的唯一value
-- 1\. 移除开始时间窗口之前的数据
redis.call('zremrangeByScore', KEYS[1], 0, ARGV[2]-ARGV[1])
-- 2\. 统计当前元素数量
local res = redis.call('zcard', KEYS[1])
-- 3\. 是否超过阈值
if (res == nil) or (res < tonumber(ARGV[3])) then
    redis.call('zadd', KEYS[1], ARGV[2], ARGV[4])
    redis.call('expire', KEYS[1], ARGV[1]/1000)
    return 0
else
    return 1
end

(END)

相关推荐
重整旗鼓~2 小时前
28.redisson源码分析分布式锁
java·开发语言
Query*2 小时前
Java 设计模式——工厂模式:从原理到实战的系统指南
java·python·设计模式
懒羊羊不懒@2 小时前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss2732 小时前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父2 小时前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛3 小时前
Spring面试
java·spring·面试
Java中文社群3 小时前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme3 小时前
java.nio 包详解
java·python·nio
零千叶3 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝3 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表