我的個人博客:https://www.wuyizuokan.com
介紹:
ZSet數據結構類似於Set結構,只是ZSet結構中,每個元素都會有一個分值,然后所有元素按照分值的大小進行排列,相當於是一個進行了排序的鏈表。
如果ZSet是一個鏈表,而且內部元素是有序的,在進行元素插入和刪除,以及查詢的時候,就必須要遍歷鏈表才行,時間復雜度就達到了O(n),這個在以單線程處理的Redis中是不能接受的。所以ZSet采用了一種跳躍表的實現。這個實現有點類似於Kafka存儲消息是使用的稀疏索引,kafka這個相對較簡單,可以用來介紹類比學習。
如果熟悉Kafka,就知道Kafka在進行持久化的時候,生成了兩個文件,一個是xxxxxxx.log,一個是xxxxxxx.index,這其中log文件中以鏈表的形式保存着消息的詳細信息,而index文件中,則是保存着這些消息的索引,或者說偏移量,但又不是每一條消息的索引都在index文件中存在,而是稀疏的,比如log文件中的消息的索引從0-10000,那么index文件中存儲的索引可能是100, 500, 700, 1000, 5000, 6500,每一個索引中都保存着對應的log文件中的消息的具體位置,如圖:
當要訪問偏移量為899的這條消息時,先去index文件中查找,找到了700和1000這個區間,根據700這個索引中的信息,找到log文件中700這條消息的具體位置,然后順序往下查找,直到找到索引為899的這條消息為止。從這個實現中我們可以看到,Kafka並沒有進行log文件的整個遍歷,而是通過index中的稀疏索引,找到消息在log中的大概位置,然后順序遍歷找到消息,這樣就大大提高了查找的效率,如圖:
Redis的跳躍表和上面類似,只是更加復雜一些,Kafka的稀疏索引只有一層,而Redis的索引被提取為多層。如圖:
所有的元素都會在L0層的鏈表中,根據分數進行排序,同時會有一部分節點有機會被抽取到L1層中,作為一個稀疏索引,同樣L1層中的索引也有一定機會被抽取到L2層中,組成一個更稀疏的索引列表。
下面用圖來演示一下在對快速鏈表進行插入、刪除、查詢時,是如何定位到L0層中的具體位置的。
首先,假定有這么一個鏈表,注意這里只展示分數,而不展示具體的值了:
如果要查找分數為66的元素,首先在L2層的索引找。很明顯,66位於25和85中間,這時就縮小了查找區間:
然后根據獲得的區間,去L1對應的區間中查找,得到一個更精確的區間:
最終,根據這個更精確的區間,去L0層順序遍歷,即可得到要查找的元素:
上述即是對Redis的跳躍表的原理的一個簡述。
這種跳躍表的實現,其實和二分查找的思路有點接近,只是一方面因為二分查找只能適用於數組,而無法適用於鏈表,所以為了讓鏈表有二分查找類似的效率,就以空間換時間來達到目的。
跳躍表因為是一個根據分數權重進行排序的列表,可以再很多場景中進行應用,比如排行榜,搜索排序等等。
命令操作:
添加元素,zadd zsetName score1 value1 score2 value2 score3 value3 .....
查看所有元素,zrange zsetName 0 -1
查看所有元素,按score逆序排列, zrevrange zsetName 0 -1
元素數量,zcard zsetName
獲取指定value的分數, zscore zsetName value
獲取指定value的排名,zrank zsetName value(從0開始)
獲取指定分值區間中的元素, zrangebyscore zsetName scoreStart scoreEnd(包含上下區間)(注意inf表示無窮大,-inf表示服務券大)
獲取指定分值區間中的元素,並且返回分數, zrangebyscore zsetName scoreStart scoreEnd withscores
刪除元素,zrem zsetName value
代碼調用:
package main import ( "fmt" "github.com/garyburd/redigo/redis" ) func main(){ // 連接redis conn,err := redis.Dial("tcp", "localhost:6379") if err != nil { fmt.Errorf("connection redis failed. error info: ", err) return } // zadd _,err = conn.Do("zadd", "phones", "100", "Nokia", "80", "tianyu", "60", "xiaomifeng", "50", "shangshai") if err != nil { fmt.Errorf("sadd failed, error info: ", err) return } // zrange result,err := redis.Strings(conn.Do("zrange", "phones", "0", "-1")) if err != nil { fmt.Errorf("zrange failed, error info: ", err) return } fmt.Println(result) // zrevrange result,err = redis.Strings(conn.Do("zrevrange", "phones", "0", "-1")) if err != nil { fmt.Errorf("zrange failed, error info: ", err) return } fmt.Println(result) // zcard size,err := conn.Do("zcard", "phones") if err != nil { fmt.Errorf("zrange failed, error info: ", err) return } fmt.Println(size) // zscore score,err := redis.Int(conn.Do("zscore", "phones", "shangshai")) if err != nil { fmt.Errorf("zrange failed, error info: ", err) return } fmt.Println(score) // zrem _,err = conn.Do("zrem", "phones", "shangshai") if err != nil { fmt.Errorf("zrange failed, error info: ", err) return } fmt.Println("delete shangshai success.") // 關閉連接 defer conn.Close() }
執行效果: