最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于Redis+Lua腳本的分布式全局令牌桶限流方案

 更新時(shí)間:2026年05月25日 09:41:52   作者:洋羊_2401  
這篇文章主要介紹了如何采用Redis和Lua腳本實(shí)現(xiàn)分布式全局令牌桶限流,解決多實(shí)例部署、并發(fā)超賣及限流一致性問(wèn)題,保障高并發(fā)場(chǎng)景下流量控制與系統(tǒng)穩(wěn)定性,需要的朋友可以參考下

采用 Redis + Lua 腳本 實(shí)現(xiàn)分布式全局令牌桶限流,核心目的是:在多實(shí)例集群部署環(huán)境下,實(shí)現(xiàn)統(tǒng)一、精準(zhǔn)、原子化的接口限流控制,解決單機(jī)限流無(wú)法控制全局總流量、并發(fā)超賣、流量失控等問(wèn)題,覆蓋接口、用戶多維度限流場(chǎng)景,保障系統(tǒng)在高并發(fā)場(chǎng)景下的穩(wěn)定性與可用性。

一:具體解決的五大問(wèn)題

1、解決多實(shí)例部署下,總流量無(wú)法精準(zhǔn)控制的問(wèn)題

  • 單機(jī)限流:每臺(tái)機(jī)器各自計(jì)數(shù),總流量 = 單臺(tái)限額 × 實(shí)例數(shù),遠(yuǎn)超預(yù)期閾值
  • 例如:你部署了3 臺(tái)服務(wù)器實(shí)例,想做「接口每秒最多 10 次請(qǐng)求」的限流。
    如果用單機(jī)限流(比如每臺(tái)實(shí)例自己記請(qǐng)求數(shù)):
    • 每臺(tái)實(shí)例自己都放 10 次請(qǐng)求
    • 3 臺(tái)實(shí)例加起來(lái),系統(tǒng)總 QPS 變成了 10×3=30
    • 你本來(lái)想限制 10,結(jié)果變成了 30,流量不可控,這就是問(wèn)題所在!
  • Redis 全局限流:所有服務(wù)實(shí)例共用同一個(gè) Redis 計(jì)數(shù)器,全局總流量嚴(yán)格等于配置閾值。

2、解決并發(fā)請(qǐng)求下的超賣 / 超限制問(wèn)題

  • 傳統(tǒng) Java 級(jí)限流在高并發(fā)下會(huì)出現(xiàn)競(jìng)態(tài)條件,導(dǎo)致實(shí)際請(qǐng)求數(shù)超過(guò)限額。
  • Lua 腳本在 Redis 中單線程原子執(zhí)行,判斷、扣減、回收一氣呵成,絕對(duì)不會(huì)超發(fā)令牌

3、解決分布式環(huán)境下的限流一致性問(wèn)題

  • 無(wú)論請(qǐng)求打到哪一臺(tái)服務(wù)器,都統(tǒng)一去 Redis 中獲取令牌。
  • 保證全集群限流規(guī)則一致,不會(huì)出現(xiàn)某臺(tái)機(jī)器松、某臺(tái)機(jī)器嚴(yán)的情況。

4、解決固定窗口限流的 “邊界突刺” 問(wèn)題(令牌桶優(yōu)勢(shì))

  • 令牌桶支持平滑流量,令牌勻速生成、自動(dòng)回收。
  • 避免傳統(tǒng)計(jì)數(shù)器在窗口切換瞬間出現(xiàn)流量突刺,壓垮服務(wù)。

5、保障高并發(fā)下接口的可用性與自我保護(hù)

  • 超出限流規(guī)則的請(qǐng)求直接攔截,防止大量無(wú)效請(qǐng)求壓垮數(shù)據(jù)庫(kù) / 第三方接口。
  • 配合降級(jí)機(jī)制,返回友好提示,提升用戶體驗(yàn)。

二:令牌桶算法介紹

原理:系統(tǒng)以恒定的速度向令牌桶中放入令牌,當(dāng)請(qǐng)求到達(dá)時(shí),需要從桶中獲取一個(gè)令牌才能被處理。如果桶中沒(méi)有令牌,則拒絕請(qǐng)求。令牌桶的容量是固定的,當(dāng)令牌放滿時(shí),多余的令牌會(huì)被丟棄。

