• 3. Redis有哪些数据类型?


    Redis的数据类型可谓是Redis的精华所在,同样的数据类型,但不同的值对应的存储结构也是不同的。比如:当你存储一个短字符串(小于44字节),实际存储的结构是embstr;长字符串对应的实际存储结构是raw,这样设计的目的就是为了更好的节约内存。

    问题:Redis都有哪些数据类型呢?

    最常用的数据类型有5种:String(字符串类型)、Hash(字典类型)、List(列表类型)、Set(集合类型)、ZSet(有序集合类型)。

    那么这些数据类型都支持哪些操作呢?我们来一一介绍,当然Redis支持的数据结构已经不止这五种了,还有几个更高级的数据结构,我们后面介绍,但是最常用的还是上面五种。

    不过在介绍之前需要安装Redis,安装过程比较简单就不说了,我这里就使用docker安装了,比较简单。

    通过:docker run -d -p 6379:6379 --name redis redis后台启动。

    启动之后,我们需要了解一下Redis的前置知识。

    • Redis默认有16个数据库,数据库名类似于数组的下表,从0到15,默认使用0号库。
    • select:切换数据库,比如select 1就表示切换到1号库
    • 统一密码管理,16个库都是一样的密码,要么都ok要么都连接不上。
    • 默认端口是6379

    Redis字符串(String)

    Redis的string类型,是一个key对应一个value。并且底层是使用自己内部实现的简单动态字符串(SSD),来表示 String 类型。 没有直接使用 C 语言定义的字符串类型。

    struct sdshdr{
        //记录 buf 数组中已使用字节的数量
        //等于 SDS 保存字符串的长度
        int len;
        //记录 buf 数组中未使用字节的数量
        int free;
        //字节数组,用于保存字符串
        char buf[];
    }
    

    然后我们来看看其支持的api操作。

    set key value:给指定的key设置value

    127.0.0.1:6379> set name hanser
    OK
    127.0.0.1:6379> set word "hello world"
    OK  # 如果字符串之间有空格,我们可以使用双引号包起来
    

    设置成功之后会返回一个ok,表示设置成功。除此之外,set还可以指定一些可选参数。

    • set key value ex 60:设置的时候指定过期时间为60秒,等价于setex key 60 value
    • set key value px 60:设置的时候指定过期时间为60毫秒,等价于psetex key 60 value
    • set key value nx:只有key不存在的时候才会设置,存在的话则会什么也不做,而如果不加nx则会覆盖。等价于setnx key value
    • set key value xx:只有key存在的时候才会设置,注意:对于xx来说,没有setxx key value

    我们发现默认参数使用set足够了,因此未来可能会移除setex、psetex、setnx。另外,我们可以同一个key多次set,相当于对原来的值进行了覆盖。

    get key:获取指定key对应的value

    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> get word
    "hello world"
    127.0.0.1:6379> get age
    (nil)
    

    如果key不存在,那么返回nil,也就是C语言中的NULL,python中的None、golang里的nil。存在的话,则返回key对应的value。

    del key1 key2···:删除指定key,可以同时删除多个

    127.0.0.1:6379> set age 28
    OK
    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> get age
    "28"  # 虽然我们设置的是一个数值,但是在Redis中都是字符串格式
    127.0.0.1:6379> del name age gender
    (integer) 2  # 会返回删除的key的个数,表示有效删除了两个,而gender不存在,因此无法删除一个不存在的key
    127.0.0.1:6379> get name
    (nil)
    127.0.0.1:6379> get nil
    (nil)
    127.0.0.1:6379> 
    

    append key value:追加

    如果key存在,那么会将value的值追加到key对应的值的末尾,如果不存在,那么会重新设置,类似于set key value。

    127.0.0.1:6379> set name han
    OK
    127.0.0.1:6379> set age 2
    OK
    127.0.0.1:6379> append name ser
    (integer) 6  # 返回拼接之后的字符数量
    127.0.0.1:6379> append age 8
    (integer) 2  # 按照字符串的格式拼接
    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> get age
    "28"
    127.0.0.1:6379> append gender female
    (integer) 6  # gender不存在,相当于重新设置,或者你理解为往一个空字符后面追加也行
    127.0.0.1:6379> get gender
    "female"
    127.0.0.1:6379> 
    

    strlen key:查看对应key的长度

    127.0.0.1:6379> strlen name
    (integer) 6
    127.0.0.1:6379> strlen age
    (integer) 2
    127.0.0.1:6379> strlen not_exists
    (integer) 0  # 不存在的key返回0
    127.0.0.1:6379> 
    

    incr key:为key存储的值自增1,必须可以转成整型,否则报错。如果不存在key,默认先设置该key值为0,然后自增1

    127.0.0.1:6379> get age
    "28"
    127.0.0.1:6379> incr age
    (integer) 29  # 返回自增后的结果
    127.0.0.1:6379> get age
    "29"
    127.0.0.1:6379> incr age1 
    (integer) 1
    127.0.0.1:6379> get age1
    "1"
    127.0.0.1:6379> incr name
    (error) ERR value is not an integer or out of range
    127.0.0.1:6379> 
    

    decr key:为key存储的值自减1,必须可以转成整型,否则报错。如果不存在key,默认先设置该key值为0,然后自减1

    127.0.0.1:6379> decr age
    (integer) 28
    127.0.0.1:6379> decr age2
    (integer) -1
    127.0.0.1:6379> 
    

    incrby key number:为key存储的值自增number,必须可以转成整型,否则报错,如果不存在的话,默认先将该值设置为0,然后自增number

    127.0.0.1:6379> incrby age 20
    (integer) 48
    127.0.0.1:6379> incrby age3 5
    (integer) 5
    127.0.0.1:6379> 
    

    decrby key number:为key存储的值自减number,必须可以转成整型,否则报错,如果不存在的话,默认先将该值设置为0,然后自减number

    127.0.0.1:6379> decrby age 20
    (integer) 28
    127.0.0.1:6379> decrby age4 5
    (integer) -5
    127.0.0.1:6379> decrby age4 -5
    (integer) 0  # 指定负数也是可以的,同理incrby也是如此
    127.0.0.1:6379> 
    

    getrange key start end:获取指定value的同时指定范围,第一个字符为0,最后一个为-1。注意:redis中的索引都是包含结尾的,不管是这里的getrange,还是后面的列表操作,索引都是包含两端的。

    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> getrange name 0 -1
    "hanser"
    127.0.0.1:6379> getrange name 0 4
    "hanse"
    127.0.0.1:6379> getrange name -3 -1
    "ser"
    127.0.0.1:6379> getrange name -3 10086
    "ser"
    127.0.0.1:6379> getrange name -3 -4
    ""
    127.0.0.1:6379> 
    

    我们看到,索引是可以从后往前数,但是只能从前往后、不能从后往前获取。也就是getrange word -1 -3是不可以的,会返回一个空字符串,因为-1在-3的后面。

    setrange key start value:从索引为start的地方开始,将key对应的值替换为value,替换的个数等于value的个数。

    127.0.0.1:6379> get name
    "hanser"
    127.0.0.1:6379> setrange name 0 you
    (integer) 6  # 从索引为0的地方开始替换,替换三个字符,因为我们指定了3个字符
    127.0.0.1:6379> get name
    "youser"
    127.0.0.1:6379> setrange name 10 you
    (integer) 13  # 从索引为10的地方开始替换,但是字符串索引最大为6,因此会使用x00填充
    127.0.0.1:6379> get name
    "youserx00x00x00x00you"
    127.0.0.1:6379> setrange myself 3 gagaga
    (integer) 9  # 对于不存在的key也是如此
    127.0.0.1:6379> get myself
    "x00x00x00gagaga"
    127.0.0.1:6379> set name hanser
    OK
    127.0.0.1:6379> setrange name 0 "han han han han"
    (integer) 15  # 替换的字符串长度比相应的key长没有关系,会自动扩充
    127.0.0.1:6379> get name
    "han han han han"
    

    mset key1 value1 key2 value2:同时设置多个key value

    这是一个原子性操作,要么都设置成功,要么都设置不成功。注意:这些都是会覆盖原来的值的,如果不想这样的话,可以使用msetnx,这个命令只会在所有的key都不存在的时候才会设置。

    mget key1 key2:同时返回多个key对应的value

    如果有的key不存在,那么返回nil。

    127.0.0.1:6379> get name
    "han han han han"
    127.0.0.1:6379> mset name hanser age 28
    OK
    127.0.0.1:6379> mget name age
    1) "hanser"
    2) "28"
    127.0.0.1:6379> 
    

    getset key value:先返回key的旧值,然后设置新值

    127.0.0.1:6379> getset name yousa
    "hanser"
    127.0.0.1:6379> get name
    "yousa"
    127.0.0.1:6379> getset ping pong
    (nil)
    127.0.0.1:6379> get ping
    "pong"
    127.0.0.1:6379> 
    

    如果有的key不存在,那么返回nil,然后设置。

    另外,Redis中还有很多关于key的操作,有必要提前说一下。

    keys pattern:查看名称满足pattern所有的key,像string、list、hash、set、zset都有自己的key,key不可以重名,比如有一个叫name的string,那么就不可以再有一个还叫name的。

    127.0.0.1:6379> keys *  # 查看所有的key
     1) "gender"
     2) "word"
     3) "ping"
     4) "name"
     5) "age2"
     6) "age1"
     7) "age"
     8) "myself"
     9) "age3"
    10) "age4"
    127.0.0.1:6379> keys *a*  # 查看包含a的key
    1) "name"
    2) "age2"
    3) "age1"
    4) "age"
    5) "age3"
    6) "age4"
    127.0.0.1:6379> keys age?  # 查看以age开头、总共4个字符的key
    1) "age2"
    2) "age1"
    3) "age3"
    4) "age4"
    

    exists key:判断某个key是否存在

    127.0.0.1:6379> exists name
    (integer) 1
    127.0.0.1:6379> exists name1
    (integer) 0  # 存在返回1,不存在返回0
    127.0.0.1:6379> exists name name1
    (integer) 1  # 也可以指定多个key,返回存在的key的个数,但是此时无法判断到底是哪个key存在
    

    ttl key:查看还有多少秒过期,-1表示永不过期,-2表示已过期

    127.0.0.1:6379> ttl name
    (integer) -1  # -1表示永不过期
    127.0.0.1:6379> ttl name1
    (integer) -2  # -2表示已经过期
    127.0.0.1:6379> 
    

    key是可以设置过期时间的,如果过期了就不能再用了。我们看到name1这个key压根就不存在,返回的也是-2,因为过期了就相当于不存在了。而name是-1,表示永不过期

    expire key 秒钟:为给定的key设置过期时间

    127.0.0.1:6379> expire name 60
    (integer) 1  # 设置60s,设置成功返回1
    127.0.0.1:6379> ttl name
    (integer) 55  # 查看时间,还剩下55秒
    127.0.0.1:6379> expire name1 60
    (integer) 0  # name1不存在,设置失败,返回0
    127.0.0.1:6379> 
    

    这里设置60s的过期时间。另外设置完之后,在过期时间结束之前是可以再次设置的,比如我先设置了60s,然后快结束的时候我再次设置60s,那么还会再持续60s。

    type key:查看你的key是什么类型

    127.0.0.1:6379> type name
    none  # name过期了,相当于不存在了,因此为none
    127.0.0.1:6379> type age
    string  # 类型为string
    127.0.0.1:6379>
    

    move key db:将key移动到指定的db中

    127.0.0.1:6379> flushdb
    OK  # 清空当前库,如果是清空所有库,可以使用flushall,当然后面都可以加上async,表示异步删除,我们前面说过的
    127.0.0.1:6379> set name hanser
    OK
    127.0.0.1:6379> keys *
    1) "name"
    127.0.0.1:6379> move name 3
    (integer) 1  # 将name移动到索引为3的库中
    127.0.0.1:6379> keys *
    (empty array)  # 此时当前库已经没有name了
    127.0.0.1:6379> select 3
    OK  # 切换到索引为3的库中
    127.0.0.1:6379[3]> keys *
    1) "name"  # keys *查看,发现name已经有了
    127.0.0.1:6379[3]> select 0  # 切换回来
    OK
    127.0.0.1:6379> 
    

    然后我们来看看如何使用Python来操作Redis,这里操作字符串

    笔者是Python系的,所以如果你不搞Python的话,那么可以不用看。

    import redis
    
    # 获取得到的默认是字节,如果指定了decode_responses,会自动进行解码
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. set key value
    client.set("name", "yousa", ex=None, px=None, nx=False, xx=False)
    
    # 2. get key
    print(client.get("name"), client.get("age"))  # yousa None
    
    # 3. del key1 key2 ...
    print(client.delete("name", "age"))  # 1
    
    # 4. apend key value
    client.set("name", "han")
    print(client.append("name", "ser"))  # 6
    print(client.get("name"))  # hanser
    
    # 5. strlen key
    print(client.strlen("name"))  # 6
    
    # 6. incr key
    client.set("age", 28)
    # incr key其实等价于incrby key 1,因此在这里两个命令都是通过incr实现
    # 第二个参数为1,是默认值,当然我们也可以自己指定
    client.incr("age", 1)
    print(client.get("age"))  # 29
    
    # 7. decr key
    client.decr("age", 10)
    print(client.get("age"))  # 19
    
    # 8. getrange key start end
    print(client.getrange("name", -3, -1))  # ser
    
    # 9. setrange key start value
    client.setrange("name", 3, "sa")
    print(client.get("name"))  # hansar
    
    # 10. mset key1 value1 key2 value2
    client.mset({"name": "yousa", "age": 20})
    
    # 11. mget key1 key2
    print(client.mget(["name", "age", "gender"]))  # ['yousa', '20', None]
    
    # 12. getset key value
    print(client.getset("name", "hanser"))  # yousa
    print(client.get("name"))  # hanser
    
    # 13. keys pattern
    print(client.keys("*"))  # ['name', 'age']
    
    # 14. exists key
    print(client.exists("name"), client.exists("ping"))  # 1 0
    
    # 15. ttl key
    print(client.ttl("name"))  # -1
    
    # 16. expire key 秒钟
    client.expire("name", 60) 
    import time; time.sleep(2)
    print(client.ttl("name"))  # 58
    
    # 17. type key
    print(client.type("name"))  # string
    
    # 18. move key db
    client.move("name", 15)
    print(client.get("name"))  # None
    # 需要重新连接
    print(redis.Redis(host="47.94.174.89", decode_responses="utf-8", db=15).get("name"))  # hanser
    

    我们看到,和Redis命令之间是几乎没有什么区别的。

    字符串这种数据结构可以用在什么地方呢?

    我们讨论完字符串的相关操作,那么我们还要理解字符串要用在什么地方。

    首先字符串类型的使用场景有很多,但从功能的角度来区分,大致可分为以下两种:

    • 1. 字符串存储和操作;
    • 2. 整数类型和浮点类型的存储和计算。

    其最常用的业务场景大致分为以下几个。

    1. 页面数据缓存

    我们知道,一个系统最宝贵的资源就是数据库资源,随着公司业务的发展壮大,数据库的存储量也会越来越大,并且要处理的请求也越来越多,当数据量和并发量到达一定级别之后,数据库就变成了拖慢系统运行的“罪魁祸首”,为了避免这种情况的发生,我们可以把查询结果放入缓存(Redis)中,让下次同样的查询直接去缓存系统取结果,而非查询数据库,这样既减少了数据库的压力,同时也提高了程序的运行速度。

    画一张图来说明一下:

    2. 数据计算与统计

    Redis 可以用来存储整数和浮点类型的数据,并且可以通过命令直接累加并存储整数信息,这样就省去了每次先要取数据、转换数据、拼加数据、再存入数据的麻烦,只需要使用一个命令就可以完成此流程。比如:微博、哔哩哔哩等社交平台,我们经常会点赞,然后还有点赞数。每点一个赞,点赞数就加1,这个功能就完全可以交给Redis实现。

    3. 共享Session信息

    通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。

    例如用户A的 Session 信息被存储在第一台服务器,但第二次访问时用户A被分配到第二台服务器,这个时候服务器并没有用户A的 Session 信息,就会出现需要重复登录的问题。分布式系统每次会把请求随机分配到不同的服务器,因此我们需要借助缓存系统对这些 Session 信息进行统一的存储和管理,这样无论请求发送到哪台服务器,服务器都会去统一的缓存系统获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。

    • 分布式系统单独存储Session

    • 分布式系统使用统一的缓存系统存储Session

    Redis列表(List)

    列表类型 (List) 是一个使用链表结构存储的有序结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作 (插入、删除) 时间复杂度为 O(1),所以相对来说速度还是比较快的,但它的查询时间复杂度为 O(n),因此查询可能会比较慢。

    Redis中的列表和字符串比较类似,只不过字符串是一个key对应一个value、获取的时候直接通过key来获取,而列表是一个key对应的多个value、获取的时候通过key + 索引来获取。

    下面我们来看看它所支持的api操作

    lpush key value1 value2···:将多个值设置到列表里面,从左边push

    rpush key value1 value2···:将多个值设置到列表里面,从右边push

    127.0.0.1:6379> lpush girls mashiro koishi
    (integer) 2  # 返回插入成功之后,列表的元素个数。这里是lpush,所以此时列表内的元素是:koishi mashiro
    127.0.0.1:6379> rpush girls satori
    (integer) 3
    127.0.0.1:6379> 
    

    lrange key start end:遍历列表,索引从0开始,最后一个为-1,且包含两端

    127.0.0.1:6379> lrange girls 0 -1
    1) "koishi"
    2) "mashiro"
    3) "satori"
    127.0.0.1:6379> lrange girls 0 2
    1) "koishi"
    2) "mashiro"
    3) "satori"
    127.0.0.1:6379> lrange girls 0 1
    1) "koishi"
    2) "mashiro"
    127.0.0.1:6379> lrange lst 0 -1
    (empty array)  # 对不存在的列表使用lrange,会得到空数组
    

    lpop key:从列表的左端弹出一个值,列表长度改变

    rpop key:从列表的右端弹出一个值,列表长度改变

    127.0.0.1:6379> lpop girls
    "koishi"
    127.0.0.1:6379> rpop girls
    "satori"
    127.0.0.1:6379> lrange girls 0 -1
    1) "mashiro"
    127.0.0.1:6379> 
    

    lindex key index:获取指定索引位置的元素,列表长度不变

    127.0.0.1:6379> lindex girls 0
    "mashiro"
    127.0.0.1:6379> lrange girls 0 -1
    1) "mashiro"
    127.0.0.1:6379> lindex lst 0 
    (nil)  # 对不存在的列表使用lindex,会得到nil
    127.0.0.1:6379> 
    

    llen key:获取指定列表的长度

    127.0.0.1:6379> llen girls
    (integer) 1
    127.0.0.1:6379> llen lst
    (integer) 0  # 对不存在的列表使用llen,会得到0。
    127.0.0.1:6379> 
    

    lrem key count value:删除count个value,如果count为0,那么将全部删除

    127.0.0.1:6379> lpush lst  1 1 1 1
    (integer) 4
    127.0.0.1:6379> lrem lst 3 1
    (integer) 3  # 删除3个1
    127.0.0.1:6379> lrange lst 0 -1
    1) "1"
    127.0.0.1:6379> 
    

    ltrim key start end:从start截取到end,再重新赋值给key

    127.0.0.1:6379> rpush lst 2 3 4 5
    (integer) 5
    127.0.0.1:6379> lrange lst 0 -1
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    5) "5"
    127.0.0.1:6379> ltrim lst 4 -1
    OK  # 将5 重新赋值给lst
    127.0.0.1:6379> lrange lst 0 -1
    1) "5"
    127.0.0.1:6379> 
    

    rpoplpush key1 key2:移除key1的最后一个元素,并添加到key2的开头

    127.0.0.1:6379> rpush lst1 1 2 3
    (integer) 3
    127.0.0.1:6379> rpush lst2 11 22 33
    (integer) 3
    127.0.0.1:6379> rpoplpush lst1 lst2
    "3"
    127.0.0.1:6379> lrange lst2 0 -1
    1) "3"
    2) "11"
    3) "22"
    4) "33"
    127.0.0.1:6379> 
    

    lset key index value:将key中索引为index的元素设置为value

    127.0.0.1:6379> lrange lst2 0 -1
    1) "3"
    2) "11"
    3) "22"
    4) "33"
    127.0.0.1:6379> lset lst2 1 2333
    OK
    127.0.0.1:6379> lrange lst2 0 -1
    1) "3"
    2) "2333"
    3) "22"
    4) "33"
    127.0.0.1:6379> lset lst2 10 2333
    (error) ERR index out of range  # 索引越界则报错,显然索引为10越界了
    127.0.0.1:6379> 
    

    linsert key before/after value1 value2:在value1的前面或者后面插入一个value2

    127.0.0.1:6379> rpush lst3 1 2 2 3
    (integer) 4
    127.0.0.1:6379> linsert lst3 before 2 666
    (integer) 5
    127.0.0.1:6379> lrange lst3 0 -1
    1) "1"
    2) "666"
    3) "2"
    4) "2"
    5) "3"
    127.0.0.1:6379> linsert lst3 after 2 2333
    (integer) 6
    127.0.0.1:6379> lrange lst3 0 -1
    1) "1"
    2) "666"
    3) "2"
    4) "2333"
    5) "2"
    6) "3"
    127.0.0.1:6379> 
    

    我们看到插入位置是由第一个元素决定的

    老规矩,我们来看看如何使用Python来操作Redis中的列表,和操作字符串是类似的,因为Python操作Redis的模块提供的api和Redis-cli控制台所使用的api是高度一致的。

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. lpush key value1 value2 ...
    client.lpush("list", 1, 2, 3)
    
    # 2. rpush key value1 value2 ...
    client.rpush("list", 2, 2, 2)
    
    # 3. lrange key start end
    print(client.lrange("list", 0, -1))  # ['3', '2', '1', '2', '2', '2']
    
    # 4. lpop key
    print(client.lpop("list"))  # 3
    
    # 5. rpop key
    print(client.rpop("list"))  # 2
    
    # 6. lindex key
    print(client.lindex("list", 0))  # 2
    
    # 7. llen key
    print(client.llen("list"))  # 4
    
    # 8. lrem key count value
    client.lrem("list", 1, 2)
    print(client.lrange("list", 0, -1))  # ['1', '2', '2']
    
    # 9. ltrim key start end
    print(client.lrange("list", 0, -1))  # ['1', '2', '2']
    client.ltrim("list", 0, -2)
    print(client.lrange("list", 0, -1))  # ['1', '2]
    
    # 10. rpoplpush key1 key2
    client.rpush("list1", 1, 2, 3)
    client.rpush("list2", 11, 22, 33)
    client.rpoplpush("list1", "list2") 
    print(client.lrange("list2", 0, -1))  # ['3', '11', '22', '33']
    
    # 11. lset key index value
    client.lset("list2", -1, "古明地觉")
    print(client.lrange("list2", 0, -1))  # ['3', '11', '22', '古明地觉']
    
    # 12. linsert key before/after value1 value2
    client.linsert("list2", "before", 22, "aaa")
    client.linsert("list2", "after", 22, "bbb")
    print(client.lrange("list2", 0, -1))  # ['3', '11', 'aaa', '22', 'bbb', '古明地觉']
    

    然后我们来分析一下Redis列表类型的内部实现

    127.0.0.1:6379> object encoding list
    "quicklist"
    127.0.0.1:6379> 
    

    我们看到列表底层的数据类型是quicklist(快速列表),quicklist是Redis3.2引入的数据类型,早期的列表是使用ziplist(压缩列表)和双向列表组成的,Redis3.2的时候改为quicklist。

    我们看一下quicklist的源码实现。

    typedef struct quicklist { // src/quicklist.h
        quicklistNode *head;
        quicklistNode *tail;
        unsigned long count;        /* ziplist 的个数 */
        unsigned long len;          /* quicklist 的节点数 */
        unsigned int compress : 16; /* LZF 压缩算法深度 */
        //...
    } quicklist;
    typedef struct quicklistNode {
        struct quicklistNode *prev;
        struct quicklistNode *next;
        unsigned char *zl;           /* 对应的 ziplist */
        unsigned int sz;             /* ziplist 字节数 */
        unsigned int count : 16;     /* ziplist 个数 */
        unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
        unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
        unsigned int recompress : 1; /* 该节点先前是否被压缩 */
        unsigned int attempted_compress : 1; /* 节点太小无法压缩 */
        //...
    } quicklistNode;
    typedef struct quicklistLZF {
        unsigned int sz; 
        char compressed[];
    } quicklistLZF;
    

    从源码中可以看出quicklist是一个双向链表,链表中的每一个节点实际上是一个ziplist,结构如图所示。

    ziplist 作为 quicklist 的实际存储结构,它本质是一个字节数组,ziplist 数据结构如下图所示:

    • zlbytes:压缩列表字节长度,占 4 字节;
    • zltail:压缩列表尾元素相对于起始元素地址的偏移量,占 4 字节;
    • zllen:压缩列表的元素个数;
    • entryX:压缩列表存储的所有元素,可以是字节数组或者是整数;
    • zlend:压缩列表的结尾,占 1 字节。

    使用场景

    列表的典型使用场景有以下两个:

    • 消息队列:列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列;
    • 文章列表:对于博客站点来说,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。

    Redis字典(Hash)

    字典类型(Hash)又被成为散列类型或是哈希表类型,它底层是通过哈希表存储的,这个哈希表包含两列数据:字段和值,假设我们使用字典来存储文章的详情信息,存储结构如图所示:

    同理我们也可以使用字典来存储用户信息,并且使用字典存储此类信息是不需要序列化和反序列化的,所以使用起来更加的方便和高效。

    下面看看字典所支持的api

    hset key field1 value1 field2 value2···:设置键值对,可同时设置多个。(这里的键值对指的是field、value,而命令中的key指的是字典、或者哈希表的名称)

    127.0.0.1:6379> hset girl name hanser age 28 gender f
    (integer) 3  # 返回3表示成功设置3个键值对
    127.0.0.1:6379> 
    

    hget key field:获取hash中field对应的value

    127.0.0.1:6379> hget girl name
    "hanser"
    127.0.0.1:6379> 
    

    hgetall key:获取hash中所有的键值对

    127.0.0.1:6379> hgetall girl
    1) "name"
    2) "hanser"
    3) "age"
    4) "28"
    5) "gender"
    6) "f"
    127.0.0.1:6379> 
    

    hlen key:获取hash中键值对的个数

    127.0.0.1:6379> hlen girl
    (integer) 3
    127.0.0.1:6379> 
    

    hexists key field:判断hash中是否存在指定的field

    127.0.0.1:6379> hexists girl name
    (integer) 1  # 存在返回1
    127.0.0.1:6379> hexists girl where
    (integer) 0  # 不存在返回0
    127.0.0.1:6379> 
    

    hkeys/hvals key:获取hash中所有的field和所有的value

    127.0.0.1:6379> hkeys girl
    1) "name"
    2) "age"
    3) "gender"
    127.0.0.1:6379> hvals girl
    1) "hanser"
    2) "28"
    3) "f"
    127.0.0.1:6379> 
    

    hincrby key field number:将hash中字段field对应的值自增number,number必须指定,显然field对应的value要能解析成整型

    127.0.0.1:6379> hincrby girl age 3
    (integer) 31  # 返回增加之后的值
    127.0.0.1:6379> hincrby girl age -3
    (integer) 28  # 可以为正、可以为负
    127.0.0.1:6379> 
    

    hsetnx key field1 value1:每次只能设置一个键值对,不存在则设置,存在则无效。

    127.0.0.1:6379> hsetnx girl name yousa
    (integer) 0  # name存在,所以设置失败
    127.0.0.1:6379> hget girl name
    "hanser"  # 还是原来的结果
    127.0.0.1:6379> hsetnx girl length 155.5
    (integer) 1  # 设置成功
    127.0.0.1:6379> hget girl length
    "155.5"
    127.0.0.1:6379> 
    

    hdel key field1 field2······:删除hash中的键,当然键没了,整个键值对就没了

    127.0.0.1:6379> hdel girl name age
    (integer) 2
    127.0.0.1:6379> hget girl name
    (nil)
    127.0.0.1:6379> hget girl age
    (nil)
    127.0.0.1:6379> 
    

    然后老规矩,看看如何使用Python操作Redis的字典

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. hset key field1 value1 field2 value2···
    # 这里的hset只能设置单个值,如果设置多个需要使用hmset,传入key和字典
    client.hmset("girl1", {"name": "yousa", "age": 26, "length": 148})
    
    # 2. hget key field
    print(client.hget("girl1", "name"))  # yousa
    
    # 3. hgetall key
    print(client.hgetall("girl1"))  # {'name': 'yousa', 'age': '26', 'length': '148'}
    # 这里还支持同时获取多个值
    print(client.hmget("girl1", ["name", "age"]))  # ['yousa', '26']
    
    # 4. hlen key
    print(client.hlen("girl1"))  # 3
    
    # 5. hexists key field
    print(client.hexists("girl1", "name"))  # True
    print(client.hexists("girl1", "name1"))  # False
    
    # 6. hkeys/hvals key
    print(client.hkeys("girl1"))  # ['name', 'age', 'length']
    print(client.hvals("girl1"))  # ['yousa', '26', '148']
    
    # 7. hincrby key field number
    client.hincrby("girl1", "age", 2)  
    print(client.hget("girl1", "age"))  # 28
    
    # 8. hsetnx key field1 value1
    client.hsetnx("girl1", "name", "hanser")
    client.hsetnx("girl1", "gender", "female")
    print(client.hmget("girl1", ["name", "gender"]))  # ['yousa', 'female']
    
    # 9. hdel key field1 field2······
    client.hdel("girl1", "name", "age")
    print(client.hmget("girl1", ["name", "age"]))  # [None, None]
    

    那么Redis中的字典是如何实现的呢?

    字典类型本质上是由数组和链表结构组成的,来看字典类型的源码实现:

    typedef struct dictEntry { // dict.h
        void *key;
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
            double d;
        } v;
        struct dictEntry *next; // 下一个 entry
    } dictEntry;
    

    字典类型的数据结构,如下图所示:

    通常情况下字典类型会使用数组的方式来存储相关的数据,但发生哈希冲突时才会使用链表的结构来存储数据。

    哈希冲突

    字典类型的存储流程是先将键进行 Hash 计算,得到存储键对应的数组索引,再根据数组索引进行数据存储,但在小概率事件下可能会出完全不相同的键进行 Hash 计算之后,得到相同的 Hash 值,这种情况我们称之为哈希冲突。

    哈希冲突一般通过链表的形式解决,相同的哈希值会对应一个链表结构,每次有哈希冲突时,就把新的元素插入到链表的尾部,请参考上面数据结构的那张图。

    键查询的流程如下:

    • 通过算法 (Hash,计算和取余等) 操作获得数组的索引值,根据索引值找到对应的元素;
    • 判断元素和查找的键值是否相等,相等则成功返回数据,否则需要查看 next 指针是否还有对应其他元素,如果没有,则返回 null,如果有的话,重复此步骤。

    渐进式rehash

    Redis 为了保证应用的高性能运行,提供了一个重要的机制——渐进式 rehash。 渐进式 rehash 是用来保证字典缩放效率的,也就是说在字典进行扩容或者缩容是会采取渐进式 rehash 的机制。

    1) 扩容

    当元素数量等于数组长度时就会进行扩容操作,源码在 dict.c 文件中,核心代码如下:

    int dictExpand(dict *d, unsigned long size)
    {
        /* 需要的容量小于当前容量,则不需要扩容 */
        if (dictIsRehashing(d) || d->ht[0].used > size)
            return DICT_ERR;
        dictht n; 
        unsigned long realsize = _dictNextPower(size); // 重新计算扩容后的值
        /* 计算新的扩容大小等于当前容量,不需要扩容 */
        if (realsize == d->ht[0].size) return DICT_ERR;
        /* 分配一个新的哈希表,并将所有指针初始化为NULL */
        n.size = realsize;
        n.sizemask = realsize-1;
        n.table = zcalloc(realsize*sizeof(dictEntry*));
        n.used = 0;
        if (d->ht[0].table == NULL) {
            // 第一次初始化
            d->ht[0] = n;
            return DICT_OK;
        }
        d->ht[1] = n; // 把增量输入放入新 ht[1] 中
        d->rehashidx = 0; // 非默认值 -1,表示需要进行 rehash
        return DICT_OK;
    }
    

    从以上源码可以看出,如果需要扩容则会申请一个新的内存地址赋值给 ht[1],并把字典的 rehashindex 设置为 0,表示之后需要进行 rehash 操作。

    2) 缩容

    当字典的使用容量不足总空间的 10% 时就会触发缩容,Redis 在进行缩容时也会把 rehashindex 设置为 0,表示之后需要进行 rehash 操作。

    3) 渐进式rehash流程

    在进行渐进式 rehash 时,会同时保留两个 hash 结构,新键值对加入时会直接插入到新的 hash 结构中,并会把旧 hash 结构中的元素一点一点的移动到新的 hash 结构中,当移除完最后一个元素时,清空旧 hash 结构,主要的执行流程如下:

    • 1. 扩容或者缩容时把字典中的字段 rehashidx 标识为 0;
    • 2. 在执行定时任务或者执行客户端的 hset、hdel 等操作指令时,判断是否需要触发 rehash 操作(通过 rehashidx 标识判断),如果需要触发 rehash 操作,也就是调用 dictRehash 函数,dictRehash 函数会把 ht[0] 中的元素依次添加到新的 Hash 表 ht[1] 中;
    • 3. rehash 操作完成之后,清空 Hash 表 ht[0],然后对调 ht[1] 和 ht[0] 的值,把新的数据表 ht[1] 更改为 ht[0],然后把字典中的 rehashidx 标识为 -1,表示不需要执行 rehash 操作。

    那么Redis中的字典都在哪些场景中使用呢?

    哈希字典的典型使用场景如下:

    • 商品购物车,购物车非常适合用哈希字典表示,使用人员唯一编号作为 key(哈希表的名称),哈希表本身则负责存储商品的 id 和数量等信息;比如:hset person_id0001 product_id product001 count 20
    • 存储用户的属性信息,使用人员唯一编号作为key,哈希表存储属性字段和对应的值;
    • 存储文章详情页信息等。

    因此通过上面内容我们知道了字典类型实际是由数组和链表组成的,当字典进行扩容或者缩容时会进行渐进式 rehash 操作,渐进式 rehash 是用来保证 Redis 运行效率的,它的执行流程是同时保留两个哈希表,把旧表中的元素一点一点的移动到新表中,查询的时候会先查询两个哈希表,当所有元素都移动到新的哈希表之后,就会删除旧的哈希表。

    Redis集合(Set)

    Redis的集合和列表是类似的,都是用来存储多个标量,但是它和列表又有不同:

    • 1. 列表中的元素是可以重复的,而集合中的元组不会重复。
    • 2. 列表在插入元素的时候可以保持顺序,而集合不保证顺序(集合在存储数据时,底层也是使用了哈希表,后面会说。)。

    下面我们来看看它所支持的api操作

    sadd key value1 value2···:向集合插入多个元素,如果重复会自动去重

    127.0.0.1:6379> sadd set1 1 1 2 3
    (integer) 3  # 返回成功插入的元素的个数,这里是3个,因为元素有重复。两个1,只会插入一个
    127.0.0.1:6379> 
    

    smembers key:查看集合的所有元素

    127.0.0.1:6379> smembers set1
    1) "1"
    2) "2"
    3) "3"
    127.0.0.1:6379> 
    

    sismember key value:查看value是否在集合中

    127.0.0.1:6379> sismember set1 1
    (integer) 1  # 在的话返回1
    127.0.0.1:6379> sismember set1 5
    (integer) 0  # 不在返回0
    127.0.0.1:6379> 
    

    scard key:查看集合的元素个数

    127.0.0.1:6379> scard set1
    (integer) 3
    127.0.0.1:6379> 
    

    srem key value1 value2 ······:删除集合中的元素

    127.0.0.1:6379> srem set1 1 2
    (integer) 2  # 返回删除成功的元素个数
    127.0.0.1:6379> srem set1 1 2
    (integer) 0
    127.0.0.1:6379> 
    

    spop key count:随机弹出集合中count个元素,注意:count是可以省略的,如果省略则弹出1个。另外一旦弹出,原来的集合里面也就没有了。

    127.0.0.1:6379> smembers set1
    1) "3"  # 还有一个元素
    127.0.0.1:6379> sadd set1 1 2
    (integer) 2  # 添加两个进去
    127.0.0.1:6379> 
    127.0.0.1:6379> smembers set1
    1) "1"
    2) "2"
    3) "3"
    127.0.0.1:6379> spop set1 1
    1) "2"  # 弹出1个元素,返回弹出的元素
    127.0.0.1:6379> smembers set1
    1) "1"
    2) "3"
    127.0.0.1:6379> 
    

    srandmember key count:随机获取集合中count个元素,注意:count是可以省略的,如果省略则获取1个。可以看到类似spop,但是srandmember不会删除集合中的元素。

    127.0.0.1:6379> smembers set1
    1) "1"
    2) "3"
    127.0.0.1:6379> srandmember set1 1
    1) "1"
    127.0.0.1:6379> smembers set1
    1) "1"
    2) "3"
    

    smove key1 key2 value:将key1当中的value移动到key2当中,因此key1当中的元素会少一个,key2会多一个(前提是value在key2中不重复,否则key2还和原来保持一致)。

    127.0.0.1:6379> smembers set1
    1) "1"
    2) "3"
    127.0.0.1:6379> smembers set2
    1) "1"
    127.0.0.1:6379> smove set1 set2 3
    (integer) 1
    127.0.0.1:6379> smembers set1
    1) "1"
    127.0.0.1:6379> smembers set2
    1) "1"
    2) "3"
    127.0.0.1:6379> 
    

    sinter key1 key2:返回即在key1中,又在key2中的元素

    sunion key1 key2:返回在key1中,或者在key2中的元素

    sdiff key1 key2:返回在key1中,但不在key2中的元素

    127.0.0.1:6379> sinter set1 set2
    1) "2"
    2) "3"
    127.0.0.1:6379> sunion set1 set2
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    127.0.0.1:6379> sdiff set1 set2
    1) "1"
    127.0.0.1:6379> sdiff set2 set1
    1) "4"
    127.0.0.1:6379> 
    

    下面我们来看看如何使用Python操作Redis的集合

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. sadd key value1 value2·····
    client.sadd("s1", 1, 2, 3, 1)  
    
    # 2. smembers key
    print(client.smembers("s1"))  # {'2', '1', '3'}
    
    # 3. sismember key value
    print(client.sismember("s1", 1))  # True
    print(client.sismember("s1", 5))  # False
    
    # 4. scard key
    print(client.scard("s1"))  # 3
    
    # 5. srem key value1 value2······
    client.srem("s1", 1, 2)
    print(client.smembers("s1"))  # {'3'}
    
    # 6. spop key count
    print(client.smembers("s1"))  # {'3'}
    client.spop("s1", 1)
    print(client.smembers("s1"))  # set()
    
    # 7. srandmember key count
    client.sadd("s1", 1, 2, 3)
    print(client.smembers("s1"))  # {'2', '1', '3'}
    client.srandmember("s1", 2)
    print(client.smembers("s1"))  # {'2', '1', '3'}
    
    # 8. smove key1 key2 value
    client.sadd("s2", 1)
    client.smove("s1", "s2", 3)
    print(client.smembers("s2"))  # {'1', '3'}
    
    # 9. sinter key1 key2
    # 10. sunion key1 key2
    # 11. sdiff key1 key2
    client.sadd("s3", 1, 2, 3)
    client.sadd("s4", 2, 3, 4)
    print(client.sinter("s3", "s4"))  # {'2', '3'}
    print(client.sunion("s3", "s4"))  # {'2', '4', '1', '3'}
    print(client.sdiff("s3", "s4"))  # {'1'}
    

    那么Redis的集合底层是如何实现的呢?

    集合类型是由 intset (整数集合) 或 hashtable (普通哈希表) 组成的。当集合类型以 hashtable 存储时,哈希表的 key 为要插入的元素值,而哈希表的 value 则为 Null,如下图所示:

    当集合中所有的值都为整数时,Redis 会使用 intset 结构来存储,如下代码所示:

    127.0.0.1:6379> sadd s 1 2 3
    (integer) 3
    127.0.0.1:6379> object encoding s
    "intset"
    127.0.0.1:6379> 
    

    从上面代码可以看出,当所有元素都为整数时,集合会以 intset 结构进行(数据)存储。 当发生以下两种情况时,会导致集合类型使用 hashtable 而非 intset 存储: 1)当元素的个数超过一定数量时,默认是 512 个,该值可通过命令 set-max-intset-entries xxx 来配置。 2)当元素为非整数时,集合将会使用 hashtable 来存储,如下代码所示:

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    client.sadd("s1", *range(513))
    # 超过512个,使用哈希表存储
    print(client.object("encoding", "s1"))  # hashtable
    
    client.sadd("s2", *range(512))
    # 没超过512个,使用intset
    print(client.object("encoding", "s2"))  # intset
    
    client.sadd("s3", "hanser")
    # 不是整数,使用哈希表存储
    print(client.object("encoding", "s3"))  # hashtable
    

    源码解析

    集合源码在 t_set.c 文件中,核心源码如下:

    /* 
     * 添加元素到集合
     * 如果当前值已经存在,则返回 0 不作任何处理,否则就添加该元素,并返回 1。
     */
    int setTypeAdd(robj *subject, sds value) {
        long long llval;
        if (subject->encoding == OBJ_ENCODING_HT) { // 字典类型
            dict *ht = subject->ptr;
            dictEntry *de = dictAddRaw(ht,value,NULL);
            if (de) {
                // 把 value 作为字典到 key,将 Null 作为字典到 value,将元素存入到字典
                dictSetKey(ht,de,sdsdup(value));
                dictSetVal(ht,de,NULL);
                return 1;
            }
        } else if (subject->encoding == OBJ_ENCODING_INTSET) { // inset 数据类型
            if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
                uint8_t success = 0;
                subject->ptr = intsetAdd(subject->ptr,llval,&success);
                if (success) {
                    // 超过 inset 的最大存储数量,则使用字典类型存储
                    if (intsetLen(subject->ptr) > server.set_max_intset_entries)
                        setTypeConvert(subject,OBJ_ENCODING_HT);
                    return 1;
                }
            } else {
                // 转化为整数类型失败,使用字典类型存储
                setTypeConvert(subject,OBJ_ENCODING_HT);
    
                serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
                return 1;
            }
        } else {
            // 未知编码(类型)
            serverPanic("Unknown set encoding");
        }
        return 0;
    }
    

    以上这些代码验证了,我们上面所说的内容,当元素都为整数并且元素的个数没有到达设置的最大值时,键值的存储使用的是 intset 的数据结构,反之到元素超过了一定的范围,又或者是存储的元素为非整数时,集合会选择使用 hashtable 的数据结构进行存储。

    使用场景

    集合类型的经典使用场景如下:

    • 微博关注我的人和我关注的人都适合用集合存储,可以保证人员不会重复;
    • 中奖人信息也适合用集合类型存储,这样可以保证一个人不会重复中奖。

    Redis有序集合(Sorted Set)

    Redis的有序集合相比集合多了一个排序属性:score(分值),对于有序集合zset来说,每个存储元素相当于有两个值,一个是有序集合的元素值,一个是排序值。有序集合存储的元素值也是不重复的,但分数可以重复。

    当我们把学生的成绩存储在有序集合中,它的存储结构如下图所示:

    下面我们来看看它所支持的api操作

    zadd key score1 value1 score2 value2:设置score和value

    127.0.0.1:6379> zadd zset1 1 n1 2 n2 3 n2
    (integer) 2
    127.0.0.1:6379> 
    

    一个score对应一个value,value不会重复,因此即便我们这里添加了3个,但是后面两个的value都是n2,所以实际上只有两个元素,并且n2是以后一个score为准,因为相当于覆盖了。

    zscore key value:获取value对应的score

    127.0.0.1:6379> zscore zset1 n2
    "3"
    127.0.0.1:6379>
    

    zrange key start end:获取指定范围的value,递增排列,这里是基于索引获取

    127.0.0.1:6379> zadd zset2 1 n1 3 n3 2 n2 4 n4
    (integer) 4
    127.0.0.1:6379> zrange zset2 0 -1
    1) "n1"
    2) "n2"
    3) "n3"
    4) "n4"
    127.0.0.1:6379> zrange zset2 0 2
    1) "n1"
    2) "n2"
    3) "n3"
    127.0.0.1:6379> 
    

    如果结尾加上with scores参数,那么会和score一同返回,注意:score是在下面。我们看到这个zset有点像hash啊,value是hash的k,score是hash的v

    127.0.0.1:6379> zrange zset2 0 2 withscores
    1) "n1"
    2) "1"
    3) "n2"
    4) "2"
    5) "n3"
    6) "3"
    127.0.0.1:6379> 
    

    zrevrange key start end:获取所有的value,递减排列,同理也有withscores参数

    127.0.0.1:6379> zrevrange zset2 0 -1
    1) "n4"
    2) "n3"
    3) "n2"
    4) "n1"
    127.0.0.1:6379> 
    

    zrangebyscore key 开始score 结束score:获取>=开始score <=结束score的value,递增排列,同理也有withscores参数

    zrevrangebyscore key 结束score 开始score:获取>=开始score <=结束score的value,递减排列,同理也有withscores参数。注意:这里的开始和结束是相反的。

    127.0.0.1:6379> zadd zset3 1 n1 2 n2 3 n3 4 n4 5 n5 6 n6 7 n7
    (integer) 7
    127.0.0.1:6379> zrangebyscore zset3 3 6
    1) "n3"
    2) "n4"
    3) "n5"
    4) "n6"
    127.0.0.1:6379> zrevrangebyscore zset3 6 3
    1) "n6"
    2) "n5"
    3) "n4"
    4) "n3"
    127.0.0.1:6379> zrangebyscore zset3 (3 (6
    1) "n4"  # 如果在分数前面加上了(, 那么会不匹配边界,同理也支持withscores
    2) "n5"
    127.0.0.1:6379> zrevrangebyscore zset3 (6 (3
    1) "n5"
    2) "n4"
    127.0.0.1:6379> 
    

    zrem key value1 value2···:移除对应的value

    127.0.0.1:6379> zrem zset3 n1 n2 n3 n4
    (integer) 4
    127.0.0.1:6379> zrange zset3 0 -1
    1) "n5"
    2) "n6"
    3) "n7"
    

    zcard key:获取集合的元素个数

    127.0.0.1:6379> zcard zset3
    (integer) 3
    127.0.0.1:6379> 
    

    zcount key 开始分数区间 结束分数区间:获取集合指定分数区间内的元素个数

    127.0.0.1:6379> zcount zset3 6 8
    (integer) 2
    127.0.0.1:6379> zcount zset3 5 7
    (integer) 3
    127.0.0.1:6379> 
    

    下面看看如何使用Python操作Redis的有序集合

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 1. zadd key score1 value1 score2 value2
    # 这里使用字典的方式传递,因为value不重复,所以作为字典传递的话,value作为键、分数作为值
    client.zadd("zset1", {"n1": 1, "n2": 2, "n3": 3})
    
    # 2. zscore key value
    print(client.zscore("zset1", "n1"))  # 1.0
    
    # 3. zrange key start end
    print(client.zrange("zset1", 0, -1))  # ['n1', 'n2', 'n3']
    print(client.zrange("zset1", 0, -1, withscores=True))  # [('n1', 1.0), ('n2', 2.0), ('n3', 3.0)]
    
    # 4. zrevrange key start end
    print(client.zrevrange("zset1", 0, -1))  # ['n3', 'n2', 'n1']
    print(client.zrevrange("zset1", 0, -1, withscores=True))  # [('n3', 3.0), ('n2', 2.0), ('n1', 1.0)]
    
    # 5. zrangebyscore key 开始score 结束score
    # 6. zrevrangebyscore key 结束score 开始score
    print(client.zrangebyscore("zset1", 1, 3))  # ['n1', 'n2', 'n3']
    print(client.zrevrangebyscore("zset1", 3, 1))  # ['n3', 'n2', 'n1']
    
    # 7. zrem key value1 value2······
    client.zrem("zset1", "n1", "n2")
    print(client.zrange("zset1", 0, -1))  # ['n3']
    
    # 8. zcard key
    print(client.zcard("zset1"))  # 1
    
    # 9. zcount key 开始分数区间 结束分数区间
    client.zadd("zset2", {"n1": 1, "n2": 2, "n3": 3, "n4": 4, "n5": 5})
    print(client.zcount("zset2", 1, 4))  # 4
    

    下面看看Redis有序集合的底层实现

    有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。

    1)ziplist

    当数据比较少时,有序集合使用的是 ziplist 存储的,如下代码所示:

    127.0.0.1:6379> zadd my_zset 1 n1 2 n2
    (integer) 2
    127.0.0.1:6379> object encoding my_zset 
    "ziplist"
    127.0.0.1:6379> 
    

    从结果可以看出,有序集合把键值对存储在 ziplist 结构中了。 有序集合使用 ziplist 格式存储必须满足以下两个条件:

    • 有序集合保存的元素个数要小于等于 128 个;
    • 有序集合保存的所有元素成员的长度都必须小于等于 64 字节。

    如果不能满足以上两个条件中的任意一个,有序集合将会使用 skiplist 结构进行存储。 接下来我们来测试以下,当有序集合中某个元素长度大于 64 字节时会发生什么情况? 代码如下:

    import redis
    
    client = redis.Redis(host="47.94.174.89", decode_responses="utf-8")
    
    # 元素长度超过64
    client.zadd("my_zset2", {"a" * 65: 1})
    print(client.object("encoding", "my_zset2"))  # skiplist
    
    # 集合元素超过128个
    client.zadd("my_zset3", dict(zip(range(129), range(129))))
    print(client.object("encoding", "my_zset3"))  # skiplist
    

    通过以上代码可以看出,当有序集合保存的元素的长度大于 64 字节、或者元素个数超过128个时,有序集合就会从 ziplist 转换成为 skiplist。

    可以通过配置文件中的 zset-max-ziplist-entries(默认 128)和 zset-max-ziplist-value(默认 64)来设置有序集合使用 ziplist 存储的临界值。

    2)skiplist

    skiplist 数据编码底层是使用 zset 结构实现的,而 zset 结构中包含了一个字典和一个跳跃表,源码如下:

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    1. 跳跃表实现原理

    跳跃表的结构如下图所示:

    根据以上图片展示,当我们在跳跃表中查询值 32 时,执行流程如下:

    • 从最上层开始找,1 比 32 小,在当前层移动到下一个节点进行比较;
    • 7 比 32 小,当前层移动下一个节点比较,由于下一个节点指向 Null,所以以 7 为目标,移动到下一层继续向后比较;
    • 18 小于 32,继续向后移动查找,对比 77 大于 32,以 18 为目标,移动到下一层继续向后比较;
    • 对比 32 等于 32,值被顺利找到。

    从上面的流程可以看出,跳跃表会想从最上层开始找起,依次向后查找,如果本层的节点大于要找的值,或者本层的节点为 Null 时,以上一个节点为目标,往下移一层继续向后查找并循环此流程,直到找到该节点并返回,如果对比到最后一个元素仍未找到,则返回 Null。

    2. 为什么是跳跃表?而非红黑树?

    因为跳跃表的性能和红黑树基本相近,但却比红黑树更好实现,所以 Redis 的有序集合会选用跳跃表来实现存储。

    使用场景

    有序集合的经典使用场景如下:

    • 学生成绩排名
    • 粉丝列表,根据关注的先后时间排序

    总结

    关于有序集合,我们了解到了如下几点:

    • 有序集合具有唯一性和排序的功能,排序功能是借助分值字段 score 实现的,score 字段不仅可以实现排序功能,还可以实现数据的筛选与过滤的功能。
    • 有序集合是由 压缩列表 (ziplist) 或跳跃列表 (skiplist) 来存储的,当元素个数小于 128 个,并且所有元素的值都小于 64 字节时,有序集合会采取 ziplist 来存储,反之则会用 skiplist 来存储。
    • skiplist 是从上往下、从前往后进行元素查找的,相比于传统的普通列表,可能会快很多,因为普通列表只能从前往后依次查找。
  • 相关阅读:
    如何将一棵树转化成二叉树
    雪碧图的使用
    CSS简介,引入方式,文字和文本样式设置
    表格Table和表单元素
    html 中< col>标签和< colgroup>标签的区别
    Emmet的HTML语法(敲代码的快捷方式)
    抖音风格字体效果
    几种有效减小电脑系统盘使用量的方法
    ubuntu 机器名称修改方法
    Ubuntu 为基于X应用程序增加启动项的正确做法
  • 原文地址:https://www.cnblogs.com/traditional/p/13279038.html
Copyright © 2020-2023  润新知