Redis 主要的五种数据结构
2019-01-06 16:27:22 阿炯

Redis 中有5种数据结构,分别是字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set),因为使用 Redis 场景的开发中肯定是无法避开这些基础结构的,所以熟练掌握它们也就成了一项必不可少的能力。本文章精要地介绍了 Redis 的这几种数据结构,主要覆盖了它们各自的定义、基本用法与相关要点。

Redis中的redisObject对象

Redis是使用C编写的,内部实现了一个struct结构体redisObject对象,通过结构体来模仿面向对象编程的“多态”,作为一个底层的数据支持。下图中的REDIS_STRING/REDIS_LIST/REDIS_ZSET/REDIS_HASH/REDIS_SET针对的是redisObject中的type,后面指向的REDIS_ENCODING_LINKEDLIST等针对的是encoding字段。

Redis的底层数据结构有以下几种:
简单动态字符串sds(Simple Dynamic String)
双端链表(LinkedList)
字典(Map)
跳跃表(SkipList)


-------------------------------
字符串类型


字符串是 Redis 中的最基础的数据结构,我们保存到 Redis 中的 key,也就是键,就是字符串结构的。除此之外,Redis 中其它数据结构也是在字符串的基础上设计的,可见字符串结构对于 Redis 是多么重要。

Redis 中的字符串结构可以保存多种数据类型,如:简单的字符串、JSON、XML、二进制等,但有一点要特别注意:在 Redis 中字符串类型的值最大只能保存 512 MB。

命令

下面通过命令了解一下对字符串类型的操作:

1.设置值

set key value [EX seconds] [PX milliseconds] [NX|XX]

set 命令有几个非必须的选项,下面我们看一下它们的具体说明:
    EX seconds:为键设置秒级过期时间
    PX milliseconds:为键设置毫秒级过期时间
    NX:键必须不存在,才可以设置成功,用于添加
    XX:键必须存在,才可以设置成功,用于更新

set 命令带上可选参数 NX 和 XX 在实际开发中的作用与 setnx 和 setxx 命令相同。我们知道 setnx 命令只有当 key 不存在的时候才能设置成功,换句话说,也就是同一个 key 在执行 setnx 命令时,只能成功一次,并且由于 Redis 的单线程命令处理机制,即使多个客户端同时执行 setnx 命令,也只有一个客户端执行成功。所以,基于 setnx 这种特性,setnx 命令可以作为分布式锁的一种解决方案。

而 setxx 命令则可以在安全性比较高的场景中使用,因为 set 命令执行时,会执行覆盖的操作,而 setxx 在更新 key 时可以确保该 key 已经存在了,所以为了保证 key 中数据类型的正确性,可以使用 setxx 命令。

2.获取值
get key

3.批量设置值
mset key value

4.批量获取值
mget key

如果有些键不存在,那么它的值将为 nil,也就是空,并且返回结果的顺序与传入时相同。

5.计数
incr key

incr 命令用于对值做自增操作,返回的结果分为 3 种情况:
    如果值不是整数,那么返回的一定是错误
    如果值是整数,那么返回自增后的结果
    如果键不存在,那么就会创建此键,然后按照值为 0 自增, 就是返回 1

除了有 incr 自增命令外,Redis 中还提供了其它对数字处理的命令。例如:
decr key 自减
incrby key increment 自增指定数字
decrby key decrement 自减指定数字
incrbyfloat key increment 自增浮点数

6.追加值
append key value

append 命令可以向字符串尾部追加值。

7.字符串长度
strlen key

注意:每个中文占用 3 个字节。

8.设置并返回原值
getset key value

9.设置指定位置的字符
setrange key offeset value

10.获取部分字符串
getrange key start end

时间复杂度

在 Redis 中执行任何命令时,都有相应的时间复杂度,复杂度越高也就越费时间,所以在执行 Redis 中的命令时,如果要执行的命令复杂度越高,就越要慎重。下面是字符串命令时间复杂度类型表:

命令

时间复杂度

set key value

O(1)

get key

O(1)

del key

O(k) k是键的个数

mset key value

O(k) k是键的个数

mget key

O(k) k是键的个数

incr key

O(1)

decr key

O(1)

incrby key increment

O(1)

decrby keky increment

O(1)

incrbyfloat key iincrement

O(1)

append key value

O(1)

strlen key

O(1)

setrange key offset value

O(1)

getrange key start end

O(n) n是字符串长度



内部编码

在 Redis 中字符串类型的内部编码有 3 种:
int:8 个字节的长整型
embstr:小于等于 39 个字节的字符串
raw:大于 39 个字节的字符串

-------------------------------
哈希类型


大部分语言基本都提供了哈希类型,如 Java 语言中的 Map 类型及 Perl 语言中的Hash类型等等。虽然语言不同,但它们基本使用都是一样的,也就是都是键值对结构的。例如:
value={{field1, value1}}

通过下图可以直观感受一下字符串类型和哈希类型的区别:


Redis 中哈希类型都是键值对结构的,所以要特别注意这里的 value 并不是指 Redis 中 key 的 value,而是哈希类型中的 field 所对应的 value。

命令

下面我们还是和介绍字符串类型一样,了解一下 Redis 中哈希类型的相关命令。

1.设置值
hset key field value

hset 命令也是有返回值的。如果 hset 命令设置成功,则返回 1,否则返回 0。除此之外 Redis 也为哈希类型提供了 hsetnx 命令。在前文对字符串的介绍中,我们知道 nx 命令只有当 key 不存在的时候,才能设置成功,同样的,hsetnx 命令在 field 不存在的时候,才能设置成功。

2.获取值
hget key field

我们看 hget 命令和 get 有很大的不同,get 命令在获取的时候,只要写一个名字就可以了,而 hget 命令则要写两个名字,第一个名字是 key,第二个名字是 field。当然 key 或者 field 不存在时,返回的结果都是 nil。

3.删除 field
hdel key field [field ...]

hdel 命令删除的时候,也会有返回值,并且这个返回就是成功删除 field 的个数。当 field 不存在时,并不会报错,而是直接返回 0。

4.计算 field 个数
hlen key

hlen 命令返回的就是当前 key 中 field 的个数,如果 key 不存在,则返回 0。

5.批量设置或获取 field-value
hmget key field [field ...]
hmset key field value [field value ...]

hmset 命令和 hmget 命令分别是批量设置和获取值的,hmset 命令没有什么要注意的,但 hmget 命令要特别注意,当我们获取一个不存在的 key 或者不存在的 field 时,Redis 并不会报错,而是返回 nil。并且有几个 field 不存在,则 Redis 返回几个 nil。

6.判断 field 是否存在
hexists key field

当执行 hexists 命令时,如果当前 key 包括 field,则返回 1,否则返回 0。

7.获取所有 field
hkeys key

8.获取所有 value
hvals key

9.获取所有的 field-value
hgetall key

hgetall 命令会返回当前 key 中的所有 field-value,并按照顺序依次返回。

10.计数
hincrby key field increment
hincrbyfloat key field increment

hincrby 命令和 incrby 命令的使用功能基本一样,都是对值进行增量操作的,唯一不同的就是 incrby 命令的作用域是 key,而 hincrby 命令的作用域则是 field。