核心優(yōu)勢(shì):

  • 允許突發(fā)流量:令牌桶可以積累令牌,當(dāng)系統(tǒng)空閑時(shí),令牌會(huì)逐漸填滿桶,此時(shí)如果有突發(fā)流量到來(lái),可以一次性獲取多個(gè)令牌進(jìn)行處理,充分利用系統(tǒng)資源。
  • 平滑限流:令牌以恒定速度放入桶中,避免了固定窗口的臨界問(wèn)題,使請(qǐng)求處理速度更加平滑。
  • 易于實(shí)現(xiàn)多維度限流:每個(gè)限流維度(如接口、IP、用戶)可以維護(hù)獨(dú)立的令牌桶,互不干擾。

令牌桶算法的數(shù)學(xué)模型:

  • 設(shè)令牌桶容量為max_tokens(即最大突發(fā)請(qǐng)求數(shù))
  • 令牌生成速率為rate(即每秒生成的令牌數(shù),等于限流 QPS)
  • 當(dāng)前令牌數(shù)為current_tokens
  • 當(dāng)請(qǐng)求到達(dá)時(shí),若current_tokens >= 1,則current_tokens -= 1,請(qǐng)求被處理;否則拒絕請(qǐng)求。
  • 每隔 1/rate 秒,current_tokens += 1,但不超過(guò)max_tokens。

代碼實(shí)例:

/**
 * 令牌桶限流算法(工業(yè)界標(biāo)準(zhǔn))
 * 優(yōu)點(diǎn):允許突發(fā)流量,同時(shí)能平滑限流,兼顧性能和靈活性
 * 缺點(diǎn):實(shí)現(xiàn)稍復(fù)雜
 */
public class TokenBucketRateLimiter {
    // 令牌桶容量(最大突發(fā)請(qǐng)求數(shù))
    private final int capacity;
    // 令牌生成速度(每秒生成的令牌數(shù),即限流QPS)
    private final double tokenRate;
    // 當(dāng)前令牌數(shù)
    private double currentTokens;
    // 上次生成令牌的時(shí)間戳
    private long lastTokenTime;
    public TokenBucketRateLimiter(int capacity, double tokenRate) {
        this.capacity = capacity;
        this.tokenRate = tokenRate;
        // 初始時(shí)桶是滿的
        this.currentTokens = capacity;
        this.lastTokenTime = System.currentTimeMillis();
    }
    /**
     * 嘗試獲取令牌
     * @return true-獲取成功(允許請(qǐng)求),false-獲取失敗(限流)
     */
    public synchronized boolean tryAcquire() {
        return tryAcquire(1);
    }
    /**
     * 嘗試獲取指定數(shù)量的令牌
     * @param permits 需要獲取的令牌數(shù)
     * @return true-獲取成功,false-獲取失敗
     */
    public synchronized boolean tryAcquire(int permits) {
        if (permits <= 0 || permits > capacity) {
            return false;
        }
        long currentTime = System.currentTimeMillis();
        // 1. 計(jì)算從上次生成令牌到現(xiàn)在應(yīng)該生成的令牌數(shù)
        double generatedTokens = (currentTime - lastTokenTime) / 1000.0 * tokenRate;
        // 2. 更新當(dāng)前令牌數(shù)(不能超過(guò)桶的容量)
        currentTokens = Math.min(capacity, currentTokens + generatedTokens);
        lastTokenTime = currentTime;
        // 3. 判斷是否有足夠的令牌
        if (currentTokens >= permits) {
            currentTokens -= permits;
            return true;
        }
        return false;
    }
    // 測(cè)試用例
    public static void main(String[] args) throws InterruptedException {
        // 令牌桶容量10,每秒生成5個(gè)令牌(即限流QPS=5,最大突發(fā)10個(gè)請(qǐng)求)
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5);
        // 模擬15個(gè)突發(fā)請(qǐng)求
        System.out.println("===== 突發(fā)15個(gè)請(qǐng)求 =====");
        for (int i = 0; i < 15; i++) {
            final int requestId = i;
            new Thread(() -> {
                if (limiter.tryAcquire()) {
                    System.out.println("請(qǐng)求" + requestId + ":成功");
                } else {
                    System.out.println("請(qǐng)求" + requestId + ":被限流");
                }
            }).start();
        }
        // 等待1秒,令牌桶會(huì)補(bǔ)充5個(gè)令牌
        Thread.sleep(1000);
        System.out.println("===== 1秒后 =====");
        // 再發(fā)10個(gè)請(qǐng)求
        for (int i = 15; i < 25; i++) {
            final int requestId = i;
            new Thread(() -> {
                if (limiter.tryAcquire()) {
                    System.out.println("請(qǐng)求" + requestId + ":成功");
                } else {
                    System.out.println("請(qǐng)求" + requestId + ":被限流");
                }
            }).start();
        }
    }
}

