欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流的示例

 更新時(shí)間:2025年04月21日 09:49:23   作者:頗有幾分姿色  
本文主要介紹了SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流的示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

使用redis的自增對接口進(jìn)行限流

1.引入依賴

<!-- springboot已集成,不需要再引入版本 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.代碼示例

2.1 基本代碼

我這里使用使用了手機(jī)號和一些其他的字符串組成了redis的key,你可以自定義自己的key.

private void validRateBasic (String phone) {
        String key = "LIMIT:RATE:" + phone;
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        try {
            String num = (String) redisTemplate.opsForValue().get(key);
            if (ObjectUtil.isNull(num)) {
                redisTemplate.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
            } else if (Integer.parseInt(num) >= 20) {
                Long expire = redisTemplate.getExpire(key);
                throw new CheckedException("操作頻繁,請" + expire + "s后再試");
            } else {
                redisTemplate.opsForValue().increment(key);
            }
        } catch (Exception e) {
            if (e instanceof CheckedException) {
                throw new CheckedException(e.getMessage());
            } else {
                log.info("校驗(yàn)上傳速率失敗,error:{}",e);
                throw new CheckedException("操作失敗,請稍后再試");
            }
        }

    }

這段代碼實(shí)現(xiàn)了同一個(gè)接口中,同一個(gè)手機(jī)號在60s內(nèi)只能訪問20次,雖然redis是單線程的,但在高并發(fā)情況下,這段代碼仍有并發(fā)問題。 在獲取訪問次數(shù)和增加訪問次數(shù)之間,訪問次數(shù)可能已經(jīng)被其他線程修改 。如果你對多出來的一兩次請求要求不高,那這個(gè)限制基本符合需求。
在redis中,我們可以使用lua腳本和redis事務(wù)來保證操作的原子性。

2.2 使用redis事務(wù)

2.2.1 SessionCallback(不推薦)

有人使用redisTemplate.setEnableTransactionSupport(true),使用redisTemplate支持事務(wù),但這樣可能存在已下幾種問題:

  • 如果你在分布式環(huán)境中使用Redis,事務(wù)支持可能會有問題,因?yàn)镽edis的事務(wù)模型是樂觀鎖,如果在事務(wù)中的操作被其他實(shí)例修改,那么事務(wù)就會失敗。在高并發(fā)場景中,這可能會導(dǎo)致大量的事務(wù)失敗。
  • 使RedisTemplate支持事務(wù)會導(dǎo)致所有的Redis操作都在事務(wù)中執(zhí)行,這可能會降低性能,特別是在需要執(zhí)行大量Redis操作的情況下。
  • 這個(gè)設(shè)置將影響所有使用這個(gè)RedisTemplate實(shí)例的代碼,所以需要確保所有相關(guān)的代碼都能正確地處理在事務(wù)中的Redis操作。

這里使用的是Spring Data Redis提供的會話回調(diào)(SessionCallback)接口。它可以讓我們在一個(gè)Redis連接中執(zhí)行多個(gè)操作,并保持原子性。

private void validRate(String phone) {
        String key = "LIMIT:RATE:" + phone;SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流
        int retryTimes = 0;
        // 失敗重試五次
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        while(retryTimes < 6) {
            retryTimes++;
            try {
                // 在事務(wù)之外獲取這個(gè)鍵的值
                String num = (String) redisTemplate.opsForValue().get(key);

                // 使用SessionCallback進(jìn)行原子性操作
                SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
                    @Override
                    public Object execute(RedisOperations operations) throws DataAccessException {
                        operations.watch(key);
                        operations.multi();
                        // 在事務(wù)內(nèi)部再次檢查這個(gè)鍵的值
                        String currentNum = (String) operations.opsForValue().get(key);
                        if (num == null ? currentNum != null : !num.equals(currentNum)) {
                            // 這個(gè)鍵的值被修改了,所以取消這個(gè)事務(wù)
                            operations.discard();
                            return null;
                        }
                        if (ObjectUtil.isNull(num)) {
                            operations.opsForValue().set(key, "1", 60, TimeUnit.SECONDS);
                        } else if (Integer.parseInt(num) >= 5) {
                            Long expire = operations.getExpire(key);
                            throw new CheckedException("操作頻繁,請" + expire + "s后再試");
                        } else {
                            operations.opsForValue().increment(key);
                        }
                        // 提交事務(wù)并返回結(jié)果
                        return operations.exec();
                    }
                };

                // 執(zhí)行SessionCallback
                List<Object> results = (List<Object>) redisTemplate.execute(sessionCallback);
                if (CollectionUtils.isEmpty(results)) {
                    // 如果事務(wù)執(zhí)行失敗,重新嘗試事務(wù)
                    log.info("重試");
                    continue;
                }
                return;
            } catch (Exception e) {
                // 在重試的情況下捕獲任何異常
                if (retryTimes >= 5) {
                    throw new CheckedException("操作頻繁,請稍后再試");
                }
            }
        }
    }