11.计算 value 的字符串长度
hstrlen key field

hstrlen 命令返回的是当前 key 中 field 中字符串的长度,如果当前 key 中没有 field 则返回 0。

时间复杂度

命令

时间复杂度

hset key field value

O(1)

hget key field

O(1)

hdel key field [field ...]

O(k) ,k是field个数

hlen key

O(1)

hgetall key

O(n) ,n是field总数

hmget key field [field ...]

O(k) ,k是field个数

hmset key field value [field value ...]

O(k) ,k是field个数

hexists key field

O(1)

hkeys key

O(n) ,n是field总数

hvals key

O(n) ,n是field总数

hsetnx key field value

O(1)

hincrby key field increment

O(1)

hincrbyfloat key field increment

O(1)

hstrlen key field

O(1)



内部编码

Redis 哈希类型的内部编码有两种,它们分别是:

ziplist(压缩列表):当哈希类型中元素个数小于 hash-max-ziplist-entries 配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使用 ziplist 作为哈希的内部实现。

hashtable(哈希表):当上述条件不满足时,Redis 则会采用 hashtable 作为哈希的内部实现。

下面我们通过以下命令来演示一下 ziplist 和 hashtable 这两种内部编码。

当 field 个数比较少并且 value 也不是很大时候 Redis 哈希类型的内部编码为 ziplist:

当 value 中的字节数大于 64 字节时(可以通过 hash-max-ziplist-value 设置),内部编码会由 ziplist 变成 hashtable。

当 field 个数超过 512(可以通过 hash-max-ziplist-entries 参数设置),内部编码也会由 ziplist 变成 hashtable。

由于直接手动创建 512 个 field 不方便,为了更好的验证该功能,我将用程序的方式,动态创建 512 个 field 来验证此功能,下面为具体的代码:

use v5.20;
use Redis;
use Data::Dumper;

my $r = Redis->new(server => '192.168.8.1:6379',debug=>0);
say $r->object('encoding','h');

hashtable

-------------------------------
列表类型


Redis 中列表类型可以简单地理解为存储多个有序字符串的一种新类型,这种类型除了字符串类型中已有的功能外,还提供了其它功能,如可以对列表的两端插入和弹出元素(在列表中的字符串都可以称之为元素),除此之外还可以获取指定的元素列表,并且还可以通过索引下标获取指定元素等等。下面我们通过下图来看一下 Redis 中列表类型的插入和弹出操作:


下面我们看一下 Redis 中列表类型的获取与删除操作:


Redis 列表类型的特点如下:
列表中所有的元素都是有序的,所以它们是可以通过索引获取的,也就是上图中的 lindex 命令。并且在 Redis 中列表类型的索引是从 0 开始的。

列表中的元素是可以重复的,也就是说在 Redis 列表类型中,可以保存同名元素,如下图所示:


命令

下面我们还是和学习其它数据类型一样,我们还是先学习一下 Redis 列表类型的命令。

1.添加操作

从右边插入元素
rpush key value [value ...]

我们看 rpush 命令在插入时,是有返回值的,返回值的数量就是当前列表中所有元素的个数。我们也可以用下面的命令从左到右获取当前列表中的所有的元素,也就是如上所示中那样。

lrange 0 -1

从左边插入元素
lpush key value [value ...]

lpush 命令的返回值及用法和 rpush 命令一样。通过上面的事例证明了我们前面说的,rpush 命令和 lpush 命令的返回值并不是当前插入元素的个数,而是当前 key 中全部元素的个数,因为当前 key 中已经有了 3 个元素,所以我们在执行插入命令时,返回的就是 6 而不是 3。

向某个元素前或者后插入元素
linsert key BEFORE|AFTER pivot value

linsert 命令在执行的时候首先会从当前列表中查找到 pivot 元素,其次再将这个新元素插入到 pivot 元素的前面或者后面。linsert 命令在执行成功后也是会有返回值的,返回的结果就是当前列表中元素的个数。

2.查找

获取指定范围内的元素列表
lrange key start stop

lrange 命令会获取列表中指定索引范围的所有元素。

通过索引获取列表主要有两个特点:
索引下标从左到右分别是 0 到 N-1,从右到左是 -1 到 -N。
lrange 命令中的 stop 参数在执行时会包括当前元素,并不是所有的语言都是这样的。

获取列表中指定索引下标的元素
lindex key index

获取列表长度
llen key

3.删除

从列表左侧弹出元素
lpop key

lpop 命令执行成功后会返回当前被删除的元素名称。

从列表右侧弹出元素
rpop 命令和 lpop 命令的使用方式一样。

删除指定元素
lrem key count value

lrem 命令会将列表中等于 value 的元素删除掉,并且会根据 count 参数来决定删除 value 的元素个数。

下面我们看一下 count 参数的使用说明:
count > 0:表示从左到右,最多删除 count 个元素。lrem 命令也是有返回值的,也就是当前成功删除元素的个数。
count < 0:从右到左,最多删除 count 个元素。
count = 0:删除所有元素。

按照索引范围修剪列表
ltrim key start stop

ltrim 命令会直接保留 start 索引到 stop 索引的之间的元素,并包括当前元素,而之外的元素则都会删除掉,所以该命令也叫修剪列表。有一点要注意,ltrim 命令不会返回当前的列表中元素的个数,而是返回改命令是否成功的状态。

4.修改

修改指定索引下标的元素
lset key index value

5.阻塞操作

blpop key [key ...] timeout
brpop key [key ...] timeout

blpop 和 brpop 命令是 lpop 和 rpop 命令的阻塞版本,它们除了弹出方向不同以外,使用方法基本相同。

    key [key ...]:可以指定多个列表的键。
    timeout:阻塞时间(单位:秒)

下面我们看一下该命令的详细使用。

列表为空:如果 timeout=3,则表示客户端等待 3 秒后才能返回结果,如果 timeout=0,则表示客户端会一直等待,也就是会阻塞。
>lrange listkey 0 -1
(empty list or set)
>brpop listkey 3
(nil)
(3.03s)
>brpop listkey 0
1) "listkey"
2) "1"
(72.66s)

由于期间向列表中插入了元素,否则上述命令将一直阻塞下去。

列表不为空:如果 timeout=0,并且列表不为空时,则 blpop 和 brpop 命令会立即返回结果,不会阻塞。
>lrange listkey 0 -1
1) "1"
>brpop listkey 0
1) "listkey"
2) "1"

下面我们看一下 blpop 和 brpop 命令的注意事项。

如果使用 blpop 和 brpop 命令指定多个键时,blpop 和 brpop 命令会从左到右遍历键,并且一旦有一个键能返回元素,则客户端会立即返回。
>brpop listkey1 listkey2 listkey3 0
1) "listkey2"
2) "1"
(11.90s)

当列表为空时,上述命令会阻塞,如果向上述中的任何一个键中插入元素,则上述命令会直接返回该键的元素。

