ZSet数据结构类似于Set结构,只是ZSet结构中,每个元素都会有一个分值,然后所有元素按照分值的大小进行排列,相当于是一个进行了排序的链表。
zset
如果ZSet是一个链表,而且内部元素是有序的,在进行元素插入和删除,以及查询的时候,就必须要遍历链表才行,时间复杂度就达到了O(n),这个在以单线程处理的Redis中是不能接受的。所以ZSet采用了一种跳跃表的实现。这个实现有点类似于Kafka存储消息是使用的稀疏索引,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跳跃表
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()
}
执行效果: