Redis 中有 5 种数据结构,分别是字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set),由于使用 Redis 场景的开发中确定是没法避开这些基础结构的,因此熟练掌握它们也就成了一项必不可少的能力。本文章精要地介绍了 Redis 的这几种数据结构,主要覆盖了它们各自的定义、基本用法与相关要点。前端
字符串是 Redis 中的最基础的数据结构,咱们保存到 Redis 中的 key,也就是键,就是字符串结构的。除此以外,Redis 中其它数据结构也是在字符串的基础上设计的,可见字符串结构对于 Redis 是多么重要。python
Redis 中的字符串结构能够保存多种数据类型,如:简单的字符串、JSON、XML、二进制等,但有一点要特别注意:在 Redis 中字符串类型的值最大只能保存 512 MB。redis
下面经过命令了解一下对字符串类型的操做:数据库
set key value [EX seconds] [PX milliseconds] [NX|XX]
set 命令有几个非必须的选项,下面咱们看一下它们的具体说明:编程
set 命令带上可选参数 NX 和 XX 在实际开发中的做用与 setnx 和 setxx 命令相同。咱们知道 setnx 命令只有当 key 不存在的时候才能设置成功,换句话说,也就是同一个 key 在执行 setnx 命令时,只能成功一次,而且因为 Redis 的单线程命令处理机制,即便多个客户端同时执行 setnx 命令,也只有一个客户端执行成功。因此,基于 setnx 这种特性,setnx 命令能够做为分布式锁的一种解决方案。安全
而 setxx 命令则能够在安全性比较高的场景中使用,由于 set 命令执行时,会执行覆盖的操做,而 setxx 在更新 key 时能够确保该 key 已经存在了,因此为了保证 key 中数据类型的正确性,可使用 setxx 命令。网络
get key
mset key value
mget key
若是有些键不存在,那么它的值将为 nil,也就是空,而且返回结果的顺序与传入时相同。数据结构
incr key
incr 命令用于对值作自增操做,返回的结果分为 3 种状况:架构
除了有 incr 自增命令外,Redis 中还提供了其它对数字处理的命令。例如:app
decr key 自减 incrby kek increment 自增指定数字 decrby key decrement 自减指定数字 incrbyfloat key increment 自增浮点数
append key value
append 命令能够向字符串尾部追加值。
strlen key
因为每一个中文占用 3 个字节,因此 jilinwula 这个键,返回是字符串长度为 12,而不是 4。
getset key value
setrange key offeset value
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 种:
大部分语言基本都提供了哈希类型,如 Java 语言中的 Map 类型及 Python 语言中的字典类型等等。虽然语言不一样,但它们基本使用都是同样的,也就是都是键值对结构的。例如:
value={{field1, value1}
经过下图能够直观感觉一下字符串类型和哈希类型的区别:
Redis 中哈希类型都是键值对结构的,因此要特别注意这里的 value 并非指 Redis 中 key 的 value,而是哈希类型中的 field 所对应的 value。
下面咱们仍是和介绍字符串类型同样,了解一下 Redis 中哈希类型的相关命令。
hset key field value
咱们看上图执行的命令知道,hset 命令也是有返回值的。若是 hset 命令设置成功,则返回 1,不然返回 0。除此以外 Redis 也为哈希类型提供了 hsetnx 命令。在前文对字符串的介绍中,咱们知道 nx 命令只有当 key 不存在的时候,才能设置成功,一样的,hsetnx 命令在 field 不存在的时候,才能设置成功。
hget key field
咱们看 hget 命令和 get 有很大的不一样,get 命令在获取的时候,只要写一个名字就能够了,而 hget 命令则要写两个名字,第一个名字是 key,第二个名字是 field。固然 key 或者 field 不存在时,返回的结果都是 nil。
hdel key field [field ...]
hdel 命令删除的时候,也会有返回值,而且这个返回就是成功删除 field 的个数。当 field 不存在时,并不会报错,而是直接返回 0。
hlen key
hlen 命令返回的就是当前 key 中 field 的个数,若是 key 不存在,则返回 0。
hmget key field [field ...] hmset key field value [field value ...]
hmset 命令和 hmget 命令分别是批量设置和获取值的,hmset 命令没有什么要注意的,但 hmget 命令要特别注意,当咱们获取一个不存在的 key 或者不存在的 field 时,Redis 并不会报错,而是返回 nil。而且有几个 field 不存在,则 Redis 返回几个 nil。
hexists key field
当执行 hexists 命令时,若是当前 key 包括 field,则返回 1,不然返回 0。
hkeys key
hvals key
hgetall key
hgetall 命令会返回当前 key 中的全部 field-value,并按照顺序依次返回。
hincrby key field increment hincrbyfloat key field increment
hincrby 命令和 incrby 命令的使用功能基本同样,都是对值进行增量操做的,惟一不一样的就是 incrby 命令的做用域是 key,而 hincrby 命令的做用域则是 field。
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 和 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 来验证此功能,下面为具体的代码:
import redis r = redis.Redis(host='127.0.0.1', port=6379) print('Key为【userinfo】的字节编码为【%s】' % r.object('encoding', 'userinfo').decode('utf-8')) for i in range(1,513): r.hset('userinfo', i, '吉林乌拉') print('Key为【userinfo】的字节编码为【%s】' % r.object('encoding', 'userinfo').decode('utf-8')) Key为【userinfo】的字节编码为【ziplist】 Key为【userinfo】的字节编码为【hashtable】
Redis 中列表类型能够简单地理解为存储多个有序字符串的一种新类型,这种类型除了字符串类型中已有的功能外,还提供了其它功能,如能够对列表的两端插入和弹出元素(在列表中的字符串均可以称之为元素),除此以外还能够获取指定的元素列表,而且还能够经过索引下标获取指定元素等等。下面咱们经过下图来看一下 Redis 中列表类型的插入和弹出操做:
下面咱们看一下 Redis 中列表类型的获取与删除操做:
Redis 列表类型的特色以下:
下面咱们仍是和学习其它数据类型同样,咱们仍是先学习一下 Redis 列表类型的命令。
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 命令在执行成功后也是会有返回值的,返回的结果就是当前列表中元素的个数。
lrange key start stop
lrange 命令会获取列表中指定索引范围的全部元素。
经过索引获取列表主要有两个特色:
lindex key index
llen key
lpop key
lpop 命令执行成功后会返回当前被删除的元素名称。
rpop key
rpop 命令和 lpop 命令的使用方式同样。
lrem key count value
lrem 命令会将列表中等于 value 的元素删除掉,而且会根据 count 参数来决定删除 value 的元素个数。
下面咱们看一下 count 参数的使用说明:
count > 0:表示从左到右,最多删除 count 个元素。也就是以下图所示:
咱们看上图中的命令中,虽然咱们将 count 参数指定的是 5,将 value 参数指定的是 2,但因为当前列表中只有一个 2,因此,当前 lrem 命令最多只能删除 1 个元素,而且 lrem 命令也是有返回值的,也就是当前成功删除元素的个数。
count < 0:从右到左,最多删除 count 个元素。
count = 0:删除全部元素。
ltrim key start stop
ltrim 命令会直接保留 start 索引到 stop 索引的之间的元素,并包括当前元素,而以外的元素则都会删除掉,因此该命令也叫修剪列表。
而且有一点要注意,ltrim 命令不会返回当前的列表中元素的个数,而是返回改命令是否成功的状态。
lset key index value
blpop key [key ...] timeout brpop key [key ...] timeout
blpop 和 brpop 命令是 lpop 和 rpop 命令的阻塞版本,它们除了弹出方向不一样之外,使用方法基本相同。
下面咱们看一下该命令的详细使用。
列表为空:若是 timeout=3,则表示客户端等待 3 秒后才能返回结果,若是 timeout=0,则表示客户端会一直等待,也就是会阻塞。
因为我期间向列表中插入了元素,不然上述命令将一直阻塞下去。
列表不为空:若是 timeout=0,而且列表不为空时,则 blpop 和 brpop 命令会当即返回结果,不会阻塞。
下面咱们看一下 blpop 和 brpop 命令的注意事项。
若是使用 blpop 和 brpop 命令指定多个键时,blpop 和 brpop 命令会从左到右遍历键,而且一旦有一个键能返回元素,则客户端会当即返回。
当列表为空时,上述命令会阻塞,若是向上述中的任何一个键中插入元素,则上述命令会直接返回该键的元素。
若是多个客户端都对同一个键执行 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) |
列表中的内部编码有两种,它们分别是:
Redis 中的集合类型,也就是 set。在 Redis 中 set 也是能够保存多个字符串的,常常有人会分不清 list 与 set,下面咱们重点介绍一下它们之间的不一样:
下面咱们介绍一下 set 中的相关命令。
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 中。
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 中的元素是无序的。
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是多个元素个数和 |
备注:咱们能够经过 set-max-intset-entries 参数来设置上述中的默认参数。
下面咱们看一下具体的事例,来验证咱们上面提到的内部编码。
当元素个数较少而且都是整数时,内部编码为 intset。
当元素不全是整数时,内部编码为 hashtable。
当元素个数超过 512 个时,内部编码为 hashtable。
import redis r = redis.Redis(host='127.0.0.1', port=6379) if r.object('encoding', 'setkey') != None: print('Key为【setkey】的字节编码为【%s】' % r.object('encoding', 'setkey').decode('utf-8')) for i in range(1, 600): r.sadd('setkey', i) if r.object('encoding', 'setkey') != None: print('Key为【setkey】的字节编码为【%s】' % r.object('encoding', 'setkey').decode('utf-8')) Key为【setkey】的字节编码为【intset】 Key为【setkey】的字节编码为【hashtable】
看名字咱们就知道,有序集合也是一种集合,而且这个集合仍是有序的。列表也是有序的,那它和有序集合又有什么不一样呢?有序集合的有序和列表的有序是不一样的。列表中的有序指的的是插入元素的顺序和查询元素的顺序相同,而有序集合中的有序指的是它会为每一个元素设置一个分数(score),而查询时能够经过分数计算元素的排名,而后再返回结果。由于有序集合也是集合类型,因此有序集合中也是不插入重复元素的,但在有序集合中分数则是能够重复,那若是在有序集合中有多个元素的分数是相同的,这些重复元素的排名是怎么计算的呢?后边咱们再作详细说明。
下面先看一下列表、集合、有序集合三种数据类型之间的区别:
数据结构 |
是否容许重复元素 |
是否有序 |
有序实现方式 |
应用场景 |
列表 |
是 |
是 |
索引下标 |
时间轴、消息队列 |
集合 |
否 |
否 |
无 |
标签、社交 |
有序集合 |
否 |
是 |
分数 |
排行榜、社交 |
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zadd 命令也是有返回值的,返回值就是当前 zadd 命令成功添加元素的个数。zadd 命令有不少选填参数:
备注:因为有序集合相比集合提供了排序字段,正是由于如此也付出了相应的代价,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 是命令不一样的,前者经过分数计算出最后的排名,然后者则是直接返回当前元素的分数。
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
zinterstore destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]
zinterstore 命令参数比较多:
下面咱们将权重设置为 0.5,这样当计算交集后,有序集合中的元素分数将都会减半,而且使用 max 参数汇总。
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是结果集中元素个数。 |
有序集合类型的内部编码有两种,它们分别是:
备注:上述中的默认值,也能够经过如下参数设置:zset-max-ziplist-entries 和 zset-max-ziplist-value。
下面咱们用如下示例来验证上述结论。
当元素个数比较少,而且每一个元素也比较小时,内部编码为 ziplist:
当元素个数超过 128 时,内部编码为 skiplist。下面咱们将采用 python 动态建立128个元素,下面为源码:
import redis r = redis.Redis(host='127.0.0.1', port=6379) if r.object('encoding', 'zsetkey') != None: print('Key为【zsetkey】的字节编码为【%s】' % r.object('encoding', 'zsetkey').decode('utf-8')) for i in range(1, 600): r.zadd('zsetkey',i,1) if r.object('encoding', 'zsetkey') != None: print('Key为【zsetkey】的字节编码为【%s】' % r.object('encoding', 'zsetkey').decode('utf-8')) Key为【zsetkey】的字节编码为【ziplist】 Key为【zsetkey】的字节编码为【skiplist】
当有序集合中有任何一个元素大于 64 个字节时,内部编码为 skiplist。
import redis r = redis.Redis(host='127.0.0.1', port=6379) if r.object('encoding', 'zsetkey') != None: print('Key为【zsetkey】的字节编码为【%s】' % r.object('encoding', 'zsetkey').decode('utf-8')) value = '' for i in range(1, 600): value += str(i) r.zadd('zsetkey',value,1) if r.object('encoding', 'zsetkey') != None: print('Key为【zsetkey】的字节编码为【%s】' % r.object('encoding', 'zsetkey').decode('utf-8')) Key为【zsetkey】的字节编码为【skiplist】
到这里,本文就结束了,写了这么多,其实主要大部分是关于命令的简单介绍,其中也介绍了一些关键要点,若有不正确的地方,欢迎留言。
裴彬,Java 工程师,喜欢编程,本身开发的博客系统已上线:jilinwula.com
本文系做者投稿文章。欢迎投稿。