如果多个客户端都对同一个键执行 blpop 或者 brpop 命令,则最先执行该命令的客户端会获取到该键的元素。同时启动了 3 个客户端,因为当前列表为空,所以上述命令执行后会阻塞。如果此时我向该列表中插入元素,则只有第一个客户端会有返回结果,因为第一个客户端是第一个执行上述命令的。

时间复杂度

操作类型

命令

时间复杂度

添加

rpush key value [value ...]

O(k),k是元素的个数

添加

lpush key value [value ...]

O(k),k是元素的个数

添加

linsert key BEFORE/AFTER pivot value

O(n),n是pivot距离列表头或者尾的距离

查找

lrange key start stop

O(s + n),s是start偏移量,n是start到stop的范围

查找

lindex key index

O(n),n是索引的偏移量

查找

llen key

O(1)

删除

lpop key

O(1)

删除

rpop key

O(1)

删除

lrem key count value

O(n),n是列表长度

删除

ltrim key start stop

O(n),n是要裁剪的元素个数

修改

lset key index value

O(n),n是索引的偏移量

阻塞操作

blpop/brpop key [key ...] timeout

O(1)



内部编码

列表中的内部编码有两种,它们分别是:
    ziplist(压缩列表):当列表中元素个数小于 512(默认)个,并且列表中每个元素的值都小于 64(默认)个字节时,Redis 会选择用 ziplist 来作为列表的内部实现以减少内存的使用。当然上述默认值也可以通过相关参数修改:list-max-ziplist-entried(元素个数)、list-max-ziplist-value(元素值)。
    linkedlist(链表):当列表类型无法满足 ziplist 条件时,Redis 会选择用 linkedlist 作为列表的内部实现。

-------------------------------
集合类型


Redis 中的集合类型,也就是 set。在 Redis 中 set 也是可以保存多个字符串的,经常有人会分不清 list 与 set,下面我们重点介绍一下它们之间的不同:
    set 中的元素是不可以重复的,而 list 是可以保存重复元素的。
    set 中的元素是无序的,而 list 中的元素是有序的。
    set 中的元素不能通过索引下标获取元素,而 list 中的元素则可以通过索引下标获取元素。
    除此之外 set 还支持更高级的功能,例如多个 set 取交集、并集、差集等。

命令

下面我们介绍一下 set 中的相关命令。

1.集合内操作

添加元素
sadd key member [member ...]

sadd 命令也是有返回值的,它的返回值就是当前执行 sadd 命令成功添加元素的个数,因为 set 中不能保存重复元素,所以在执行 sadd setkey c d 命令时,返回的是 1,而不是 2。因为元素 c 已经成功保存到 set 中,不能再保存了,只能将 d 保存到 set 中。

删除元素
srem key member [member ...]

srem 命令和 sadd 命令一样也是有返回值的,返回值就是当前删除元素的个数。

计算元素个数
scard key

scard 命令的时间复杂度为O(1),scard 命令不会遍历 set 中的所有元素,而是直接使用 Redis 中的内部变量。

判读元素是否在集合中
sismember key member

sismember 命令也有返回值,如果返回值为1则表示当前元素在当前 set 中,如果返回 0 则表示当前元素不在 set 中。

随机从 set 中返回指定个数元素
srandmember key [count]

srandmember 命令中有一个可选参数 count,count 参数指的是返回元素的个数,如果当前 set 中的元素个数小于 count,则 srandmember 命令返回当前 set 中的所有元素,如果 count 参数等于 0,则不返回任何数据,如果 count 参数小于 0,则随机返回当前 count 个数的元素。

从集合中随机弹出元素
spop key [count]

spop 命令也是随机从 set 中弹出元素,并且也支持 count 可选参数,但有一点和 srandmember 命令不同。spop 命令在随机弹出元素之后,会将弹出的元素从 set 中删除。

获取所有元素
smembers key

smembers 命令虽然能获取当前 set 中所有的元素,但返回元素的顺序与 sadd 添加元素的顺序不一定相同,这也就是前面提到过的保存在 set 中的元素是无序的。

2.集合间操作

集合的交集
sinter key [key ...]

集合的并集
sunion key [key ...]

集合的差集
sdiff key [key ...]

将集合的交集、并集、差集的结果保存
sinterstore destination key [key ...]
sunionstore destination key [key ...]
sdiffstore destination key [key ...]

为什么 Redis 要提供 sinterstore、sunionstore、sdiffstore 命令来将集合的交集、并集、差集的结果保存起来呢?这是因为 Redis 在进行上述比较时,会比较耗费时间,所以为了提高性能可以将交集、并集、差集的结果提前保存起来,这样在需要使用时,可以直接通过 smembers 命令获取。

时间复杂度

下面我们看一下 set 中相关命令的时间复杂度。

命令

时间复杂度

sadd key member [member ...]

O(k),k是元素的个数

srem key member [member ...]

O(k),k是元素的个数

scard key

O(1)

sismember key member

O(1)

srandmember key [count]

O(count)

spop key [count]

O(1)

smembers key

O(n),n是元素的总数

sinter key [key ...]

O(m * k),k是多个集合中元素最少的个数,m是键个数

sunion key [key ...]

O(k),k是多个元素个数和

sdiff key [key ...]

O(k),k是多个元素个数和

sinterstore destination key [key ...]

O(m * k),k是多个集合中元素最少的个数,m是键个数

sunionstore destination key [key ...]

O(k),k是多个元素个数和

sdiffstore destination key [key ...]

O(k),k是多个元素个数和



内部编码

    intset(整数集合):当集合中的元素都是整数,并且集合中的元素个数小于 512 个时,Redis 会选用 intset 作为底层内部实现。
    hashtable(哈希表):当上述条件不满足时,Redis 会采用 hashtable 作为底层实现。

备注:我们可以通过 set-max-intset-entries 参数来设置上述中的默认参数。下面我们看一下具体的事例,来验证我们上面提到的内部编码。

当元素个数较少并且都是整数时,内部编码为 intset。

当元素不全是整数时,内部编码为 hashtable。

当元素个数超过 512 个时,内部编码为 hashtable。


-------------------------------
有序集合类型


看名字我们就知道,有序集合也是一种集合,并且这个集合还是有序的。列表也是有序的,那它和有序集合又有什么不同呢?有序集合的有序和列表的有序是不同的。列表中的有序指的的是插入元素的顺序和查询元素的顺序相同,而有序集合中的有序指的是它会为每个元素设置一个分数(score),而查询时可以通过分数计算元素的排名,然后再返回结果。因为有序集合也是集合类型,所以有序集合中也是不插入重复元素的,但在有序集合中分数则是可以重复,那如果在有序集合中有多个元素的分数是相同的,这些重复元素的排名是怎么计算的呢?后边我们再做详细说明。

下面先看一下列表、集合、有序集合三种数据类型之间的区别:

数据结构

是否允许重复元素

是否有序

有序实现方式

应用场景

列表

索引下标

时间轴、消息队列

集合

标签、社交

有序集合

分数

排行榜、社交



命令

1.集合内操作

添加元素
zadd key [NX|XX] [CH] [INCR] score member [score member ...]