三:分布式限流代碼實(shí)現(xiàn)

核心實(shí)現(xiàn):

  • @RateLimit 注解:給方法打標(biāo)簽,配置限流規(guī)則
  • RateLimitAspect AOP 切面:攔截注解方法,執(zhí)行限流邏輯
  • rate_limit.lua 腳本:Redis 中原子執(zhí)行的令牌桶算法,保證并發(fā)安全

整個(gè)流程:

用戶請(qǐng)求 → 進(jìn)入被@RateLimit標(biāo)記的方法 → AOP切面攔截 → 調(diào)用Lua腳本執(zhí)行限流判斷
→ 通過(guò):執(zhí)行原方法
→ 不通過(guò):執(zhí)行降級(jí)方法 / 拋出異常

1、@RateLimit注解(限流規(guī)則定義)

這是給方法打標(biāo)簽的注解,用來(lái)配置限流的維度、次數(shù)、時(shí)間窗口等。

/**
 * 限流注解
 * 用于方法級(jí)別的限流控制,支持多維度組合限流
 *
 * @see RateLimitAspect
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    /**
     * 限流維度枚舉
     */
    enum Dimension {
        /**
         * 全局限流:對(duì)所有請(qǐng)求統(tǒng)一限流
         */
        GLOBAL,
        /**
         * IP限流:按客戶端IP地址限流
         */
        IP,
        /**
         * 用戶限流:按用戶ID限流
         */
        USER
    }

    /**
     * 限流維度配置
     * 支持多維度組合,只有所有維度都滿足條件時(shí)才允許請(qǐng)求通過(guò)
     * 例如:{Dimension.GLOBAL, Dimension.USER} 表示同時(shí)進(jìn)行全局限流和用戶級(jí)限流
     *
     * @return 限流維度數(shù)組
     */
    Dimension[] dimensions() default {Dimension.GLOBAL};

    /**
     * 在指定時(shí)間窗口內(nèi)允許的最大請(qǐng)求數(shù)
     * 例如:count = 10, interval = 1, timeUnit = MINUTES 表示每分鐘最多 10 次
     *
     * @return 令牌總數(shù)
     */
    double count();

    /**
     * 時(shí)間窗口大小
     * 默認(rèn) 1
     *
     * @return 時(shí)間窗口
     */
    long interval() default 1;

    /**
     * 時(shí)間單位
     * 默認(rèn)為秒,即默認(rèn)“每秒 count 次”
     *
     * @return 時(shí)間單位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 等待令牌的超時(shí)時(shí)間
     * 如果設(shè)置為0,表示不等待,直接獲取令牌,失敗則拒絕
     * 如果大于0,會(huì)嘗試等待指定時(shí)間獲取令牌
     *
     * @return 超時(shí)時(shí)間
     */
    long timeout() default 0;

    /**
     * 降級(jí)方法名
     * 當(dāng)限流觸發(fā)時(shí),調(diào)用指定方法進(jìn)行降級(jí)處理
     * 降級(jí)方法支持:
     * 1. 無(wú)參方法
     * 2. 與原方法參數(shù)列表完全一致的方法
     * 降級(jí)方法必須在同一個(gè)類中,返回值類型與原方法兼容
     * 如果為空字符串,則拋出 RateLimitExceededException 異常
     *
     * @return 降級(jí)方法名
     */
    String fallback() default "";

    /**
     * 時(shí)間單位枚舉
     */
    enum TimeUnit {
        MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS
    }
}
  • dimensions:多維度組合限流,比如 {GLOBAL, USER} 表示同時(shí)做全局限流和用戶級(jí)限流,只有兩個(gè)都通過(guò)才放行
  • count + interval + timeUnit:組成限流規(guī)則,比如 count=10, interval=1, timeUnit=MINUTES 表示 “每分鐘最多 10 次請(qǐng)求”
  • timeout:請(qǐng)求拿不到令牌時(shí)的等待時(shí)間,默認(rèn) 0 是直接拒絕
  • fallback:限流觸發(fā)時(shí)調(diào)用的降級(jí)方法名,支持無(wú)參或和原方法參數(shù)一致的方法

2、Lua 腳本(Redis 端原子限流邏輯)

這部分是限流的核心,用 Lua 腳本在 Redis 中原子執(zhí)行,解決并發(fā)競(jìng)爭(zhēng)問(wèn)題。

---@diagnostic disable: undefined-global
-- 原子化多維度限流腳本
-- 基于令牌桶算法實(shí)現(xiàn),支持多維度組合限流
-- 只有所有維度都滿足條件時(shí)才扣減令牌,確保原子性

-- 參數(shù)說(shuō)明:
-- KEYS[1..N]: 限流維度鍵列表
-- ARGV[1]: 當(dāng)前時(shí)間戳(毫秒)
-- ARGV[2]: 申請(qǐng)令牌數(shù)
-- ARGV[3]: 時(shí)間窗口(毫秒)
-- ARGV[4]: 最大令牌數(shù)(窗口內(nèi)允許的總數(shù))
-- ARGV[5]: 請(qǐng)求唯一標(biāo)識(shí)

local now_ms = tonumber(ARGV[1])
local permits = tonumber(ARGV[2])
local interval = tonumber(ARGV[3])
local max_tokens = tonumber(ARGV[4])
local request_id = ARGV[5]

-- 第一階段:預(yù)檢查階段 - 檢查所有維度是否有足夠令牌
for i, key in ipairs(KEYS) do
    local value_key = key .. ":value"
    local permits_key = key .. ":permits"

    -- 初始化 value_key(如果不存在)
    if redis.call("exists", value_key) == 0 then
        redis.call("set", value_key, max_tokens)
    end

    -- 回收過(guò)期令牌
    -- 清理過(guò)期的 permit 記錄,并回收配額到 value_key
    local expired_values = redis.call("zrangebyscore", permits_key, 0, now_ms - interval)
    if #expired_values > 0 then
        local expired_count = 0
        for _, v in ipairs(expired_values) do
            -- 優(yōu)化解析邏輯:使用更高效的模式匹配
            local p = tonumber(string.match(v, ":(%d+)$"))
            if p then
                expired_count = expired_count + p
            end
        end

        -- 刪除過(guò)期記錄
        redis.call("zremrangebyscore", permits_key, 0, now_ms - interval)

        -- 回收配額
        if expired_count > 0 then
            local curr_v = tonumber(redis.call("get", value_key) or max_tokens)
            local next_v = math.min(max_tokens, curr_v + expired_count)
            redis.call("set", value_key, next_v)
        end
    end

    -- 核心檢查:當(dāng)前可用令牌是否足夠
    local current_val = tonumber(redis.call("get", value_key) or max_tokens)
    if current_val < permits then
        -- 任何一個(gè)維度配額不足,直接返回失敗
        return 0
    end
end

-- 第二階段:扣減階段 - 只有所有維度都通過(guò)后才執(zhí)行
for i, key in ipairs(KEYS) do
    local value_key = key .. ":value"
    local permits_key = key .. ":permits"

    -- 記錄本次令牌分配(格式:request_id:permits)
    local permit_record = request_id .. ":" .. permits
    redis.call("zadd", permits_key, now_ms, permit_record)

    -- 扣減令牌
    local current_v = tonumber(redis.call("get", value_key) or max_tokens)
    redis.call("set", value_key, current_v - permits)

    -- 設(shè)置過(guò)期時(shí)間,確保過(guò)期令牌能被正?;厥?(窗口的2倍,至少1秒)
    local expire_time = math.ceil(interval * 2 / 1000)
    if expire_time < 1 then expire_time = 1 end
    redis.call("expire", value_key, expire_time)
    redis.call("expire", permits_key, expire_time)
end

-- 成功獲取所有維度的令牌
return 1

這部分的主要工作:

  • 初始化 Redis 中的令牌數(shù)(不存在則設(shè)置為最大數(shù))
  • 回收過(guò)期的令牌:清理有序集合里超過(guò)時(shí)間窗口的發(fā)放記錄,把配額回收回來(lái)
  • 檢查所有維度的剩余令牌數(shù),只要有一個(gè)維度不夠,直接返回失敗
  • 把本次發(fā)放的令牌記錄存入有序集合(用請(qǐng)求 UUID 做標(biāo)識(shí),score 為當(dāng)前時(shí)間戳)
  • 原子扣減剩余令牌數(shù)
  • 設(shè)置 key 的過(guò)期時(shí)間,防止 Redis 中存在永久 key

3、RateLimitAspectAOP 切面(Java 端限流流程)

