Redis进阶-如何从海量的 key 中找出特定的key列表 Scan详解
文章目錄
- 需求
- scan
- scan基本使用
- 批量寫入一批模擬數據
- 字典的結構
- scan 遍歷順序 (高位進位法)
- 漸進式 rehash
- 更多的 scan 指令
- 大 key 掃描 --bigkeys
- 使用scan的注意事項 20201101更新
需求
假設你需要從 Redis 實例成千上萬的 key 中找出特定前綴的 key 列表來手動處理數據,可能是修改它的值,也可能是刪除 key。那該如何從海量的 key 中找出滿足特定前綴的 key 列表來?
我們可以用 keys 來列出所有滿足特定正則字符串規則的 key .
192.168.18.131:8001> set artisan 1 OK 192.168.18.131:8001> set artisan2 2 -> Redirected to slot [6066] located at 192.168.18.132:8002 OK 192.168.18.132:8002> set artisan3 3 -> Redirected to slot [1939] located at 192.168.18.131:8001 OK 192.168.18.131:8001> set artisan4 4 -> Redirected to slot [14196] located at 192.168.18.132:8005 OK 192.168.18.132:8005> set artisan5 5 -> Redirected to slot [10069] located at 192.168.18.132:8002 OK 192.168.18.132:8002> 192.168.18.132:8002> keys * 1) "artisanKey" 2) "clusterArtisan" 3) "artisan2" 4) "ar" 5) "test" 6) "artisan5" 192.168.18.132:8002> keys artisan* 1) "artisanKey" 2) "artisan2" 3) "artisan5" 192.168.18.132:8002>我這個是集群環境,有幾個key被分到了其他的slot上去了,所以看到的數據僅僅是當前slot的數據。
keys 優點呢 ,使用簡單
當然了,也有缺點
- 一次性列出所有滿足條件的 key. keys 算法是遍歷算法,復雜度是 O(n) ,如果數據量很大,會導致 Redis 服務卡頓,所有讀寫 Redis 的其它的指令都會被延后甚至會超時報錯,因為Redis 是單線程程序,順序執行所有指令,其它指令必須等到當前的 keys 指令執行完了才可以繼續。
咋辦呢? ---------------> Redis在 2.8 版本中加入了scan指令.
scan
scan 相比keys 具備有以下特點:
- 復雜度雖然也是 O(n),但是它是通過游標分步進行的,不會阻塞線程;
- 提供 limit 參數,可以控制每次返回結果的最大條數,limit 只是一個 hint,返回的結果可多可少;
- 同 keys 一樣,它也提供模式匹配功能;
- 服務器不需要為游標保存狀態,游標的唯一狀態就是 scan 返回給客戶端的游標整數;
- 返回的結果可能會有重復,需要客戶端去重復,這點非常重要;
- 遍歷的過程中如果有數據修改,改動后的數據能不能遍歷到是不確定的;
- 單次返回的結果是空的并不意味著遍歷結束,而要看返回的游標值是否為零
scan基本使用
批量寫入一批模擬數據
import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPoolConfig;import java.io.IOException; import java.util.HashSet; import java.util.Set;public class JedisClusterDemo {public static void main(String[] args) throws IOException {JedisPoolConfig config = new JedisPoolConfig();config.setMaxTotal(20);config.setMaxIdle(10);config.setMinIdle(5);Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();jedisClusterNode.add(new HostAndPort("192.168.18.131", 8001));jedisClusterNode.add(new HostAndPort("192.168.18.131", 8004));jedisClusterNode.add(new HostAndPort("192.168.18.132", 8002));jedisClusterNode.add(new HostAndPort("192.168.18.132", 8005));jedisClusterNode.add(new HostAndPort("192.168.18.133", 8003));jedisClusterNode.add(new HostAndPort("192.168.18.133", 8006));JedisCluster jedisCluster = null;try {//connectionTimeout:指的是連接一個url的連接等待時間//soTimeout:指的是連接上一個url,獲取response的返回等待時間jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, "artisan", config);for (int i = 0; i < 10000; i++) {jedisCluster.set("{art}:clusterArtisan:" + i, "artisanValue:" + i);}System.out.println("DONE");} catch (Exception e) {e.printStackTrace();} finally {if (jedisCluster != null)jedisCluster.close();}} }scan 參數提供了三個參數:
-
第一個是 cursor 整數值
-
第二個是 key 的正則模式
-
第三個是遍歷的 limit hint。
第一次遍歷時,cursor 值為 0,然后將返回結果中第一個整數值作為下一次遍歷的 cursor。一直遍歷到返回的 cursor 值為 0 時結束。
192.168.18.132:8005> scan 0 match {art}:clusterArtisan:9* count 1000 1) "13848" 2) 1) "{art}:clusterArtisan:9927"2) "{art}:clusterArtisan:9660"3) "{art}:clusterArtisan:994"4) "{art}:clusterArtisan:934".................129) "{art}:clusterArtisan:9830"130) "{art}:clusterArtisan:9470" 192.168.18.132:8005>將第一次的返回結果 13848 ,作為第二次的入參
192.168.18.132:8005> scan 13848 match {art}:clusterArtisan:9* count 1000 1) "11596" 2) 1) "{art}:clusterArtisan:9347"2) "{art}:clusterArtisan:9737"3) "{art}:clusterArtisan:9943"4) "{art}:clusterArtisan:9944".................122) "{art}:clusterArtisan:9744"123) "{art}:clusterArtisan:9256" 192.168.18.132:8005>省略過程 …
依次,獲取 ,直到cursor為0 ,遍歷結束
192.168.18.132:8005> scan 13209 match {art}:clusterArtisan:9* count 10000 1) "0" 2) 1) "{art}:clusterArtisan:927"2) "{art}:clusterArtisan:9474"3) "{art}:clusterArtisan:9685"........................424) "{art}:clusterArtisan:9424" 192.168.18.132:8005>我們發現limit 是 1000,但是返回的結果并不是返回1000個。因為這個 limit 不是限定返回結果的數量,而是限定服務器單次遍歷的字典槽位數量(約等于)。
如果將 limit 設置為 10,你會發現返回結果是空的,但是游標值不為零,意味著遍歷還沒結束。
比如
192.168.18.132:8005> scan 13822 match {art}:clusterArtisan:999* count 1000 1) "13209" 2) (empty list or set) 192.168.18.132:8005>字典的結構
在 Redis 中所有的 key 都存儲在一個很大的字典中.
這個字典的結構和 Java 中的HashMap 一樣,是一維數組 + 二維鏈表結構.
第一維數組的大小總是 2^n(n>=0),擴容一次數組大小空間加倍,也就是 n++。
scan 指令返回的游標就是第一維數組的位置索引,我們將這個位置索引稱為槽 (slot)。
如果不考慮字典的擴容縮容,直接按數組下標挨個遍歷就行了。
limit 參數就表示需要遍歷的槽位數,之所以返回的結果可能多可能少,是因為不是所有的槽位上都會掛接鏈表,有些槽位可能是空的,還有些槽位上掛接的鏈表上的元素可能會有多個。
每一次遍歷都會將 limit數量的槽位上掛接的所有鏈表元素進行模式匹配過濾后,一次性返回給客戶端。
scan 遍歷順序 (高位進位法)
scan 的遍歷順序非常特別。它不是從第一維數組的第 0 位一直遍歷到末尾,而是采用了高位進位加法來遍歷。之所以使用這樣特殊的方式進行遍歷,是考慮到字典的擴容和縮容時避免槽位的遍歷重復和遺漏.
高位進位法從左邊加,進位往右邊移動,同普通加法正好相反。但是最終它們都會遍歷所有的槽位并且沒有重復。
漸進式 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 掃描 --bigkeys
[redis@artisan redis-5.0.3]$ ./bin/redis-cli -c -h 192.168.18.131 -p 8001 -a artisan --bigkeys Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.# Scanning the entire keyspace to find biggest keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed).[00.00%] Biggest string found so far 'artisan3' with 1 bytes-------- summary -------Sampled 2 keys in the keyspace! Total key length in bytes is 15 (avg len 7.50)Biggest string found 'artisan3' has 1 bytes2 strings with 2 bytes (100.00% of keys, avg size 1.00) 0 lists with 0 items (00.00% of keys, avg size 0.00) 0 sets with 0 members (00.00% of keys, avg size 0.00) 0 hashs with 0 fields (00.00% of keys, avg size 0.00) 0 zsets with 0 members (00.00% of keys, avg size 0.00) 0 streams with 0 entries (00.00% of keys, avg size 0.00) [redis@artisan redis-5.0.3]$如果你擔心這個指令會大幅抬升 Redis 的 ops 導致線上報警,還可以增加一個休眠參數。
--bigkeys -i 0.1上面這個指令每隔 100 條 scan 指令就會休眠 0.1s,ops 就不會劇烈抬升,但是掃描的時間會變長.
[redis@artisan redis-5.0.3]$ ./bin/redis-cli -c -h 192.168.18.131 -p 8001 -a artisan --bigkeys -i 0.1使用scan的注意事項 20201101更新
如果在scan的過程中如果有鍵的變化(增加、 刪除、 修改) ,遍歷效果可能會碰到如下問題: 新增的鍵可能沒有遍歷到, 遍歷出了重復的鍵等情況, 也就是說scan并不能保證完整的遍歷出來所有的鍵, 我們在使用的過程中需要考慮到這一點。
總結
以上是生活随笔為你收集整理的Redis进阶-如何从海量的 key 中找出特定的key列表 Scan详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis进阶-Redis的惰性删除
- 下一篇: Redis进阶-Stream多播的可持久