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

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

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

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

一:具體解決的五大問題

1、解決多實例部署下,總流量無法精準控制的問題

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

2、解決并發(fā)請求下的超賣 / 超限制問題

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

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

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

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

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

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

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

二:令牌桶算法介紹

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

核心優(yōu)勢:

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

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

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

代碼實例:

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

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

核心實現(xiàn):

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

整個流程:

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

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

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

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

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

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

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

    /**
     * 時間窗口大小
     * 默認 1
     *
     * @return 時間窗口
     */
    long interval() default 1;

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

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

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

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

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

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

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

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

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

    -- 回收過期令牌
    -- 清理過期的 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

        -- 刪除過期記錄
        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

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

-- 第二階段:扣減階段 - 只有所有維度都通過后才執(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è)置過期時間,確保過期令牌能被正?;厥?(窗口的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ù))
  • 回收過期的令牌:清理有序集合里超過時間窗口的發(fā)放記錄,把配額回收回來
  • 檢查所有維度的剩余令牌數(shù),只要有一個維度不夠,直接返回失敗
  • 把本次發(fā)放的令牌記錄存入有序集合(用請求 UUID 做標識,score 為當前時間戳)
  • 原子扣減剩余令牌數(shù)
  • 設(shè)置 key 的過期時間,防止 Redis 中存在永久 key

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

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

/**
 * 限流 AOP 切面
 * 實現(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. 計算時間窗口(毫秒)
        long intervalMs = calculateIntervalMs(rateLimit.interval(), rateLimit.timeUnit());
        // 2. 根據(jù)配置維度動態(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);
        // 準備參數(shù)
        List<Object> keysList = new ArrayList<>(keys);
        Object[] args = {
                String.valueOf(System.currentTimeMillis()), // ARGV[1]: 當前時間戳
                String.valueOf(1),                          // ARGV[2]: 申請令牌數(shù)(默認1個)
                String.valueOf(intervalMs),                 // ARGV[3]: 時間窗口
                String.valueOf(rateLimit.count()),          // ARGV[4]: 最大令牌數(shù)
                UUID.randomUUID().toString()               // ARGV[5]: 請求唯一標識
        };
        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();
    }
    //計算時間窗口毫秒數(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é)果對象安全轉(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("無法將字符串轉(zhuǎn)換為Long: {}", obj);
                return null;
            }
        }
        log.warn("不支持的對象類型轉(zhuǎn)換為Long: {}", obj.getClass().getName());
        return null;
    }
    //生成限流鍵列表
    private List<String> generateKeys(String className, String methodName, RateLimit.Dimension[] dimensions) {
        List<String> keys = new ArrayList<>();
        // 使用 {} 包含類名和方法名作為 Hash Tag,確保該方法的所有限流 Key 落在同一個 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();
        // 如果配置了降級方法,則調(diào)用降級方法
        if (rateLimit.fallback() != null && !rateLimit.fallback().isEmpty()) {
            try {
                Method fallbackMethod = findFallbackMethod(joinPoint, rateLimit.fallback());
                if (fallbackMethod != null) {
                    log.debug("限流觸發(fā),執(zhí)行降級方法: {}.{} -> {}",
                            joinPoint.getTarget().getClass().getSimpleName(),
                            methodName,
                            rateLimit.fallback());
                    // 如果降級方法有參數(shù),傳入原方法的參數(shù)
                    if (fallbackMethod.getParameterCount() > 0) {
                        return fallbackMethod.invoke(joinPoint.getTarget(), joinPoint.getArgs());
                    } else {
                        return fallbackMethod.invoke(joinPoint.getTarget());
                    }
                }
            } catch (Exception e) {
                log.error("降級方法執(zhí)行失敗: {}", rateLimit.fallback(), e);
            }
        }
        // 沒有降級方法或降級失敗,拋出限流異常
        log.debug("限流觸發(fā),拒絕請求: keys={}, count={} per {} {}",
                keys, rateLimit.count(), rateLimit.interval(), rateLimit.timeUnit());
        throw new BusinessException("請求過于頻繁,請稍后再試");
    }
    /**
     * 查找降級方法
     * 優(yōu)先查找與原方法參數(shù)列表完全一致的方法,找不到則查找無參方法
     */
    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. 嘗試查找無參方法
            try {
                Method method = targetClass.getDeclaredMethod(fallbackName);
                method.setAccessible(true);
                return method;
            } catch (NoSuchMethodException ex) {
                log.warn("未找到降級方法: {}.{} (需無參或參數(shù)列表一致)",
                        targetClass.getSimpleName(), fallbackName);
                return null;
            }
        }
    }
    /**
     * 獲取客戶端真實 IP
     * 處理 X-Forwarded-For 頭,支持代理服務(wù)器場景
     */
    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();
        }
        // 處理多個 IP 的情況(X-Forwarded-For 可能包含多個 IP)
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip != null ? ip : "unknown";
    }
    /**
     * 獲取當前用戶 ID
     * 從請求屬性或 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)用,避免每次都傳整個腳本內(nèi)容
  • 解析注解配置,把時間窗口轉(zhuǎn)換為毫秒
  • 根據(jù)限流維度生成 Redis key(全局 / IP / 用戶級)
  • 調(diào)用 Lua 腳本執(zhí)行原子限流判斷
  • 根據(jù)返回結(jié)果處理:限流則執(zhí)行降級,通過則執(zhí)行原方法
  • 把注解中的時間單位統(tǒng)一轉(zhuǎn)換為毫秒,給 Lua 腳本使用。
  • {}包裹類名 + 方法名,作為 HashTag,確保同一個方法的所有限流 key 落在同一個 Redis Slot,避免集群模式下跨 slot 操作報錯
  • 按不同維度生成 key:全局 key、IP key、用戶 key
  • 限流觸發(fā)時,優(yōu)先調(diào)用配置的降級方法,支持無參或和原方法參數(shù)一致的方法
  • 降級方法執(zhí)行失敗或未配置時,拋出BusinessException異常,提示用戶請求頻繁

4、使用示例

// 每分鐘最多5次請求,同時做用戶級限流,觸發(fā)限流時調(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ù)邏輯
}

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

四:代碼問題解答

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

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

在代碼中的作用:

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

2、SHA1是什么

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

在代碼中的作用:

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

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

3、HashTag是什么

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

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

{} 就是 Redis 的 HashTag 語法:

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

為什么這么做:

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

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

相關(guān)文章

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

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

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

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

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

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

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

    Redis的主從同步與對象模型詳解

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

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

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

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

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

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

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

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

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

    Java Socket實現(xiàn)Redis客戶端的詳細說明

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

    Redis Redisson lock和tryLock的原理分析

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

最新評論

伊春市| 东乡县| 普安县| 天柱县| 饶河县| 桐柏县| 垣曲县| 遵化市| 广西| 西乌珠穆沁旗| 蕲春县| 固阳县| 黔西| 连平县| 红桥区| 乐昌市| 阿坝| 社会| 泸水县| 宣威市| 崇阳县| 宁武县| 六枝特区| 海淀区| 铜梁县| 乌拉特后旗| 石楼县| 隆安县| 岚皋县| 宁国市| 习水县| 盐津县| 敦化市| 涪陵区| 兴文县| 英吉沙县| 德安县| 察雅县| 西昌市| 洛隆县| 九寨沟县|