zadd 命令也是有返回值的,返回值就是当前 zadd 命令成功添加元素的个数。zadd 命令有很多选填参数:

    nx: 元素必须不存在时,才可以设置成功。
    xx: 元素必须存在时,才可以设置成功。
    ch: 返回此命令执行完成后,有序集合元素和分数发生变化的个数
    incr: 对 score 做增加。

备注:由于有序集合相比集合提供了排序字段,正是因为如此也付出了相应的代价,sadd 的时间复杂度为 O(1),而 zadd 的时间复杂度为O(log(n))。

计算成员个数
zcard key

计算某个成员的分数
zscore key member

在使用 zscore 命令时,如果 key 不存在,或者元素不存在时,该命令返回的都是(nil)。

计算成员的排名
zrank key member
zrevrank key member

zrank 命令是从分数低到高排名,而 zrevrank 命令则恰恰相反,从高到低排名。有一点要特别注意, zrank 和 zrevrank 命令与 zscore 是命令不同的,前者通过分数计算出最后的排名,而后者则是直接返回当前元素的分数。排名是从0开始计算。

删除元素
zrem key member [member ...]

返回的结果为成功删除元素的个数,因为 zrem 命令是支持批量删除的。

增加元素分数
zincrby key increment member

虽然 zincrby 命令是增加元素分数的,但我们也可以指定负数,这样当前元素的分数,则会相减。

返回指定排名范围的元素
zrange key start stop [WITHSCORES]
zrevrange key start stop [WITHSCORES]

zrange 命令是通过分数从低到高返回数据,而 zrevrange 命令是通过分数从高到低返回数据。如果执行命令时添加了 WITHSCORES 可选参数,则返回数据时会返回当前元素的分数。

返回指定分数范围的元素
zrangebyscore key min max [WITHSCORES] [LIMIT offset count]
zrevrangebyscore key max min [WITHSCORES] [LIMIT offset count]

min 和 max 参数还支持开区间(小括号)和闭区间(中括号),同时我们还可以用 -inf 和 +inf 参数代表无限小和无限大,这样就可以取到所有的记录元素。

返回指定分数范围元素个数
zcount key min max

删除指定排名内的升序元素
zremrangebyrank key start stop

删除指定分数范围元素
zremrangebyscore key min max

2.集合间操作

交集
zinterstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

zinterstore 命令参数比较多:
    destination:将交集的计算结果,保存到这个键中。
    numkeys:需要做交集计算键的个数。
    key [key ...]:需要做交集计算的键。
    WEIGHTS weight:每个键的权重,在做交集计算时,每个键中的分数值都会乘以这个权重,默认每个键的权重为 1。
    AGGREGATE SUM|MIN|MAX:计算成员交集后,分值可以按照 sum(和)、min(最小值)、max(最大值)做汇总,默认值为  sum。

下面我们将权重设置为 0.5,这样当计算交集后,有序集合中的元素分数将都会减半,并且使用 max 参数汇总。
>zinterstore dest1 2 zs1 zs2 weights 1 0.5 aggregate max
>zrange dest1 0 -1 withscores

并集
zunionstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

zunionstore 命令的相关参数和 zinterstore 命令相同。

时间复杂度

命令

时间复杂度

zadd key [NX/XX] [CH] [INCR] score member [score member ...]

O(k*log(n)),k是添加元素的个数,n是当前有序集合元素个数

zcard key

O(1)

zscore key member

O(1)

zrank key member

O(log(n)),n是当前有序集合元素个数

zrevrank key member

O(log(n)),n是当前有序集合元素个数

zrem key member [member ...]

O(k*log(n)),k是删除元素的个数,n是当前有序集合元素个数

zincrby key increment member

O(log(n)),n是当前有序集合元素个数

zrange key start stop [WITHSCORES]

O(log(n) + k),k是要获取的元素个数,n是当前有序集合个数

zrevrange key start stop [WITHSCORES]

O(log(n) + k),k是要获取的元素个数,n是当前有序集合个数

zrangebyscore key min max [WITHSCORES] [LIMIT offset count]

O(log(n) + k),k是要获取的元素个数,n是当前有序集合个数

zrevrangebyscore key max min [WITHSCORES] [LIMIT offset count]

O(log(n) + k),k是要获取的元素个数,n是当前有序集合个数

zcount key min max

O(log(n)),n是当前有序集合元素个数

zremrangebyrank key start stop

O(log(n) + k),k是要删除的元素个数,n是当前有序集合个数

zremrangebyscore key min max

O(log(n) + k),k是要删除的元素个数,n是当前有序集合个数

zinterstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM/MIN/MAX]

O(n k) + O(m log(m)),n是元素元素最小的有序集合元素个数,k是有序集合个数,m是结果集中元素个数

zunionstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM/MIN/MAX]

O(n) + O(m * log(m)),n是所有有序集合元素个数和,m是结果集中元素个数。



内部编码

有序集合类型的内部编码有两种,它们分别是:
    ziplist(压缩列表):当有序集合的元素个数小于 128 个(默认设置),同时每个元素的值都小于 64 字节(默认设置),Redis 会采用 ziplist 作为有序集合的内部实现。
    skiplist(跳跃表):当上述条件不满足时,Redis 会采用 skiplist 作为内部编码。

备注:上述中的默认值,也可以通过以下参数设置:zset-max-ziplist-entries 和 zset-max-ziplist-value。

下面我们用以下示例来验证上述结论:
当元素个数比较少,并且每个元素也比较小时,内部编码为 ziplist。

当元素个数超过 128 时,内部编码为 skiplist。


参考来源:
Redis 避不开的五种数据结构

-------------------------------
上面讲解了理论上的知识点,本节分享自华为云社区《你真的懂 Redis 的 5 种基本数据结构吗》,这些知识点或许你还需要看看,理论与实践相结合;作者:李子捌。

一、简介

Redis 中所有的的数据结构都是通过一个唯一的字符串 key 来获取相应的 value 数据。Redis 有 5 种基础数据结构,分别是:

string(字符串)
list(列表)
hash(字典)
set(集合)
zset(有序集合)

其中 list、set、hash、zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:

create if not exists:容器不存在则创建
drop if no elements:如果容器中没有元素,则立即删除容器,释放内存

本文将详细讲述的是 Redis 的 5 种基础数据结构。

二、string(字符串)

1、string (字符串) 相关介绍


1.1 string (字符串) 的内部结构

string (字符串) 是 Redis 最简单也是使用最广泛的数据结构,它的内部是一个字符数组。Redis 中 string (字符串) 是动态字符串,允许修改;它在结构上的实现类似于 Java 中的 ArrayList(默认构造一个大小为 10 的初始数组),这是冗余分配内存的思想,也称为预分配;这种思想可以减少扩容带来的性能消耗。


1.2 string (字符串) 的扩容

当 string (字符串) 的大小达到扩容阈值时,将会对 string (字符串) 进行扩容,string (字符串) 的扩容主要有以下几个点:
长度小于 1MB,扩容后为原先的两倍;length = length * 2
长度大于 1MB,扩容后增加 1MB; length = length + 1MB
字符串的长度最大值为 512MB

2、string (字符串) 的指令

2.1 单个键值对增删改查操作

set -> key 不存在则新增,存在则修改
set key value