這一段代碼看起來沒啥毛病,一運(yùn)行你會發(fā)現(xiàn) String num = (String) operations.opsForValue().get(key);一直是null。這是因?yàn)樵趓edis事務(wù)中,事務(wù)中的所有命令都會被放在隊(duì)列中,等到exec命令被調(diào)用時(shí)才會一次性執(zhí)行。redis的事務(wù)在某些方面是不如關(guān)系型數(shù)據(jù)庫的:

  • 無隔離性:redis的事務(wù)沒有隔離性,在事務(wù)開始(multi命令執(zhí)行)之后,其他的客戶端仍然可以對事務(wù)中的鍵進(jìn)行讀寫操作,這可能會影響到事務(wù)的結(jié)果。
  • 無原子讀:無法讀取到自己事務(wù)未提交的數(shù)據(jù),也無法讀取到其他事務(wù)寫入的數(shù)據(jù)。如上面代碼,事務(wù)開始后的get命令返回的是null,而不是最新數(shù)據(jù)。
  • 無回滾:一旦一個(gè)事務(wù)被提交(exec命令執(zhí)行),事務(wù)中的所有操作都會被執(zhí)行,即使其中某些操作失敗了,其他的操作也不會被回滾。
  • 無鎖:redis事務(wù)并不提供鎖,或者說redis并沒有鎖的概念,和無隔離性造的結(jié)果是一樣的。

2.2.2 分布式鎖(推薦)

分布式鎖已經(jīng)有很多成熟的框架了和很多優(yōu)秀的博客了,這里就不贅述了,有空會補(bǔ)充一篇。

2.3 使用Lua腳本(推薦)

Lua腳本在執(zhí)行時(shí)是原子性的:當(dāng)腳本正在運(yùn)行的時(shí)候,不會有其他的腳本或Redis命令被執(zhí)行。

private void validRateLua(String phone) {
        String key = "LIMIT:RATE:" + phone;
        int retryTimes = 0;
        // 創(chuàng)建Lua腳本,返回新的計(jì)數(shù)值
        String luaScript =
                "local num = redis.call('GET', KEYS[1]);" +
                        "if num == false then " +
                        "   redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]);" +
                        "   return ARGV[1];" +
                        "elseif tonumber(num) <= tonumber(ARGV[3]) then " +
                        "   local newNum = redis.call('INCR', KEYS[1]);" +
                        "   return newNum;" +
                        "else " +
                        "   return num;" +
                        "end;";
        RedisScript<String> redisScript = new DefaultRedisScript<>(luaScript, String.class);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        while(retryTimes < 5) {
            retryTimes++;
            try {
                // 執(zhí)行Lua腳本
                String num = (String) redisTemplate.execute(redisScript, Collections.singletonList(key), "1", "60","5");
                if (num != null && Integer.parseInt(num) > 5) {
                    Long expire = redisTemplate.getExpire(key);
                    throw new CheckedException("操作頻繁,請" + expire + "s后再試");
                }

                return;
            } catch (Exception e) {
                if (e instanceof CheckedException) {
                    throw new CheckedException(e.getMessage());
                } else {
                    // 在重試的情況下捕獲任何異常
                    // 有需要的可以加入指數(shù)退避、最大重試時(shí)間等
                    if (retryTimes >= 5) {
                        log.error("上傳失敗,error:{}",e);
                        throw new CheckedException("操作頻繁,請稍后再試");
                    }
                }
            }
        }
    }

