<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                [TOC] 在平時線上 Redis 維護工作中,有時候需要從 Redis 實例成千上萬的 key 中找出特定前綴的 key 列表來手動處理數據,可能是修改它的值,也可能是刪除 key。這里就有一個問題,如何從海量的 key 中找出滿足特定前綴的 key 列表來? Redis 提供了一個簡單暴力的指令`keys`用來列出所有滿足特定正則字符串規則的 key。 ~~~ 127.0.0.1:6379> set codehole1 a OK 127.0.0.1:6379> set codehole2 b OK 127.0.0.1:6379> set codehole3 c OK 127.0.0.1:6379> set code1hole a OK 127.0.0.1:6379> set code2hole b OK 127.0.0.1:6379> set code3hole b OK 127.0.0.1:6379> keys * 1) "codehole1" 2) "code3hole" 3) "codehole3" 4) "code2hole" 5) "codehole2" 6) "code1hole" 127.0.0.1:6379> keys codehole* 1) "codehole1" 2) "codehole3" 3) "codehole2" 127.0.0.1:6379> keys code*hole 1) "code3hole" 2) "code2hole" 3) "code1hole" ~~~ 這個指令使用非常簡單,提供一個簡單的正則字符串即可,但是有很明顯的兩個**缺點**。 1. 沒有 offset、limit 參數,一次性吐出所有滿足條件的 key,萬一實例中有幾百 w 個 key 滿足條件,當你看到滿屏的字符串刷的沒有盡頭時,你就知道難受了。 2. keys 算法是遍歷算法,復雜度是 O(n),如果實例中有千萬級以上的 key,這個指令就會導致 Redis 服務卡頓,所有讀寫 Redis 的其它的指令都會被延后甚至會超時報錯,因為 Redis 是單線程程序,順序執行所有指令,其它指令必須等到當前的 keys 指令執行完了才可以繼續。 面對這兩個顯著的缺點該怎么辦呢? Redis 為了解決這個問題,它在 2.8 版本中加入了大海撈針的指令——`scan`。`scan`相比`keys`具備有以下特點: 1. 復雜度雖然也是 O(n),但是它是通過游標分步進行的,不會阻塞線程; 2. 提供 limit 參數,可以控制每次返回結果的最大條數,limit 只是一個 hint,返回的結果可多可少; 3. 同 keys 一樣,它也提供模式匹配功能; 4. 服務器不需要為游標保存狀態,游標的唯一狀態就是 scan 返回給客戶端的游標整數; 5. 返回的結果可能會有重復,需要客戶端去重復,這點非常重要; 6. 遍歷的過程中如果有數據修改,改動后的數據能不能遍歷到是不確定的; 7. 單次返回的結果是空的并不意味著遍歷結束,而要看返回的游標值是否為零; ## scan 基礎使用 在使用之前,讓我們往 Redis 里插入 10000 條數據來進行測試 ~~~ import redis client = redis.StrictRedis() for i in range(10000): client.set("key%d" % i, i) ~~~ 好,Redis 中現在有了 10000 條數據,接下來我們找出以 key99 開頭 key 列表。 scan 參數提供了三個參數,第一個是`cursor 整數值`,第二個是`key 的正則模式`,第三個是`遍歷的 limit hint`。第一次遍歷時,cursor 值為 0,然后將返回結果中第一個整數值作為下一次遍歷的 cursor。一直遍歷到返回的 cursor 值為 0 時結束。 ~~~ 127.0.0.1:6379> scan 0 match key99* count 1000 1) "13976" 2) 1) "key9911" 2) "key9974" 3) "key9994" 4) "key9910" 5) "key9907" 6) "key9989" 7) "key9971" 8) "key99" 9) "key9966" 10) "key992" 11) "key9903" 12) "key9905" 127.0.0.1:6379> scan 13976 match key99* count 1000 1) "1996" 2) 1) "key9982" 2) "key9997" 3) "key9963" 4) "key996" 5) "key9912" 6) "key9999" 7) "key9921" 8) "key994" 9) "key9956" 10) "key9919" 127.0.0.1:6379> scan 1996 match key99* count 1000 1) "12594" 2) 1) "key9939" 2) "key9941" 3) "key9967" 4) "key9938" 5) "key9906" 6) "key999" 7) "key9909" 8) "key9933" 9) "key9992" ...... 127.0.0.1:6379> scan 11687 match key99* count 1000 1) "0" 2) 1) "key9969" 2) "key998" 3) "key9986" 4) "key9968" 5) "key9965" 6) "key9990" 7) "key9915" 8) "key9928" 9) "key9908" 10) "key9929" 11) "key9944" ~~~ 從上面的過程可以看到雖然提供的 limit 是 1000,但是返回的結果只有 10 個左右。因為這個 limit 不是限定返回結果的數量,而是限定服務器單次遍歷的字典槽位數量(約等于)。如果將 limit 設置為 10,你會發現返回結果是空的,但是游標值不為零,意味著遍歷還沒結束。 ~~~ 127.0.0.1:6379> scan 0 match key99* count 10 1) "3072" 2) (empty list or set) ~~~ ## 字典的結構 在 Redis 中所有的 key 都存儲在一個很大的字典中,這個字典的結構和 Java 中的 HashMap 一樣,是一維數組 + 二維鏈表結構,第一維數組的大小總是 2^n(n>=0),擴容一次數組大小空間加倍,也就是 n++。 ![](https://user-gold-cdn.xitu.io/2018/7/5/164695b9f06c757e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) scan 指令返回的游標就是第一維數組的位置索引,我們將這個位置索引稱為槽 (slot)。如果不考慮字典的擴容縮容,直接按數組下標挨個遍歷就行了。limit 參數就表示需要遍歷的槽位數,之所以返回的結果可能多可能少,是因為不是所有的槽位上都會掛接鏈表,有些槽位可能是空的,還有些槽位上掛接的鏈表上的元素可能會有多個。每一次遍歷都會將 limit 數量的槽位上掛接的所有鏈表元素進行模式匹配過濾后,一次性返回給客戶端。 ## scan 遍歷順序 scan 的遍歷順序非常特別。它不是從第一維數組的第 0 位一直遍歷到末尾,而是采用了高位進位加法來遍歷。之所以使用這樣特殊的方式進行遍歷,是考慮到字典的擴容和縮容時避免槽位的遍歷重復和遺漏。 首先我們用動畫演示一下普通加法和高位進位加法的區別。 ![](https://user-gold-cdn.xitu.io/2018/7/5/16469760d12e0cbd?imageslim) 從動畫中可以看出高位進位法從左邊加,進位往右邊移動,同普通加法正好相反。但是最終它們都會遍歷所有的槽位并且沒有重復。 ## 字典擴容 Java 中的 HashMap 有擴容的概念,當 loadFactor 達到閾值時,需要重新分配一個新的 2 倍大小的數組,然后將所有的元素全部 rehash 掛到新的數組下面。rehash 就是將元素的 hash 值對數組長度進行取模運算,因為長度變了,所以每個元素掛接的槽位可能也發生了變化。又因為數組的長度是 2^n 次方,所以取模運算等價于位與操作。 ~~~ a mod 8 = a & (8-1) = a & 7 a mod 16 = a & (16-1) = a & 15 a mod 32 = a & (32-1) = a & 31 ~~~ 這里的 7, 15, 31 稱之為字典的 mask 值,mask 的作用就是保留 hash 值的低位,高位都被設置為 0。 接下來我們看看 rehash 前后元素槽位的變化。 假設當前的字典的數組長度由 8 位擴容到 16 位,那么 3 號槽位 011 將會被 rehash 到 3 號槽位和 11 號槽位,也就是說該槽位鏈表中大約有一半的元素還是 3 號槽位,其它的元素會放到 11 號槽位,11 這個數字的二進制是 1011,就是對 3 的二進制 011 增加了一個高位 1。 ![](https://user-gold-cdn.xitu.io/2018/7/5/164698cd0d3eec33?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 抽象一點說,假設開始槽位的二進制數是 xxx,那么該槽位中的元素將被 rehash 到 0xxx 和 1xxx(xxx+8) 中。 如果字典長度由 16 位擴容到 32 位,那么對于二進制槽位 xxxx 中的元素將被 rehash 到 0xxxx 和 1xxxx(xxxx+16) 中。 ## 對比擴容縮容前后的遍歷順序 ![](https://user-gold-cdn.xitu.io/2018/7/5/164699dae277cc19?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 觀察這張圖,我們發現采用高位進位加法的遍歷順序,rehash 后的槽位在遍歷順序上是相鄰的。 假設當前要即將遍歷 110 這個位置 (橙色),那么擴容后,當前槽位上所有的元素對應的新槽位是 0110 和 1110(深綠色),也就是在槽位的二進制數增加一個高位 0 或 1。這時我們可以直接從 0110 這個槽位開始往后繼續遍歷,0110 槽位之前的所有槽位都是已經遍歷過的,這樣就可以避免擴容后對已經遍歷過的槽位進行重復遍歷。 再考慮縮容,假設當前即將遍歷 110 這個位置 (橙色),那么縮容后,當前槽位所有的元素對應的新槽位是 10(深綠色),也就是去掉槽位二進制最高位。這時我們可以直接從 10 這個槽位繼續往后遍歷,10 槽位之前的所有槽位都是已經遍歷過的,這樣就可以避免縮容的重復遍歷。不過縮容還是不太一樣,它會對圖中 010 這個槽位上的元素進行重復遍歷,因為縮融后 10 槽位的元素是 010 和 110 上掛接的元素的融合。 ## 漸進式 rehash Java 的 HashMap 在擴容時會一次性將舊數組下掛接的元素全部轉移到新數組下面。如果 HashMap 中元素特別多,線程就會出現卡頓現象。Redis 為了解決這個問題,它采用**漸進式 rehash**。 它會同時保留舊數組和新數組,然后在定時任務中以及后續對 hash 的指令操作中漸漸地將舊數組中掛接的元素遷移到新數組上。這意味著要操作處于 rehash 中的字典,需要同時訪問新舊兩個數組結構。如果在舊數組下面找不到元素,還需要去新數組下面去尋找。 scan 也需要考慮這個問題,對與 rehash 中的字典,它需要同時掃描新舊槽位,然后將結果融合后返回給客戶端。 ## 更多的 scan 指令 scan 指令是一系列指令,除了可以遍歷所有的 key 之外,還可以對指定的容器集合進行遍歷。比如 zscan 遍歷 zset 集合元素,hscan 遍歷 hash 字典的元素、sscan 遍歷 set 集合的元素。 它們的原理同 scan 都會類似的,因為 hash 底層就是字典,set 也是一個特殊的 hash(所有的 value 指向同一個元素),zset 內部也使用了字典來存儲所有的元素內容,所以這里不再贅述。 ## 大 key 掃描 有時候會因為業務人員使用不當,在 Redis 實例中會形成很大的對象,比如一個很大的 hash,一個很大的 zset 這都是經常出現的。這樣的對象對 Redis 的集群數據遷移帶來了很大的問題,因為在集群環境下,如果某個 key 太大,會數據導致遷移卡頓。另外在內存分配上,如果一個 key 太大,那么當它需要擴容時,會一次性申請更大的一塊內存,這也會導致卡頓。如果這個大 key 被刪除,內存會一次性回收,卡頓現象會再一次產生。 **在平時的業務開發中,要盡量避免大 key 的產生**。 如果你觀察到 Redis 的內存大起大落,這極有可能是因為大 key 導致的,這時候你就需要定位出具體是那個 key,進一步定位出具體的業務來源,然后再改進相關業務代碼設計。 **那如何定位大 key 呢?** 為了避免對線上 Redis 帶來卡頓,這就要用到 scan 指令,對于掃描出來的每一個 key,使用 type 指令獲得 key 的類型,然后使用相應數據結構的 size 或者 len 方法來得到它的大小,對于每一種類型,保留大小的前 N 名作為掃描結果展示出來。 上面這樣的過程需要編寫腳本,比較繁瑣,不過 Redis 官方已經在 redis-cli 指令中提供了這樣的掃描功能,我們可以直接拿來即用。 ~~~ redis-cli -h 127.0.0.1 -p 7001 –-bigkeys ~~~ 如果你擔心這個指令會大幅抬升 Redis 的 ops 導致線上報警,還可以增加一個休眠參數。 ~~~ redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 ~~~ 上面這個指令每隔 100 條 scan 指令就會休眠 0.1s,ops 就不會劇烈抬升,但是掃描的時間會變長。 ## 擴展閱讀 感興趣可以繼續深入閱讀[美團近期修復的Scan的一個bug](https://link.juejin.im/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%2FufoLJiXE0wU4Bc7ZbE9cDQ)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看