get -> 查询,返回对应 key 的 value,不存在返回 (nil)
get key

del -> 删除指定的 key (key 可以是多个)
del key [key …]

示例:
1127.0.0.1:6379> set name liziba
2OK
3127.0.0.1:6379> get name
4"liziba"
5127.0.0.1:6379> set name liziba001
6OK
7127.0.0.1:6379> get name
8"liziba001"
9127.0.0.1:6379> del name
10(integer) 1
11127.0.0.1:6379> get name
12(nil)

2.2 批量键值对

批量键值读取和写入最大的优势在于节省网络传输开销

mset -> 批量插入
mset key value [key value …]

mget -> 批量获取
mget key [key …]

示例:
1127.0.0.1:6379> mset name1 liziba1 name2 liziba2 name3 liziba3
2OK
3127.0.0.1:6379> mget name1 name2 name3
41) "liziba1"
52) "liziba2"
63) "liziba3"

2.3 过期 set 命令

过期 set 是通过设置一个缓存 key 的过期时间,使得缓存到期后自动删除从而失效的机制。

方式一:
expire key seconds

示例:
1127.0.0.1:6379> set name liziba
2OK
3127.0.0.1:6379> get name
4"liziba"
5127.0.0.1:6379> expire name 10   # 10s 后get name 返回 nil
6(integer) 1
7127.0.0.1:6379> get name
8(nil)

方式二:
setex key seconds value

示例:
1127.0.0.1:6379> setex name 10 liziba    # 10s 后get name 返回 nil
2OK
3127.0.0.1:6379> get name
4(nil)

2.4 不存在创建存在不更新

上面的 set 操作不存在创建,存在则更新;此时如果需要存在不更新的场景,那么可以使用如下这个指令

setnx -> 不存在创建存在不更新
setnx key value

示例:
1127.0.0.1:6379> get name
2(nil)
3127.0.0.1:6379> setnx name liziba        
4(integer) 1
5127.0.0.1:6379> get name
6"liziba"
7127.0.0.1:6379> setnx name liziba_98        # 已经存在再次设值,失败
8(integer) 0
9127.0.0.1:6379> get name
10"liziba"

2.5 计数

string (字符串) 也可以用来计数,前提是 value 是一个整数,那么可以对它进行自增的操作。自增的范围必须在 signed long 的区间访问内,[-9223372036854775808,9223372036854775808]

incr -> 自增 1
incr key

示例:
1127.0.0.1:6379> set fans 1000
2OK
3127.0.0.1:6379> incr fans # 自增1
4(integer) 1001

incrby -> 自定义累加值
incrby key increment

1127.0.0.1:6379> set fans 1000
2OK
3127.0.0.1:6379> incr fans
4(integer) 1001
5127.0.0.1:6379> incrby fans 999
6(integer) 2000

测试 value 为整数的自增区间

最大值:
1127.0.0.1:6379> set fans 9223372036854775808
2OK
3127.0.0.1:6379> incr fans
4(error) ERR value is not an integer or out of range

最小值:
1127.0.0.1:6379> set money -9223372036854775808
2OK
3127.0.0.1:6379> incrby money -1
4(error) ERR increment or decrement would overflow

三、list(列表)​

1、list (列表) 相关介绍


1.1 list (列表) 的内部结构

Redis 的列表相当于 Java 语言中的 LinkedList,它是一个双向链表数据结构(但是这个结构设计比较巧妙,后面会介绍),支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度 O (1),查询慢,时间复杂度 O (n)。

1.2 list (列表) 的使用场景

根据 Redis 双向列表的特性,因此其也被用于异步队列的使用。实际开发中将需要延后处理的任务结构体序列化成字符串,放入 Redis 的队列中,另一个线程从这个列表中获取数据进行后续处理。其流程类似如下的图:


2、list (列表) 的指令


2.1 右进左出 — 队列

队列在结构上是先进先出(FIFO)的数据结构(比如排队购票的顺序),常用于消息队列类似的功能,例如消息排队、异步处理等场景。通过它可以确保元素的访问顺序。
lpush -> 从左边边添加元素
lpush key value [value …]

rpush -> 从右边添加元素
rpush key value [value …]

llen -> 获取列表的长度
llen key

lpop -> 从左边弹出元素
lpop key

1127.0.0.1:6379> rpush code java c python    # 向列表中添加元素
2(integer) 3
3127.0.0.1:6379> llen code    # 获取列表长度
4(integer) 3
5127.0.0.1:6379> lpop code # 弹出最先添加的元素
6"java"
7127.0.0.1:6379> lpop code    
8"c"
9127.0.0.1:6379> lpop code
10"python"
11127.0.0.1:6379> llen code
12(integer) 0
13127.0.0.1:6379> lpop code
14(nil)

2.2 右进右出 —— 栈

栈在结构上是先进后出(FILO)的数据结构(比如弹夹压入子弹,子弹被射击出去的顺序就是栈),这种数据结构一般用来逆序输出。

lpush -> 从左边边添加元素
lpush key value [value …]

rpush -> 从右边添加元素
rpush key value [value …]

rpop -> 从右边弹出元素
rpop code

1127.0.0.1:6379> rpush code java c python
2(integer) 3
3127.0.0.1:6379> rpop code            # 弹出最后添加的元素
4"python"
5127.0.0.1:6379> rpop code
6"c"
7127.0.0.1:6379> rpop code
8"java"
9127.0.0.1:6379> rpop code
10(nil)

2.3 慢操作

列表 (list) 是个链表数据结构,它的遍历是慢操作,所以涉及到遍历的性能将会遍历区间 range 的增大而增大。注意 list 的索引运行为负数,-1 代表倒数第一个,-2 代表倒数第二个,其它同理。

lindex -> 遍历获取列表指定索引处的值
lindex key ind

lrange -> 获取从索引 start 到 stop 处的全部值
lrange key start stop

ltrim -> 截取索引 start 到 stop 处的全部值,其它将会被删除
ltrim key start stop

1127.0.0.1:6379> rpush code java c python
2(integer) 3
3127.0.0.1:6379> lindex code 0        # 获取索引为0的数据
4"java"
5127.0.0.1:6379> lindex code 1   # 获取索引为1的数据
6"c"
7127.0.0.1:6379> lindex code 2        # 获取索引为2的数据
8"python"
9127.0.0.1:6379> lrange code 0 -1    # 获取全部 0 到倒数第一个数据  == 获取全部数据
101) "java"
112) "c"
123) "python"
13127.0.0.1:6379> ltrim code 0 -1    # 截取并保理 0 到 -1 的数据 == 保理全部
14OK
15127.0.0.1:6379> lrange code 0 -1
161) "java"
172) "c"
183) "python"
19127.0.0.1:6379> ltrim code 1 -1    # 截取并保理 1 到 -1 的数据 == 移除了索引为0的数据 java
20OK
21127.0.0.1:6379> lrange code 0 -1
221) "c"
232) "python"

3、list (列表) 深入理解

Redis 底层存储 list (列表) 不是一个简单的 LinkedList,而是 quicklist ——“快速列表”。关于 quicklist 是什么,下面会简单介绍,具体源码我也还在学习中,后面大家一起探讨。
quicklist 是多个 ziplist (压缩列表) 组成的双向列表;而这个 ziplist (压缩列表) 又是什么呢?ziplist 指的是一块连续的内存存储空间,Redis 底层对于 list (列表) 的存储,当元素个数少的时候,它会使用一块连续的内存空间来存储,这样可以减少每个元素增加 prev 和 next 指针带来的内存消耗,最重要的是可以减少内存碎片化问题。

3.1 常见的链表结构示意图

每个 node 节点元素,都会持有一个 prev-> 执行前一个 node 节点和 next-> 指向后一个 node 节点的指针(引用),这种结构虽然支持前后顺序遍历,但是也带来了不小的内存开销,如果 node 节点仅仅是一个 int 类型的值,那么可想而知,引用的内存比例将会更大。


3.2 ziplist 示意图

ziplist 是一块连续的内存地址,他们之间无需持有 prev 和 next 指针,能通过地址顺序寻址访问。


3.3 quicklist 示意图

quicklist 是由多个 ziplist 组成的双向链表。


四、hash(字典)​

1、hash (字典) 相关介绍


1.1 hash (字典) 的内部结构

Redis 的 hash (字典) 相当于 Java 语言中的 HashMap,它是根据散列值分布的无序字典,内部的元素是通过键值对的方式存储。


hash (字典) 的实现与 Java 中的 HashMap(JDK1.7)的结构也是一致的,它的数据结构也是数组 + 链表组成的二维结构,节点元素散列在数组上,如果发生 hash 碰撞则使用链表串联在数组节点上。


1.2 hash (字典) 扩容

Redis 中的 hash (字典) 存储的 value 只能是字符串值,此外扩容与 Java 中的 HashMap 也不同。Java 中的 HashMap 在扩容的时候是一次性完成的,而 Redis 考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式 rehash 策略。

渐进式 rehash 指的是并非一次性完成,它是多次完成的,因此需要保理旧的 hash 结构,所以 Redis 中的 hash (字典) 会存在新旧两个 hash 结构,在 rehash 结束后也就是旧 hash 的值全部搬迁到新 hash 之后,新的 hash 在功能上才会完全替代以前的 hash。


1.3 hash (字典) 的相关使用场景

hash (字典) 可以用来存储对象的相关信息,一个 hash (字典) 代表一个对象,hash 的一个 key 代表对象的一个属性,key 的值代表属性的值。hash (字典) 结构相比字符串来说,它无需将整个对象进行序列化后进行存储。这样在获取的时候可以进行部分获取。所以相比之下 hash (字典) 具有如下的优缺点:
读取可以部分读取,节省网络流量
存储消耗的高于单个字符串的存储

2 hash (字典) 相关指令


2.1 hash (字典) 常用指令

hset -> hash (字典) 插入值,字典不存在则创建 key 代表字典名称,field 相当于 key,value 是 key 的值
hset key field value

hmset -> 批量设值
hmset key field value [field value …]

示例:
17.0.0.1:6379> hset book java "Thinking in Java"        # 字符串包含空格需要""包裹
2(integer) 1
3127.0.0.1:6379> hset book python "Python code"
4(integer) 1
5127.0.0.1:6379> hset book c "The best of c"
6(integer) 1
7127.0.0.1:6379> hmset book go "concurrency in go" mysql "high-performance MySQL" # 批量设值
8OK

hget -> 获取字典中的指定 key 的 value
hget key field

hgetall -> 获取字典中所有的 key 和 value,换行输出
hgetall key

示例:
1127.0.0.1:6379> hget book java
2"Thinking in Java"
3127.0.0.1:6379> hgetall book
41) "java"
52) "Thinking in Java"
63) "python"
74) "Python code"
85) "c"
96) "The best of c"

hlen -> 获取指定字典的 key 的个数
hlen key

举例:
1127.0.0.1:6379> hlen book
2(integer) 5

2.2 hash (字典) 使用小技巧

在 string (字符串) 中可以使用 incr 和 incrby 对 value 是整数的字符串进行自加操作,在 hash (字典) 结构中如果单个子 key 是整数也可以进行自加操作。

hincrby -> 增对 hash (字典) 中的某个 key 的整数 value 进行自加操作
hincrby key field increment

1127.0.0.1:6379> hset liziba money 10
2(integer) 1
3127.0.0.1:6379> hincrby liziba money -1
4(integer) 9
5127.0.0.1:6379> hget liziba money
6"9"

注意如果不是整数会报错。

1127.0.0.1:6379> hset liziba money 10.1
2(integer) 1
3127.0.0.1:6379> hincrby liziba money 1
4(error) ERR hash value is not an integer

五、set(集合)

1、set (集合) 相关介绍


1.1 set (集合) 的内部结构

Redis 的 set (集合) 相当于 Java 语言里的 HashSet,它内部的键值对是无序的、唯一的。它的内部实现了一个所有 value 为 null 的特殊字典。
集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。


1.2 set (集合) 的使用场景

set (集合) 由于其特殊去重复的功能,我们可以用来存储活动中中奖的用户的 ID,这样可以保证一个用户不会中奖两次。

2、set (集合) 相关指令

sadd -> 添加集合成员,key 值集合名称,member 值集合元素,元素不能重复
sadd key member [member …]

1127.0.0.1:6379> sadd name zhangsan
2(integer) 1
3127.0.0.1:6379> sadd name zhangsan        # 不能重复,重复返回0
4(integer) 0
5127.0.0.1:6379> sadd name lisi wangwu liumazi # 支持一次添加多个元素
6(integer) 3

smembers -> 查看集合中所有的元素,注意是无序的
smembers key

1127.0.0.1:6379> smembers name    # 无序输出集合中所有的元素
21) "lisi"
32) "wangwu"
43) "liumazi"
54) "zhangsan"

sismember -> 查询集合中是否包含某个元素
sismember key member

1127.0.0.1:6379> sismember name lisi  # 包含返回1
2(integer) 1
3127.0.0.1:6379> sismember name tianqi # 不包含返回0
4(integer) 0

scard -> 获取集合的长度
scard key

1127.0.0.1:6379> scard name
2(integer) 4

spop -> 弹出元素,count 指弹出元素的个数
spop key [count]

1127.0.0.1:6379> spop name    # 默认弹出一个
2"wangwu"
3127.0.0.1:6379> spop name 3    
41) "lisi"
52) "zhangsan"
63) "liumazi"

六、zset(有序集合)

1、zset (有序集合) 相关介绍


1.1 zset (有序集合) 的内部结构

zset (有序集合) 是 Redis 中最常问的数据结构。它类似于 Java 语言中的 SortedSet 和 HashMap 的结合体,它一方面通过 set 来保证内部 value 值的唯一性,另一方面通过 value 的 score(权重)来进行排序。这个排序的功能是通过 Skip List(跳跃列表)来实现的。
zset (有序集合) 的最后一个元素 value 被移除后,数据结构被自动删除,内存被回收。


1.2 zset (有序集合) 的相关使用场景

利用 zset 的去重和有序的效果可以由很多使用场景,举两个例子:
存储粉丝列表,value 是粉丝的 ID,score 是关注时间戳,这样可以对粉丝关注进行排序
存储学生成绩,value 使学生的 ID,score 是学生的成绩,这样可以对学生的成绩排名

2、zset (有序集合) 相关指令


1、zadd -> 向集合中添加元素,集合不存在则新建,key 代表 zset 集合名称,score 代表元素的权重,member 代表元素

zadd key [NX|XX] [CH] [INCR] score member [score member …]

1127.0.0.1:6379> zadd name 10 zhangsan
2(integer) 1
3127.0.0.1:6379> zadd name 10.1 lisi
4(integer) 1
5127.0.0.1:6379> zadd name 9.9 wangwu
6(integer) 1

2、zrange -> 按照 score 权重从小到大排序输出集合中的元素,权重相同则按照 value 的字典顺序排序 ([lexicographical order])

超出范围的下标并不会引起错误。 比如说,当 start 的值比有序集的最大下标还要大,或是 start > stop 时, zrange 命令只是简单地返回一个空列表。 另一方面,假如 stop 参数的值比有序集的最大下标还要大,那么 Redis 将 stop 当作最大下标来处理。

可以通过使用 WITHSCORES 选项,来让成员和它的 score 值一并返回,返回列表以 value1,score1, …, valueN,scoreN 的格式表示。客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。

zrange key start stop [WITHSCORES]

1127.0.0.1:6379> zrange name 0 -1 # 获取所有元素,按照score的升序输出
21) "wangwu"
32) "zhangsan"
43) "lisi"
5127.0.0.1:6379> zrange name 0 1        # 获取第一个和第二个slot的元素
61) "wangwu"
72) "zhangsan"
8127.0.0.1:6379> zadd name 10 tianqi    # 在上面的基础上添加score为10的元素
9(integer) 1
10127.0.0.1:6379> zrange name 0 2    # key相等则按照value字典排序输出
111) "wangwu"
122) "tianqi"
133) "zhangsan"
14127.0.0.1:6379> zrange name 0 -1 WITHSCORES # WITHSCORES 输出权重
151) "wangwu"
162) "9.9000000000000004"
173) "tianqi"
184) "10"
195) "zhangsan"
206) "10"
217) "lisi"
228) "10.1"

3、zrevrange -> 按照 score 权重从大到小输出集合中的元素,权重相同则按照 value 的字典逆序排序

其中成员的位置按 score 值递减 (从大到小) 来排列。 具有相同 score 值的成员按字典序的逆序 (reverse lexicographical order) 排列。 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE key start stop [WITHSCORES] 命令一样

zrevrange key start stop [WITHSCORES]

1127.0.0.1:6379> zrevrange name 0 -1 WITHSCORES
21) "lisi"
32) "10.1"
43) "zhangsan"
54) "10"
65) "tianqi"
76) "10"
87) "wangwu"
98) "9.9000000000000004"

4、zcard -> 当 key 存在且是有序集类型时,返回有序集的基数。 当 key 不存在时,返回 0

zcard key

1127.0.0.1:6379> zcard name
2(integer) 4

5、zscore -> 返回有序集 key 中,成员 member 的 score 值,如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil

zscore key member z

1127.0.0.1:6379> zscore name zhangsan
2"10"
3127.0.0.1:6379> zscore name liziba
4(nil)

6、zrank -> 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增 (从小到大) 顺序排列。

排名以 0 为底,也就是说,score 值最小的成员排名为 0

zrank key member

1127.0.0.1:6379> zrange name 0 -1
21) "wangwu"
32) "tianqi"
43) "zhangsan"
54) "lisi"
6127.0.0.1:6379> zrank name wangwu
7(integer) 0

7、zrangebyscore -> 返回有序集 key 中,所有 score 值介于 min 和 max 之间 (包括等于 min 或 max) 的成员。有序集成员按 score 值递增 (从小到大) 次序排列。

min 和 max 可以是 -inf 和 +inf ,这样一来,你就可以在不知道有序集的最低和最高 score 值的情况下,使用 [ZRANGEBYSCORE] 这类命令。

默认情况下,区间的取值使用闭区间,你也可以通过给参数前增加 (符号来使用可选的 [开区间] 小于或大于)

zrangebyscore key min max [WITHSCORES] [LIMIT offset count]

1127.0.0.1:6379> zrange name 0 -1 WITHSCORES # 输出全部元素
21) "wangwu"
32) "9.9000000000000004"
43) "tianqi"
54) "10"
65) "zhangsan"
76) "10"
87) "lisi"
98) "10.1"
10127.0.0.1:6379> zrangebyscore name 9 10
111) "wangwu"
122) "tianqi"
133) "zhangsan"
14127.0.0.1:6379> zrangebyscore name 9 10 WITHSCORES    # 输出分数
151) "wangwu"
162) "9.9000000000000004"
173) "tianqi"
184) "10"
195) "zhangsan"
206) "10"
21127.0.0.1:6379> zrangebyscore name -inf 10 # -inf 从负无穷开始
221) "wangwu"
232) "tianqi"
243) "zhangsan"
25127.0.0.1:6379> zrangebyscore name -inf +inf    # +inf 直到正无穷
261) "wangwu"
272) "tianqi"
283) "zhangsan"
294) "lisi"
30127.0.0.1:6379> zrangebyscore name (10 11  #  10 < score <=11
311) "lisi"
32127.0.0.1:6379> zrangebyscore name (10 (10.1  # 10 < socre < -11
33(empty list or set)
34127.0.0.1:6379> zrangebyscore name (10 (11
351) "lisi"

8、zrem -> 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略

zrem key member [member …]

1127.0.0.1:6379> zrange name 0 -1
21) "wangwu"
32) "tianqi"
43) "zhangsan"
54) "lisi"
6127.0.0.1:6379> zrem name zhangsan # 移除元素
7(integer) 1
8127.0.0.1:6379> zrange name 0 -1
91) "wangwu"
102) "tianqi"
113) "lisi"

七、Skip List

1、简介

跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

Skip List (跳跃列表) 这种随机的数据结构,可以看做是一个二叉树的变种,它在性能上与红黑树、AVL 树很相近;但是 Skip List (跳跃列表) 的实现相比前两者要简单很多,目前 Redis 的 zset 实现采用了 Skip List (跳跃列表)(其它还有 LevelDB 等也使用了跳跃列表)。

RBT 红黑树与 Skip List (跳跃列表) 简单对比:

RBT 红黑树
插入、查询时间复杂度 O (logn)
数据天然有序
实现复杂,设计变色、左旋右旋平衡等操作
需要加锁

Skip List 跳跃列表
插入、查询时间复杂度 O (logn)
数据天然有序
实现简单,链表结构
无需加锁

2、Skip List 算法分析


2.1 Skip List 论文

这里贴出 Skip List 的论文,需要详细研究的请看论文,下文部分公式、代码、图片出自该论文。
Skip Lists: A Probabilistic Alternative to Balanced Treeshttps://www. cl.cam.ac.uk/teaching/2005/Algorithms/skiplists.pdf

2.2 Skip List 动态图

