滑动窗口版限流算法-----------线程安全版写法

本文介绍了一种适用于多线程环境下的滑动窗口限流算法实现方式,该算法利用CAS操作及JUC包中的原子类确保线程安全,并通过窗口数组模拟队列进行请求计数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇文章贴了滑动窗口版限流算法单线程版,也就是再单线程的情况下是正常运行的,那么这篇文章就贴一下在多线程的情况下线程安全的写法,主要运用的是 cas,以及JUC的一些原子类,代码内部有注释,这边就不详细说明了,如果有错误的地方,欢迎指正!

package com.ccc;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: chencc
 */
public class ConcurrentSlidingWindowRateLimit {
    /**
     * 窗口大小
     */
    private int windowSize;
    /**
     * 最大请求数
     */
    private int maxRequestNum;
    /**
     * 以1000毫秒为一个分割(qps)
     */
    private final int LIMIT_UNIT = 1000;
    /**
     * 窗口数组 模拟队列
     */
    private Window[] windowArr;
    /**
     * 窗口数组的长度
     */
    private int arrLen;
    /**
     * 尾下标
     */
    private AtomicInteger tailIndex;
    /**
     * 头下标
     */
    private AtomicInteger headIndex;
    /**
     * 当前请求数
     */
    private AtomicInteger currentRequestNum;

    class Window {
        /**
         * 窗口的起点
         */
        private long start;
        /**
         * 在这个区间内的请求数
         */
        private AtomicInteger requestNum;
        /**
         * 当前窗口的状态 1激活状态  -1无效状态
         */
        private AtomicInteger state;

        public Window() {
            this.requestNum = new AtomicInteger(0);
            this.state = new AtomicInteger(-1);
        }

        public Window(int start, int requestNum) {
            this.start = start;
            this.requestNum = new AtomicInteger(0);
        }
    }

    public ConcurrentSlidingWindowRateLimit(int maxRequestNum) {
        this(100, maxRequestNum);
    }

    public ConcurrentSlidingWindowRateLimit(int windowSize, int maxRequestNum) {
        if (LIMIT_UNIT % windowSize != 0) {
            throw new IllegalArgumentException(LIMIT_UNIT + " % windowSize must is 0");
        }
        this.windowSize = windowSize;
        this.maxRequestNum = maxRequestNum;
        this.arrLen = getLen(LIMIT_UNIT / windowSize);
        this.windowArr = new Window[this.arrLen];
        tailIndex = new AtomicInteger(0);
        headIndex = new AtomicInteger(0);
        currentRequestNum = new AtomicInteger(0);
        // 预先初始化window对象
        initWindowArr(this.arrLen);
        // 方便位运算取模的时候不用再减一了
        this.arrLen--;
    }

    private void initWindowArr(int len) {
        for (int n = 0; n < len; n++) {
            windowArr[n] = new Window();
        }
    }

    /**
     * 判断当前这个请求是否能够通过
     * @return
     */
    public boolean entry() {
        // 获得当前时间
        long currentTime = System.currentTimeMillis();
        int index;
        // 判断窗口是否可以移出去
        while (enableRemoveWindow(currentTime, index = tailIndex.get())) {
            // 删除窗口
            removeTailWindow(index);
        }
        return doEntry(currentTime);
    }

    private boolean doEntry(long time) {
        int index, count = 0;
        for (;currentRequestNum.get() < maxRequestNum;) {
            // 如果头下标等于尾下标
            if ((index = headIndex.get()) == tailIndex.get()) {
                activateWindow(time, index);
            }
            // 获得当前最新的window对象下标索引
            int headWindowIndex = getCurrentHeadWindowIndex();
            Window currentHeadWindow = windowArr[headWindowIndex];
            // 如果处于未激活状态 自旋等待
            if (currentHeadWindow.state.get() < 0) {
                continue;
            }
            // 如果window的开始时间已经大于等于当前这个时间
            // 如果count == 0 尝试更新一次时间
            // 如果count == 1 代表已经更新过一次了 可能在里面 自旋太久了 直接返回false
            if (currentHeadWindow.start > time) {
                if (count == 0) {
                    time = System.currentTimeMillis();
                    count++;
                    continue;
                }
                return false;
            }
            // 如果不需要新生成一个窗口
            if (!check(currentHeadWindow, time)) {
                int requestNum;
                // 如果当前请求数量小于最大请求量  并且 窗口状态正常 一直尝试cas 直到成功或者条件不成立
                for (;(requestNum = currentRequestNum.get()) < maxRequestNum && currentHeadWindow.start <= time && currentHeadWindow.state.get() > 0;) {
                    // 如果cas成功的话
                    if (currentRequestNum.compareAndSet(requestNum, requestNum + 1)) {
                        currentHeadWindow.requestNum.incrementAndGet();
                        return true;
                    }
                }
            } else {
                // 如果当前时间已经和最新的那个窗口相差了至少一个窗口大小 需要新激活一个窗口 再次检查,防止中间已经变化了
                if (headWindowIndex == getCurrentHeadWindowIndex() && check(currentHeadWindow = windowArr[headWindowIndex], time)) {
                    activateWindow(getStartTime(currentHeadWindow, time), (headWindowIndex + 1) & arrLen);
                }
            }
        }
        return false;
    }