這是連接注解和 Lua 腳本的橋梁,攔截方法、組裝參數(shù)、調(diào)用 Lua 腳本、處理結(jié)果。

/**
 * 限流 AOP 切面
 * 實(shí)現(xiàn)基于令牌桶算法的多維度限流
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
    private final RedissonClient redissonClient;
    //Lua 腳本緩存
    private static String LUA_SCRIPT;
    private String luaScriptSha;
    static {
        try {
            ClassPathResource resource = new ClassPathResource("rate_limit.lua");
            LUA_SCRIPT = new String(resource.getContentAsByteArray(), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException("加載限流 Lua 腳本失敗", e);
        }
    }
    //初始化:預(yù)加載腳本到 Redis 提高性能
    @PostConstruct
    public void init() {
        this.luaScriptSha = redissonClient.getScript(StringCodec.INSTANCE).scriptLoad(LUA_SCRIPT);
        log.info("限流 Lua 腳本加載完成, SHA1: {}", luaScriptSha);
    }
    //環(huán)繞通知:攔截帶 @RateLimit 注解的方法
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        String className = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();
        // 1. 計(jì)算時(shí)間窗口(毫秒)
        long intervalMs = calculateIntervalMs(rateLimit.interval(), rateLimit.timeUnit());
        // 2. 根據(jù)配置維度動(dòng)態(tài)生成 Redis Keys
        List<String> keys = generateKeys(className, methodName, rateLimit.dimensions());
        // 3. 調(diào)用 Lua 腳本執(zhí)行原子限流
        // 使用 StringCodec 確保參數(shù)正確傳遞為字符串
        RScript script = redissonClient.getScript(StringCodec.INSTANCE);
        // 準(zhǔn)備參數(shù)
        List<Object> keysList = new ArrayList<>(keys);
        Object[] args = {
                String.valueOf(System.currentTimeMillis()), // ARGV[1]: 當(dāng)前時(shí)間戳
                String.valueOf(1),                          // ARGV[2]: 申請(qǐng)令牌數(shù)(默認(rèn)1個(gè))
                String.valueOf(intervalMs),                 // ARGV[3]: 時(shí)間窗口
                String.valueOf(rateLimit.count()),          // ARGV[4]: 最大令牌數(shù)
                UUID.randomUUID().toString()               // ARGV[5]: 請(qǐng)求唯一標(biāo)識(shí)
        };
        Object resultObj = script.evalSha(
                RScript.Mode.READ_WRITE,
                luaScriptSha,
                RScript.ReturnType.VALUE,
                keysList,
                args
        );
        // 將結(jié)果轉(zhuǎn)換為 Long
        Long result = convertToLong(resultObj);
        // 4. 處理限流結(jié)果
        if (result == null || result == 0) {
            return handleRateLimitExceeded(joinPoint, rateLimit, keys);
        }
        // 5. 執(zhí)行原方法
        return joinPoint.proceed();
    }
    //計(jì)算時(shí)間窗口毫秒數(shù)
    private long calculateIntervalMs(long interval, RateLimit.TimeUnit unit) {
        return switch (unit) {
            case MILLISECONDS -> interval;
            case SECONDS -> interval * 1000;
            case MINUTES -> interval * 60 * 1000;
            case HOURS -> interval * 3600 * 1000;
            case DAYS -> interval * 86400 * 1000;
        };
    }
    //將結(jié)果對(duì)象安全轉(zhuǎn)換為 Long
    private Long convertToLong(Object obj) {
        if (obj == null) {
            return null;
        }
        if (obj instanceof Long) {
            return (Long) obj;
        } else if (obj instanceof Integer) {
            return ((Integer) obj).longValue();
        } else if (obj instanceof Short) {
            return ((Short) obj).longValue();
        } else if (obj instanceof Byte) {
            return ((Byte) obj).longValue();
        } else if (obj instanceof String) {
            try {
                return Long.parseLong((String) obj);
            } catch (NumberFormatException e) {
                log.warn("無(wú)法將字符串轉(zhuǎn)換為L(zhǎng)ong: {}", obj);
                return null;
            }
        }
        log.warn("不支持的對(duì)象類型轉(zhuǎn)換為L(zhǎng)ong: {}", obj.getClass().getName());
        return null;
    }
    //生成限流鍵列表
    private List<String> generateKeys(String className, String methodName, RateLimit.Dimension[] dimensions) {
        List<String> keys = new ArrayList<>();
        // 使用 {} 包含類名和方法名作為 Hash Tag,確保該方法的所有限流 Key 落在同一個(gè) Redis Slot
        // 從而適配 Redis Cluster 模式
        String hashTag = "{" + className + ":" + methodName + "}";
        String keyPrefix = "ratelimit:" + hashTag;
        for (RateLimit.Dimension dimension : dimensions) {
            switch (dimension) {
                case GLOBAL -> keys.add(keyPrefix + ":global");
                case IP -> keys.add(keyPrefix + ":ip:" + getClientIp());
                case USER -> keys.add(keyPrefix + ":user:" + getCurrentUserId());
            }
        }
        return keys;
    }
    //處理限流超出情況
    private Object handleRateLimitExceeded(ProceedingJoinPoint joinPoint, RateLimit rateLimit, List<String> keys)
            throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        // 如果配置了降級(jí)方法,則調(diào)用降級(jí)方法
        if (rateLimit.fallback() != null && !rateLimit.fallback().isEmpty()) {
            try {
                Method fallbackMethod = findFallbackMethod(joinPoint, rateLimit.fallback());
                if (fallbackMethod != null) {
                    log.debug("限流觸發(fā),執(zhí)行降級(jí)方法: {}.{} -> {}",
                            joinPoint.getTarget().getClass().getSimpleName(),
                            methodName,
                            rateLimit.fallback());
                    // 如果降級(jí)方法有參數(shù),傳入原方法的參數(shù)
                    if (fallbackMethod.getParameterCount() > 0) {
                        return fallbackMethod.invoke(joinPoint.getTarget(), joinPoint.getArgs());
                    } else {
                        return fallbackMethod.invoke(joinPoint.getTarget());
                    }
                }
            } catch (Exception e) {
                log.error("降級(jí)方法執(zhí)行失敗: {}", rateLimit.fallback(), e);
            }
        }
        // 沒(méi)有降級(jí)方法或降級(jí)失敗,拋出限流異常
        log.debug("限流觸發(fā),拒絕請(qǐng)求: keys={}, count={} per {} {}",
                keys, rateLimit.count(), rateLimit.interval(), rateLimit.timeUnit());
        throw new BusinessException("請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試");
    }
    /**
     * 查找降級(jí)方法
     * 優(yōu)先查找與原方法參數(shù)列表完全一致的方法,找不到則查找無(wú)參方法
     */
    private Method findFallbackMethod(ProceedingJoinPoint joinPoint, String fallbackName) {
        Class<?> targetClass = joinPoint.getTarget().getClass();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Class<?>[] parameterTypes = signature.getParameterTypes();
        try {
            // 1. 嘗試查找同參數(shù)列表的方法
            Method method = targetClass.getDeclaredMethod(fallbackName, parameterTypes);
            method.setAccessible(true);
            return method;
        } catch (NoSuchMethodException e) {
            // 2. 嘗試查找無(wú)參方法
            try {
                Method method = targetClass.getDeclaredMethod(fallbackName);
                method.setAccessible(true);
                return method;
            } catch (NoSuchMethodException ex) {
                log.warn("未找到降級(jí)方法: {}.{} (需無(wú)參或參數(shù)列表一致)",
                        targetClass.getSimpleName(), fallbackName);
                return null;
            }
        }
    }
    /**
     * 獲取客戶端真實(shí) IP
     * 處理 X-Forwarded-For 頭,支持代理服務(wù)器場(chǎng)景
     */
    private String getClientIp() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return "unknown";
        }
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 處理多個(gè) IP 的情況(X-Forwarded-For 可能包含多個(gè) IP)
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip != null ? ip : "unknown";
    }
    /**
     * 獲取當(dāng)前用戶 ID
     * 從請(qǐng)求屬性或 Session 中獲取
     */
    private String getCurrentUserId() {
        UserVO user = UserHolder.getUser();

        if (user != null) {
            return user.getId().toString();
        }
        return "anonymous";
    }
}