先通过一张动图来了解 Skip List 的插入节点元素的流程,此图来自维基百科。


2.3 Skip List 算法性能分析

2.3.1 计算随机层数算法

首先分析的是执行插入操作时计算随机数的过程,这个过程会涉及层数的计算,所以十分重要。对于节点他有如下特性:

节点都有第一层的指针
节点有第 i 层指针,那么第 i+1 层出现的概率为 p
节点有最大层数限制,MaxLevel


2.3.2 节点包含的平均指针数目

Skip List 属于空间换时间的数据结构,这里的空间指的就是每个节点包含的指针数目,这一部分是额外的内内存开销,可以用来度量空间复杂度。random () 是个随机数,因此产生越高的节点层数,概率越低(Redis 标准源码中的晋升率数据 1/4,相对来说 Skip List 的结构是比较扁平的,层高相对较低)。其定量分析如下:

level = 1 概率为 1-p
level >=2 概率为 p
level = 2 概率为 p (1-p)
level >= 3 概率为 p^2
level = 3 概率为 p^2 (1-p)
level >=4 概率为 p^3
level = 4 概率为 p^3 (1-p)
……

得出节点的平均层数(节点包含的平均指针数目):


所以 Redis 中 p=1/4 计算的平均指针数目为 1.33

2.3.3 时间复杂度计算

以下推算来自论文内容

假设 p=1/2,在以 p=1/2 生成的 16 个元素的跳过列表中,我们可能碰巧具有 9 个元素,1 级 3 个元素,3 个元素 3 级元素和 1 个元素 14 级(这不太可能,但可能会发生)。我们该怎么处理这种情况?如果我们使用标准算法并在第 14 级开始我们的搜索,我们将会做很多无用的工作。那么我们应该从哪里开始搜索?此时我们假设 SkipList 中有 n 个元素,第 L 层级元素个数的期望是 1/p 个;每个元素出现在 L 层的概率是 p^(L-1), 那么第 L 层级元素个数的期望是 n * (p^L-1);得到 1 /p =n * (p^L-1)

11 / p = n * (p^L-1)
2n = (1/p)^L
3L = log(1/p)^n

所以我们应该选择 MaxLevel = log (1/p)^n
定义:MaxLevel = L (n) = log (1/p)^n

推算 Skip List 的时间复杂度,可以用逆向思维,从层数为 i 的节点 x 出发,返回起点的方式来回溯时间复杂度,节点 x 点存在两种情况:
节点 x 存在 (i+1) 层指针,那么向上爬一级,概率为 p,对应下图 situation c.
节点 x 不存在 (i+1) 层指针,那么向左爬一级,概率为 1-p,对应下图 situation b.


设 C (k) = 在无限列表中向上攀升 k 个 level 的搜索路径的预期成本(即长度)那么推演如下:
1C(0)=0
2C(k)=(1-p)×(情况b的查找长度) + p×(情况c的查找长度)
3C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
4C(k)=1/p+C(k-1)
5C(k)=k/p

上面推演的结果可知,爬升 k 个 level 的预期长度为 k/p,爬升一个 level 的长度为 1/p。

由于 MaxLevel = L (n), C (k) = k /p,因此期望值为:(L (n) – 1) /p;将 L (n) = log (1/p)^n 代入可得:(log (1/p)^n - 1) /p;将 p = 1 / 2 代入可得:2 * log2^n - 2,即 O (logn) 的时间复杂度。

3、Skip List 特性及其实现


2.1 Skip List 特性

Skip List 跳跃列表通常具有如下这些特性

Skip List 包含多个层,每层称为一个 level,level 从 0 开始递增
Skip List 0 层,也就是最底层,应该包含所有的元素
每一个 level / 层都是一个有序的列表
level 小的层包含 level 大的层的元素,也就是说元素 A 在 X 层出现,那么 想 X>Z>=0 的 level / 层都应该包含元素 A
每个节点元素由节点 key、节点 value 和指向当前节点所在 level 的指针数组组成

2.2 Skip List 查询

假设初始 Skip List 跳跃列表中已经存在这些元素,他们分布的结构如下所示:


此时查询节点 88,它的查询路线如下所示:


从 Skip List 跳跃列表最顶层 level3 开始,往后查询到 10 < 88 && 后续节点值为 null && 存在下层 level2
level2 10 往后遍历,27 < 88 && 后续节点值为 null && 存在下层 level1
level1 27 往后遍历,88 = 88,查询命中

2.3 Skip List 插入

Skip List 的初始结构与 2.3 中的初始结构一致,此时假设插入的新节点元素值为 90,插入路线如下所示:

查询插入位置,与 Skip List 查询方式一致,这里需要查询的是第一个比 90 大的节点位置,插入在这个节点的前面, 88 < 90 < 100
构造一个新的节点 Node (90),为插入的节点 Node (90) 计算一个随机 level,这里假设计算的是 1,这个 level 时随机计算的,可能时 1、2、3、4… 均有可能,level 越大的可能越小,主要看随机因子 x ,层数的概率大致计算为 (1/x)^level ,如果 level 大于当前的最大 level3,需要新增 head 和 tail 节点
节点构造完毕后,需要将其插入列表中,插入十分简单步骤 -> Node (88).next = Node (90); Node (90).prev = Node (80); Node (90).next = Node (100); Node (100).prev = Node (90);


2.4 Skip List 删除

删除的流程就是查询到节点,然后删除,重新将删除节点左右两边的节点以链表的形式组合起来即可,这里不再画图。

4、手写实现一个简单 Skip List

实现一个 Skip List 比较简单,主要分为两个步骤:
定义 Skip List 的节点 Node,节点之间以链表的形式存储,因此节点持有相邻节点的指针,其中 prev 与 next 是同一 level 的前后节点的指针,down 与 up 是同一节点的多个 level 的上下节点的指针
定义 Skip List 的实现类,包含节点的插入、删除、查询,其中查询操作分为升序查询和降序查询(往后和往前查询),这里实现的 Skip List 默认节点之间的元素是升序链表

3.1 定义 Node 节点

Node 节点类主要包括如下重要属性:
score -> 节点的权重,这个与 Redis 中的 score 相同,用来节点元素的排序作用
value -> 节点存储的真实数据,只能存储 String 类型的数据
prev -> 当前节点的前驱节点,同一 level
next -> 当前节点的后继节点,同一 level
down -> 当前节点的下层节点,同一节点的不同 level
up -> 当前节点的上层节点,同一节点的不同 level


3.2 SkipList 节点元素的操作类

SkipList 主要包括如下重要属性:
head -> SkipList 中的头节点的最上层头节点(level 最大的层的头节点),这个节点不存储元素,是为了构建列表和查询时做查询起始位置的,具体的结构请看 2.3 中的结构
tail -> SkipList 中的尾节点的最上层尾节点(level 最大的层的尾节点),这个节点也不存储元素,是查询某一个 level 的终止标志
level -> 总层数
size -> Skip List 中节点元素的个数
random -> 用于随机计算节点 level,如果 random.nextDouble () < 1/2 则需要增加当前节点的 level,如果当前节点增加的 level 超过了总的 level 则需要增加 head 和 tail (总 level)