執(zhí)行Lua腳本有幾點(diǎn)需要注意:

  • lua腳本會阻塞Redis的所有操作,需要盡量保證Lua腳本的執(zhí)行時(shí)間短,以免影響redis的性能.
  • lua腳本一旦被執(zhí)行,它就會被加載到內(nèi)存中,即使沒被執(zhí)行也會持續(xù)保存在內(nèi)存中,這樣設(shè)計(jì)的目的是方便快速執(zhí)行,避免每次執(zhí)行腳本都要重新加載
  • lua腳本一般都很小,但是如果你有大量的lua腳本長時(shí)間保存在內(nèi)存中,被頻繁的加載和執(zhí)行,就會占用大量的內(nèi)存。這個(gè)問題可以通過script命令和LUA-EVAL-NOLOAD配置選項(xiàng)來解決。

到此這篇關(guān)于SpringBoot整合redis實(shí)現(xiàn)計(jì)數(shù)器限流的示例的文章就介紹到這了,更多相關(guān)SpringBoot redis計(jì)數(shù)器限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家! 

相關(guān)文章

  • 聊聊spring繼承的問題

    聊聊spring繼承的問題

    這篇文章主要介紹了spring繼承的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法示例

    Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法示例

    這篇文章主要介紹了Java基于循環(huán)遞歸回溯實(shí)現(xiàn)八皇后問題算法,結(jié)合具體實(shí)例形式分析了java的遍歷、遞歸、回溯等算法實(shí)現(xiàn)八皇后問題的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下
    2017-06-06
  • Redis使用RedisTemplate模板類的常用操作方式

    Redis使用RedisTemplate模板類的常用操作方式

    這篇文章主要介紹了Redis使用RedisTemplate模板類的常用操作方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Eclipse 開發(fā)java 出現(xiàn)Failed to create the Java Virtual Machine錯(cuò)誤解決辦法

    Eclipse 開發(fā)java 出現(xiàn)Failed to create the Java Virtual Machine錯(cuò)誤

    這篇文章主要介紹了Eclipse 開發(fā)java 出現(xiàn)Failed to create the Java Virtual Machine錯(cuò)誤解決辦法的相關(guān)資料,需要的朋友可以參考下
    2017-04-04
  • Java字符編碼簡介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    Java字符編碼簡介_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理

    這篇文章主要介紹了Java字符編碼簡介,本文主要包括以下幾個(gè)方面:編碼基本知識,Java,系統(tǒng)軟件,url,工具軟件等,感興趣的朋友一起看看吧
    2017-08-08
  • 如何在MyBatis中使用XML和注解混合配置過程

    如何在MyBatis中使用XML和注解混合配置過程

    這篇文章主要介紹了如何在MyBatis中使用XML和注解混合配置過程,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2025-04-04
  • Java線程阻塞方法sleep()與wait()的全面講解

    Java線程阻塞方法sleep()與wait()的全面講解

    這篇文章主要介紹了Java線程阻塞方法sleep()與wait()的全面講解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • Java中數(shù)組的使用與注意事項(xiàng)詳解(推薦)

    Java中數(shù)組的使用與注意事項(xiàng)詳解(推薦)

    數(shù)組是一組地址連續(xù)、長度固定的具有相同類型的數(shù)據(jù)的集合,通過數(shù)組下標(biāo)我們可以指定數(shù)字中的每一個(gè)元素,下面這篇文章主要給大家介紹了關(guān)于Java中數(shù)組的使用與注意事項(xiàng)的相關(guān)資料,需要的朋友可以參考下
    2021-08-08
  • JavaWeb之監(jiān)聽器案例講解

    JavaWeb之監(jiān)聽器案例講解

    這篇文章主要介紹了JavaWeb之監(jiān)聽器案例講解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • SpringBoot中的分布式追蹤及使用詳解

    SpringBoot中的分布式追蹤及使用詳解

    隨著互聯(lián)網(wǎng)應(yīng)用程序的復(fù)雜性不斷增加,分布式系統(tǒng)已經(jīng)成為了許多企業(yè)級應(yīng)用程序的標(biāo)配,由于服務(wù)之間的調(diào)用關(guān)系錯(cuò)綜復(fù)雜,很難追蹤到一個(gè)請求在整個(gè)系統(tǒng)中的執(zhí)行路徑和時(shí)間,為了解決這個(gè)問題,本文將介紹SpringBoot中的分布式追蹤技術(shù)及其使用方法
    2023-07-07

最新評論