這部分的主要工作:

  • 靜態(tài)代碼塊加載 Lua 腳本內(nèi)容
  • @PostConstruct 預(yù)加載腳本到 Redis,獲取 SHA1 值,后續(xù)用 SHA1 調(diào)用,避免每次都傳整個(gè)腳本內(nèi)容
  • 解析注解配置,把時(shí)間窗口轉(zhuǎn)換為毫秒
  • 根據(jù)限流維度生成 Redis key(全局 / IP / 用戶級(jí))
  • 調(diào)用 Lua 腳本執(zhí)行原子限流判斷
  • 根據(jù)返回結(jié)果處理:限流則執(zhí)行降級(jí),通過(guò)則執(zhí)行原方法
  • 把注解中的時(shí)間單位統(tǒng)一轉(zhuǎn)換為毫秒,給 Lua 腳本使用。
  • {}包裹類名 + 方法名,作為 HashTag,確保同一個(gè)方法的所有限流 key 落在同一個(gè) Redis Slot,避免集群模式下跨 slot 操作報(bào)錯(cuò)
  • 按不同維度生成 key:全局 key、IP key、用戶 key
  • 限流觸發(fā)時(shí),優(yōu)先調(diào)用配置的降級(jí)方法,支持無(wú)參或和原方法參數(shù)一致的方法
  • 降級(jí)方法執(zhí)行失敗或未配置時(shí),拋出BusinessException異常,提示用戶請(qǐng)求頻繁