    /**
     * 当这个窗口和这个时间不在一个窗口内,获得这个时间应该在的窗口的startTime(可能中间时间已经相差了好几个窗口)
     * @param window
     * @param time
     * @return
     */
    public long getStartTime(Window window, long time) {
        long start = window.start;
        long interval = time - start;
        // 获得相差窗口的个数
        long num = interval / windowSize;
        return num * windowSize + start;
    }

    /**
     * 检查是否需要新生成一个窗口,也就是这个时间是否在这个窗口内
     * @param currentHeadWindow
     * @param time
     * @return
     */
    private boolean check(Window currentHeadWindow, long time) {
        return currentHeadWindow.start + windowSize <= time;
    }

    /**
     * 获得当前最新的window对象的下标索引
     * @return
     */
    private int getCurrentHeadWindowIndex() {
        if (headIndex.get() == 0) {
            return arrLen;
        }
        return headIndex.get() - 1;
    }

    /**
     * 激活对应下标窗口
     * @param startTime
     * @param index
     */
    private void activateWindow(long startTime, int index) {
        Window window = windowArr[index];
        // 可能这个窗口的状态不是为失效的状态
        if (window.state.get() == 1) {
            return;
        }
        if (headIndex.compareAndSet(index, (index + 1) & arrLen)) {
            // 激活窗口
            window.requestNum.set(0);
            window.start = startTime;
            window.state.set(1);
        }
        return;
    }

    /**
     * 移动尾下标 来表示移除掉窗口
     */
    private void removeTailWindow(int index) {
        // 如果cas成功的话
        if (tailIndex.compareAndSet(index, (index + 1) & arrLen)) {
            Window window = windowArr[index];
            currentRequestNum.addAndGet(-window.requestNum.get());
            window.state.set(-1);
        }
    }

    /**
     * 判断是否能够移除掉窗口
     * @param time
     * @return
     */
    private boolean enableRemoveWindow(long time, int index) {
        if (index == headIndex.get()) {
            return false;
        }
        return time - windowArr[index].start >= LIMIT_UNIT;
    }

    /**
     * 获得大于len的最小2的N次方的数
     * 参考 ArrayDeque 里面的 源码 基本拷贝的 哈哈
     * 为了 % 可以使用位运算
     * @param len
     * @return
     */
    private int getLen(int len) {
        len |= (len >>>  1);
        len |= (len >>>  2);
        len |= (len >>>  4);
        len |= (len >>>  8);
        len |= (len >>> 16);
        len++;
        if (len < 0) {
            len >>>= 1;
        }
        return len;
    }
}

掌握数据结构和算法,可称为算法工程师!这是成为架构师的基础,有Google算法大神亲授。我保证你可以写出时空复杂度都很优的架构。有人专门答疑哟!你想要成为架构工程师吗?立即点击报名按钮吧!北上广容不下肉身,三四线放不下灵魂,程序员里没有穷人,有一种土豪叫 算法工程师。程序 = 数据结构 + 算法程序是为了解决实际问题而存在的。然而为了解决问题,必定会使用到某些数据结构以及设计一个解决这种数据结构的算法。如果说各种编程语言是程序员的招式,那么数据结构和算法就相当于程序员的内功。编程实战算法,不是念PPT,我们讲的就是实战与代码实现与企业应用。程序 = 数据结构 + 算法           ——图灵奖得主,计算机科学家N.Wirth(沃斯)作为程序员,我们做机器学习也好,做python开发也好,java开发也好。有一种对所有程序员无一例外的刚需 —— 算法与数据结构日常增删改查 + 粘贴复制 + 搜索引擎可以实现很多东西。同样,这样也是没有任何竞争力的。我们只可以粘贴复制相似度极高的功能,稍复杂的逻辑没有任何办法。语言有很多,开发框架更是日新月异3个月不学就落后我们可以学习很多语言,很多框架,但招聘不会考你用5种语言10种框架实现同一个功能。真正让程序员有区分度,企业招聘万年不变的重点 —— 算法与数据结构。算法代表程序员水平的珠穆朗玛。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值