使用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)

相关推荐
袁庭新4 分钟前
Cannal实现MySQL主从同步环境搭建
java·数据库·mysql·计算机·java程序员·袁庭新
无尽的大道5 分钟前
深入理解 Java 阻塞队列:使用场景、原理与性能优化
java·开发语言·性能优化
岁岁岁平安22 分钟前
springboot实战(15)(注解@JsonFormat(pattern=“?“)、@JsonIgnore)
java·spring boot·后端·idea
Oak Zhang23 分钟前
TheadLocal出现的内存泄漏具体泄漏的是什么?弱引用在里面有什么作用?什么情景什么问题?
java·系统安全
数据小小爬虫25 分钟前
如何利用Java爬虫获得1688店铺详情
java·开发语言
天若有情67326 分钟前
c++框架设计展示---提高开发效率!
java·c++·算法
Reese_Cool1 小时前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
TheITSea1 小时前
云服务器宝塔安装静态网页 WordPress、VuePress流程记录
java·服务器·数据库
AuroraI'ncoding2 小时前
SpringMVC接收请求参数
java
九圣残炎2 小时前
【从零开始的LeetCode-算法】3354. 使数组元素等于零
java·算法·leetcode