4、使用示例

// 每分鐘最多5次請(qǐng)求,同時(shí)做用戶級(jí)限流,觸發(fā)限流時(shí)調(diào)用fallback方法
@RateLimit(
    dimensions = {RateLimit.Dimension.USER},
    count = 5,
    interval = 1,
    timeUnit = RateLimit.TimeUnit.MINUTES,
    fallback = "seckillFallback"
)
public ResponseDTO<String> seckillFosterCoupon(Long couponId) {
    //業(yè)務(wù)邏輯
}

// 降級(jí)方法(和原方法參數(shù)一致)
public ResponseDTO<String> seckillFallback(Long couponId) {
    return ResponseDTO.fail("您的請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試");
}

四:代碼問(wèn)題解答

1、@PostConstruct注解的作用是什么

它是 Spring 的注解作用是:在 Bean 對(duì)象創(chuàng)建完成、所有依賴注入(@Resource/@Autowired)完成之后,自動(dòng)執(zhí)行一次這個(gè)方法!

在代碼中的作用:

  • RateLimitAspect 對(duì)象創(chuàng)建好
  • redissonClient 注入完成
  • 自動(dòng)執(zhí)行一次 init () 方法
    • 把 Lua 腳本加載到 Redis
    • 拿到腳本的 SHA1 值
    • 后面限流直接用 SHA1,性能更高

2、SHA1是什么

SHA1 是一種哈希算法,能把一段長(zhǎng)文本,算出一串固定 40 位十六進(jìn)制短字符串,這個(gè)結(jié)果就叫SHA1 摘要。

在代碼中的作用:

Lua 腳本內(nèi)容很長(zhǎng),每次調(diào)用都傳整段腳本很慢。Redis 支持:

  1. 先把腳本上傳 Redis,Redis 算出SHA1 哈希值存起來(lái)
  2. 后續(xù)調(diào)用只用傳短小的 SHA1 值,Redis 根據(jù)值找腳本執(zhí)行大幅節(jié)省網(wǎng)絡(luò)傳輸、提升速度。

3、HashTag是什么

 String hashTag = "{" + className + ":" + methodName + "}";

{} 中間的內(nèi)容叫 HashTag,Redis 只計(jì)算 {} 里面的字符串作為槽位!

{} 就是 Redis 的 HashTag 語(yǔ)法:

  • Redis 計(jì)算 slot 時(shí),只會(huì)用{}里的內(nèi)容來(lái)計(jì)算
  • 如果兩個(gè) key 的 HashTag 部分都是 {UserService:login},那么它們計(jì)算出來(lái)的 slot 號(hào)是完全一樣的,會(huì)落在同一個(gè) Redis 節(jié)點(diǎn)上

為什么這么做:

Lua 腳本有一個(gè)鐵律:Lua 腳本里用到的所有 key,必須在同一個(gè) slot(同一個(gè)抽屜)里!否則 直接報(bào)錯(cuò)!

以上就是基于Redis+Lua腳本的分布式全局令牌桶限流方案的詳細(xì)內(nèi)容,更多關(guān)于Redis Lua全局令牌桶限流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Redis中緩存和數(shù)據(jù)庫(kù)雙寫數(shù)據(jù)不一致的原因及解決方案

    Redis中緩存和數(shù)據(jù)庫(kù)雙寫數(shù)據(jù)不一致的原因及解決方案

    這篇文章主要介紹了Redis中緩存和數(shù)據(jù)庫(kù)雙寫數(shù)據(jù)不一致的原因及解決方案,文中通過(guò)圖文結(jié)合的方式講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-03-03
  • Redis連接錯(cuò)誤的情況總結(jié)分析

    Redis連接錯(cuò)誤的情況總結(jié)分析

    這篇文章主要給大家總結(jié)介紹了關(guān)于Redis連接錯(cuò)誤的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-02-02
  • 基于Redis實(shí)現(xiàn)消息隊(duì)列的示例代碼

    基于Redis實(shí)現(xiàn)消息隊(duì)列的示例代碼

    消息隊(duì)列在分布式系統(tǒng)中非常重要,能夠有效解耦系統(tǒng)的各個(gè)模塊,提供異步處理能力和緩沖能力,本文介紹了基于Redis實(shí)現(xiàn)消息隊(duì)列的示例代碼,感興趣的可以了解一下
    2025-04-04
  • Redis的主從同步與對(duì)象模型詳解

    Redis的主從同步與對(duì)象模型詳解

    本文介紹了Redis的內(nèi)存管理機(jī)制、淘汰策略、以及持久化方法,包括RDB、AOF和混合持久化等,同時(shí),還討論了Redis的主從復(fù)制、集群配置以及高可用性方案,感興趣的朋友跟隨小編一起看看吧
    2026-03-03
  • Redis的Cluster集群搭建的實(shí)現(xiàn)步驟

    Redis的Cluster集群搭建的實(shí)現(xiàn)步驟

    本文檔只對(duì)Redis的Cluster集群做簡(jiǎn)單的介紹,并沒(méi)有對(duì)分布式系統(tǒng)的所涉及到的概念做深入的探討。感興趣的小伙伴們可以參考一下
    2021-07-07
  • 利用redisson快速實(shí)現(xiàn)自定義限流注解(接口防刷)

    利用redisson快速實(shí)現(xiàn)自定義限流注解(接口防刷)

    利用redis的有序集合即Sorted?Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個(gè)令牌桶來(lái)實(shí)施限流,而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過(guò)redisson,即可快速實(shí)現(xiàn)我們的目標(biāo),這篇文章主要介紹了利用redisson快速實(shí)現(xiàn)自定義限流注解,需要的朋友可以參考下
    2024-07-07
  • redis GEO數(shù)據(jù)結(jié)構(gòu)、實(shí)現(xiàn)附近商鋪功能實(shí)踐

    redis GEO數(shù)據(jù)結(jié)構(gòu)、實(shí)現(xiàn)附近商鋪功能實(shí)踐

    文章介紹了Redis中的GEO命令及其用途,包括地理坐標(biāo)存儲(chǔ)、距離計(jì)算、坐標(biāo)轉(zhuǎn)換和位置搜索等功能,還分享了如何使用Redis實(shí)現(xiàn)查詢附近商鋪的功能,包括導(dǎo)入商鋪信息和根據(jù)類型及距離進(jìn)行搜索
    2025-12-12
  • Redis遠(yuǎn)程連接Redis客戶端的實(shí)現(xiàn)步驟

    Redis遠(yuǎn)程連接Redis客戶端的實(shí)現(xiàn)步驟

    本文主要介紹了Redis遠(yuǎn)程連接Redis客戶端的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2022-06-06
  • Java Socket實(shí)現(xiàn)Redis客戶端的詳細(xì)說(shuō)明

    Java Socket實(shí)現(xiàn)Redis客戶端的詳細(xì)說(shuō)明

    socket編程是一門技術(shù),它主要是在網(wǎng)絡(luò)通信中經(jīng)常用到.這篇文章主要介紹了如何用Java Socket實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Redis客戶端,需要的朋友可以參考下
    2021-05-05
  • Redis Redisson lock和tryLock的原理分析

    Redis Redisson lock和tryLock的原理分析

    這篇文章主要介紹了Redis Redisson lock和tryLock的原理分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-04-04

最新評(píng)論

宝鸡市| 德昌县| 宁都县| 河南省| 新蔡县| 五大连池市| 龙里县| 阳山县| 永定县| 云龙县| 临沧市| 固原市| 绵阳市| 岳阳县| 潢川县| 金溪县| 阳高县| 博白县| 苍南县| 泽州县| 工布江达县| 高唐县| 宁晋县| 天门市| 鄂尔多斯市| 津南区| 永胜县| 开化县| 连平县| 克拉玛依市| 申扎县| 宝丰县| 彰化市| 红安县| 庆安县| 自贡市| 大石桥市| 晴隆县| 赤城县| 庆云县| 鹤峰县|