Redis 提供了 5 种数据结构,理解每种数据结构的特点对于 Redis 开发运维⾮常重要,同时掌握每种数据结构的常⻅命令,会在使⽤ Redis 的时候做到游刃有余。
目录
2.1 预备知识
2.1.1 基本全局命令
2.1.2 数据结构和内部编码
2.1.3 单线程架构
2.2 String 字符串
2.2.1 常⻅命令
2.2.2 计数命令
2.2.3 其他命令
2.2.4 命令⼩结
2.2.5 内部编码
2.2.6 典型使⽤场景
2.3 Hash 哈希
2.3.1 命令
2.3.2 命令⼩结
2.3.3 内部编码
2.3.4 使⽤场景
2.3.5 缓存⽅式对⽐
2.4 List 列表
2.4.1 命令
2.4.2 阻塞版本命令
2.4.3 命令⼩结
2.4.4 内部编码
2.4.5 使⽤场景
2.5 Set 集合
2.5.1 普通命令
2.5.2 集合间操作
2.5.3 命令⼩结
2.5.4 内部编码
2.5.5 使⽤场景
2.6 Zset 有序集合
2.6.1 普通命令
2.6.2 集合间操作
2.6.3 命令⼩结
2.6.4 内部编码
2.6.5 使⽤场景
2.7 渐进式遍历
2.8 数据库管理
2.8.1 切换数据库
2.8.2 清除数据库
2.9 本章重点回顾
在正式介绍 5 种数据结构之前,了解⼀下 Redis 的⼀些全局命令、数据结构和内部编码、单线程 命令处理机制是⼗分必要的,它们能为后⾯内容的学习打下⼀个良好的基础. 主要体现在两个⽅⾯:
1)Redis 的命令有上百个,如果纯靠死记硬背⽐较困难,但是如果理解 Redis 的⼀些机制,会发现这些命令有很强的通⽤性。
2)Redis 不是万⾦油,有些数据结构和命令必须在特定场景下使⽤,⼀旦使⽤不当可能对 Redis 本⾝ 或者应⽤本⾝造成致命伤害。
Redis 有 5 种数据结构,但它们都是键值对种的值,对于键来说有⼀些通⽤的命令。
KEYS 返回所有满⾜样式(pattern)的 key。
⽀持如下统配样式。
• h?llo 匹配 hello , hallo 和 hxllo
• h*llo 匹配 hllo 和 heeeello
• h[ae]llo 匹配 hello 和 hallo 但不匹配 hillo
• h[^e]llo 匹配 hallo , hbllo , ... 但不匹配 hello
• h[a-b]llo 匹配 hallo 和 hbllo
语法:
KEYS pattern
时间复杂度:O(N)
返回值:匹配 pattern 的所有 key。
redis> MSET firstname Jack lastname Stuntman age 35
"OK"
redis> KEYS *name*
1) "firstname"
2) "lastname"
redis> KEYS a??
1) "age"
redis> KEYS *
1) "age"
2) "firstname"
3) "lastname"
EXISTS
判断某个 key 是否存在。
语法:
EXISTS key [key ...]
时间复杂度:O(1)
返回值:key 存在的个数。
⽰例:
redis> SET key1 "Hello"
"OK"
redis> EXISTS key1
(integer) 1
redis> EXISTS nosuchkey
(integer) 0
redis> SET key2 "World"
"OK"
redis> EXISTS key1 key2 nosuchkey
(integer) 2
DEL
删除指定的 key。
语法:
DEL key [key ...]
时间复杂度:O(1)
返回值:删除掉的 key 的个数。
⽰例:
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> DEL key1 key2 key3
(integer) 2
EXPIRE
为指定的 key 添加秒级的过期时间(Time To Live TTL)
语法:
EXPIRE key seconds
时间复杂度:O(1)
返回值:1 表⽰设置成功。0 表⽰设置失败。
⽰例:
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
TTL
获取指定 key 的过期时间,秒级。
语法:
TTL key
时间复杂度:O(1)
返回值:剩余过期时间。-1 表⽰没有关联过期时间,-2 表⽰ key 不存在。
⽰例:
redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
TYPE
返回 key 对应的数据类型。
语法:
TYPE key
时间复杂度:O(1)
返回值: none , string , list , set , zset , hash and stream .。
⽰例:
redis> SET key1 "value"
"OK"
redis> LPUSH key2 "value"
(integer) 1
redis> SADD key3 "value"
(integer) 1
redis> TYPE key1
"string"
redis> TYPE key2
"list"
redis> TYPE key3
"set"
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列 表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构
实际上 Redis 针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现,这样 Redis 会 在合适的场景选择合适的内部编码 ,可以通 过 object encoding 命令查询内部编码:
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> lpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding hello
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist
Redis 使⽤了单线程架构来实现⾼性能的内存数据库服务,本节⾸先通过多个客⼾端命令调⽤的例 ⼦说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型为什么性能如此之⾼,最终给出为什 么理解单线程模型是使⽤和运维 Redis 的关键。
1. 引出单线程模型
现在开启了三个 redis-cli 客⼾端同时执⾏命令。 客⼾端 1 设置⼀个字符串键值对:
127.0.0.1:6379> set hello world
客⼾端 2 对 counter 做⾃增操作:
127.0.0.1:6379> incr counter
客⼾端 3 对 counter 做⾃增操作:
127.0.0.1:6379> incr counter
我们已经知道从客⼾端发送的命令经历了:发送命令、执⾏命令、返回结果三个阶段,其中我们重点关注第 2 步。我们所谓的 Redis 是采⽤单线程模型执⾏命令的是指:虽然三个客⼾端看起来是同时要求 Redis 去执⾏命令的,但微观⻆度,这些命令还是采⽤线性⽅式去执⾏的,只是原则上命令的执⾏顺序是不确定的,但⼀定不会有两条命令被同步执⾏,可以想象 Redis 内部只有⼀个服务窗⼝,多个客⼾端按照它们达到的先后顺序被排队在窗⼝前,依次接受 Redis 的服务,所以两条 incr 命令⽆论执⾏顺序,结果⼀定是 2,不会发⽣并发问题,这个就是 Redis 的单线程执⾏模型。
2. 为什么单线程还能这么快 ?
通常来讲,单线程处理能⼒要⽐多线程差,例如有 10 000 公⽄货物,每辆⻋的运载能⼒是每次 200 公⽄,那么要 50 次才能完成;但是如果有 50 辆⻋,只要安排合理,只需要依次就可以完成任 务。那么为什么 Redis 使⽤单线程模型会达到每秒万级别的处理能⼒呢?可以将其归结为三点:
a. 纯内存访问。Redis 将所有数据放在内存中,内存的响应时⻓⼤约为 100 纳秒,这是 Redis 达 到每秒万级别访问的重要基础。
b. ⾮阻塞 IO。Redis 使⽤ epoll 作为 I/O 多路复⽤技术的实现,再加上 Redis ⾃⾝的事件处理模型 将 epoll 中的连接、读写、关闭都转换为事件,不在⽹络 I/O 上浪费过多的时间
c. 单线程避免了线程切换和竞态产⽣的消耗。单线程可以简化数据结构和算法的实现,让程序模 型更简单;其次多线程避免了在线程竞争同⼀份共享数据时带来的切换和等待消耗。
虽然单线程给 Redis 带来很多好处,但还是有⼀个致命的问题:对于单个命令的执⾏时间都是有 要求的。如果某个命令执⾏过⻓,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客⼾ 端的阻塞,对于 Redis 这种⾼性能的服务来说是⾮常严重的,所以 Redis 是⾯向快速执⾏场景的数据 库。
字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:
1)⾸先 Redis 中所有的键的类型都是字符串类型,⽽且其他⼏种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。
2)其次,字符串类型的值实际可以是字符串,包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚⾄是⼆进制流数据,例如图⽚、⾳频、视频等。不过⼀个字符串的最⼤值不能超过 512 MB。
SET —— 将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,⽆论原来的数据类型是什么。之 前关于此 key 的 TTL 也全部失效。
语法:
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
时间复杂度:O(1)
返回值:如果设置成功,返回 OK。 如果 SET 指定了 NX 或 XX 但条件不满⾜,SET 不会执⾏,并返回 (nil)。
选项: SET 命令⽀持多种选项来影响它的⾏为:
• EX seconds⸺使⽤秒作为单位设置 key 的过期时间。
• PX milliseconds⸺使⽤毫秒作为单位设置 key 的过期时间。
• NX ⸺只在 key 不存在时才进⾏设置,即如果 key 之前已经存在,设置不执⾏。
• XX ⸺只在 key 存在时才进⾏设置,即如果 key 之前不存在,设置不执⾏。
注意:由于带选项的 SET 命令可以被 SETNX 、 SETEX 、 PSETEX 等命令代替,所以之后的版本 中,Redis 可能进⾏合并。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> SET mykey "Hello"
OK
redis> GET mykey
"Hello"
redis> SET mykey "World" NX
(nil)
redis> DEL mykey
(integer) 1
redis> EXISTS mykey
(integer) 0
redis> SET mykey "World" XX
(nil)
redis> GET mykey
(nil)
redis> SET mykey "World" NX
OK
redis> GET mykey
"World"
redis> SET mykey "Will expire in 10s" EX 10
OK
redis> GET mykey
"Will expire in 10s"
redis> GET mykey # 10秒之后
(nil)
GET —— 获取 key 对应的 value。如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错。
语法:
GET key
时间复杂度:O(1)
返回值:key 对应的 value,或者 nil 当 key 不存在。
⽰例:
redis> GET nonexisting
(nil)
redis> SET mykey "Hello"
"OK"
redis> GET mykey
"Hello"
redis> DEL mykey
(integer) 1
redis> EXISTS mykey
(integer) 0
redis> HSET mykey name Bob
(integer) 1
redis> GET mykey
(error) WRONGTYPE Operation against a key holding the wrong kind of value
MGET —— ⼀次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。
语法:
MGET key [key ...]
时间复杂度:O(N) N 是 key 数量
返回值:对应 value 的列表
⽰例:
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)
MSET —— ⼀次性设置多个 key 的值。
语法:
MSET key value [key value ...]
时间复杂度:O(N) N 是 key 数量
返回值:永远是 OK
⽰例:
redis> MSET key1 "Hello" key2 "World"
"OK"
redis> GET key1
"Hello"
redis> GET key2
"World"
使⽤ mget / mset 由于可以有效地减少了⽹络时间,所以性能相较更⾼。
学会使⽤批量操作,可以有效提⾼业务处理效率,但是要注意,每次批量操作所发送的键的数量也不 是⽆节制的,否则可能造成单⼀命令执⾏时间过⻓,导致 Redis 阻塞。
SETNX —— 设置 key-value 但只允许在 key 之前不存在的情况下。
语法:
SETNX key value
时间复杂度:O(1)
返回值:1 表⽰设置成功。0 表⽰没有设置。
⽰例:
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
INCR ——将 key 对应的 string 表⽰的数字加⼀。
如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对 应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。
语法:
INCR key
时间复杂度:O(1)
返回值:integer 类型的加完后的数值。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> INCR mykey
(integer) 1
redis> SET mykey "10"
"OK"
redis> INCR mykey
(integer) 11
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> INCR mykey
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> INCR mykey
(error) value is not an integer or out of range
INCRBY—— 将 key 对应的 string 表⽰的数字加上对应的值。
如果 key 不存在,则视为 key 对应的 value 是 0。如 果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。
语法:
INCRBY key decrement
时间复杂度:O(1)
返回值:integer 类型的加完后的数值。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> INCRBY mykey 3
1
2
3
(integer) 3
redis> SET mykey "10"
"OK"
redis> INCRBY mykey 3
(integer) 13
redis> INCRBY mykey "not a number"
(error) ERR value is not an integer or out of range
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> INCRBY mykey 3
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> INCRBY mykey 3
(error) value is not an integer or out of range
DECR ——将 key 对应的 string 表⽰的数字减⼀。
如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对 应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。
语法:
DECR key
时间复杂度:O(1)
返回值:integer 类型的减完后的数值。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> DECR mykey
(integer) -1
redis> SET mykey "10"
"OK"
redis> DECR mykey
(integer) 9
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> DECR mykey
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> DECR mykey
(error) value is not an integer or out of range
DECYBY ——将 key 对应的 string 表⽰的数字减去对应的值。
如果 key 不存在,则视为 key 对应的 value 是 0。如 果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。
语法:
DECRBY key decrement
时间复杂度:O(1)
返回值:integer 类型的减完后的数值。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> DECRBY mykey 3
(integer) -3
redis> SET mykey "10"
"OK"
redis> DECRBY mykey 3
(integer) 7
redis> DECRBY mykey "not a number"
(error) ERR value is not an integer or out of range
redis> SET mykey "234293482390480948029348230948"
"OK"
redis> DECRBY mykey 3
(error) value is not an integer or out of range
redis> SET mykey 'not a number'
"OK"
redis> DECRBY mykey 3
(error) value is not an integer or out of range
INCRBYFLOAT——将 key 对应的 string 表⽰的浮点数加上对应的值。
如果对应的值是负数,则视为减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是⼀个浮点数,则报 错。允许采⽤科学计数法表⽰浮点数。
语法:
INCRBYFLOAT key increment
时间复杂度:O(1)
返回值:加/减完后的数值。
⽰例:
redis> SET mykey 10.50
"OK"
redis> INCRBYFLOAT mykey 0.1
"10.6"
redis> INCRBYFLOAT mykey -5
"5.6"
redis> SET mykey 5.0e3
"OK"
redis> INCRBYFLOAT mykey 2.0e2
"5200"
很多存储系统和编程语⾔内部使⽤ CAS 机制实现计数功能,会有⼀定的 CPU 开销,但在 Redis 中完全 不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执⾏。
APPEND —— 如果 key 已经存在并且是⼀个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在, 则效果等同于 SET 命令。
语法:
APPEND KEY VALUE
时间复杂度:O(1). 追加的字符串⼀般⻓度较短, 可以视为 O(1).
返回值:追加完成之后 string 的⻓度。
⽰例:
redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> GET mykey
"Hello"
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"
GETRANGE —— 返回 key 对应的 string 的⼦串,由 start 和 end 确定(左闭右闭)。
可以使⽤负数表⽰倒数。-1 代表 倒数第⼀个字符,-2 代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整 成正确的值。
语法:
GETRANGE key start end
时间复杂度:O(N). N 为 [start, end] 区间的⻓度. 由于 string 通常⽐较短, 可以视为是 O(1)
返回值:string 类型的⼦串
⽰例:
redis> SET mykey "This is a string"
"OK"
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"
redis> GETRANGE mykey 10 100
"string"
SETRANGE —— 覆盖字符串的⼀部分,从指定的偏移开始。
语法:
SETRANGE key offset value
时间复杂度:O(N), N 为 value 的⻓度. 由于⼀般给的 value ⽐较短, 通常视为 O(1).
返回值:替换后的 string 的⻓度。
⽰例:
redis> SET key1 "Hello World"
"OK"
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"
STRLEN —— 获取 key 对应的 string 的⻓度。
当 key 存放的类似不是 string 时,报错。
语法:
STRLEN key
时间复杂度:O(1) 返回值:string 的⻓度。或者当 key 不存在时,返回 0。 ⽰例:
redis> SET mykey "Hello world"
"OK"
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0
字符串类型的内部编码有 3 种:
• int:8 个字节的⻓整型。
• embstr:⼩于等于 39 个字节的字符串。
• raw:⼤于 39 个字节的字符串。
Redis 会根据当前值的类型和⻓度动态决定使⽤哪种内部编码实现。 整型类型⽰例如下:
127.0.0.1:6379> set key 6379
OK
127.0.0.1:6379> object encoding key
"int"
短字符串⽰例如下:
# ⼩于等于 39 个字节的字符串
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> object encoding key
"embstr"
⻓字符串⽰例如下:
# ⼤于 39 个字节的字符串
127.0.0.1:6379> set key "one string greater than 39 bytes ........"
OK
127.0.0.1:6379> object encoding key
"raw"
缓存(Cache)功能
下图为 Redis + MySQL 组成的缓存存储架构 ,是⽐较典型的缓存使⽤场景,其中 Redis 作为缓冲层,MySQL 作为存储层,绝⼤部分请 求的数据都是从 Redis 中获取。由于 Redis 具有⽀撑⾼并发的特性,所以缓存通常能起到加速读写和 降低后端压⼒的作⽤。
通过增加缓存功能,在理想情况下,每个⽤⼾信息,⼀个⼩时期间只会有⼀次 MySQL 查询,极⼤地提 升了查询效率,也降低了 MySQL 的访问数。
与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,⽽且也没有对键名 有强制要求(除了不能使⽤⼀些特殊字符)。但设计合理的键名,有利于防⽌键冲突和项⽬ 的可维护性,⽐较推荐的⽅式是使⽤ "业务名:对象名:唯⼀标识:属性" 作为键名。例如 MySQL 的数据库名为 vs,⽤⼾表名为 user_info,那么对应的键可以使⽤ "vs:user_info:6379"、"vs:user_info:6379:name" 来表⽰,如果当前 Redis 只会被⼀个业务 使⽤,可以省略业务名 "vs:"。如果键名过程,则可以使⽤团队内部都认同的缩写替代,例如"user:6379:friends:messages:5217" 可以被 "u:6379:fr:m:5217" 代替。毕竟键名过⻓,还 是会导致 Redis 的性能明显下降的。
计数(Counter)功能
许多应⽤都会使⽤ Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数 据可以异步处理或者落地到其他数据源。例如视频⽹站的视频播放次数可以使⽤ Redis 来完成:⽤⼾每播放⼀次视频,相应的视频播放数就会⾃增 1。
共享会话(Session)
⼀个分布式 Web 服务将⽤⼾的 Session 信息(例如⽤⼾登录信息)保存在各⾃ 的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,分布式服务会将⽤⼾的访问请求均衡到 不同的服务器上,并且通常⽆法保证⽤⼾每次请求都会被均衡到同⼀台服务器上,这样当⽤⼾刷新⼀ 次访问是可能会发现需要重新登录,这个问题是⽤⼾⽆法容忍的。
为了解决这个问题,可以使⽤ Redis 将⽤⼾的 Session 信息进⾏集中管理,在这种模式下,只要保证 Redis 是⾼可⽤和可扩展性的,⽆论⽤⼾被均衡到哪台 Web 服务器上,都集中从 Redis 中查询、更新 Session 信息。
⼿机验证码
很多应⽤出于安全考虑,会在每次进⾏登录时,让⽤⼾输⼊⼿机号并且配合给⼿机发送验证码, 然后让⽤⼾再次输⼊收到的验证码并进⾏验证,从⽽确定是否是⽤⼾本⼈。为了短信接⼝不会频繁访 问,会限制⽤⼾每分钟获取验证码的频率,例如⼀分钟不能超过 5 次
⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数 组、映射。在 Redis 中,哈希类型是指值本⾝⼜是⼀个键值对结构,形如 key = "key",value = { { field1, value1 }, ..., {fieldN, valueN } }
❗ 哈希类型中的映射关系通常称为 field-value,⽤于区分 Redis 整体的键值对(key-value), 注意这⾥的 value 是指 field 对应的值,不是键(key)对
HGET key field
应的值,请注意 value 在不同上下 ⽂的作⽤。
HSET ——设置 hash 中指定的字段(field)的值(value)。
语法:
HSET key field value [field value ...]
时间复杂度:插⼊⼀组 field 为 O(1), 插⼊ N 组 field 为 O(N)
返回值:添加的字段的个数。
⽰例:
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HGET myhash field1
"Hello"
HGET——获取 hash 中指定字段的值。
语法:
HGET key field
时间复杂度:O(1)
返回值:字段对应的值或者 nil。
⽰例:
redis> HSET myhash field1 "foo"
(integer) 1
redis> HGET myhash field1
"foo"
redis> HGET myhash field2
(nil)
HEXISTS ——判断 hash 中是否有指定的字段。
语法:
HEXISTS key field
时间复杂度:O(1)
返回值:1 表⽰存在,0 表⽰不存在。
⽰例:
redis> HSET myhash field1 "foo"
(integer) 1
redis> HEXISTS myhash field1
(integer) 1
redis> HEXISTS myhash field2
(integer) 0
HDEL ——删除 hash 中指定的字段。
语法:
HDEL key field [field ...]
时间复杂度:删除⼀个元素为 O(1). 删除 N 个元素为 O(N).
返回值:本次操作删除的字段个数。
⽰例:
redis> HSET myhash field1 "foo"
(integer) 1
redis> HDEL myhash field1
(integer) 1
redis> HDEL myhash field2
(integer) 0
HKEYS——获取 hash 中的所有字段。
语法:
HKEYS key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段列表。
⽰例
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HKEYS myhash
1) "field1"
2) "field2"
HVALS ——获取 hash 中的所有的值。
语法:
HVALS key
时间复杂度:O(N), N 为 field 的个数.
返回值:所有的值。
⽰例:
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"
HGETALL ——获取 hash 中的所有字段以及对应的值。
语法:
HGETALL key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段和对应的值。
⽰例:
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "World"
HMGET—— ⼀次获取 hash 中多个字段的值。
语法:
HMGET key field [field ...]
时间复杂度:只查询⼀个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.
返回值:字段对应的值或者 nil。
⽰例:
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HMGET myhash field1 field2 nofield
1) "Hello"
2) "World"
3) (nil)
在使⽤ HGETALL 时,如果哈希元素个数⽐较多,会存在阻塞 Redis 的可能。如果开发⼈员只 需要获取部分 field,可以使⽤ HMGET,如果⼀定要获取全部 field,可以尝试使⽤ HSCAN 命令,该命令采⽤渐进式遍历哈希类型,HSCAN 会在后续章节介绍。
HLEN ——获取 hash 中的所有字段的个数。
语法:
HLEN key
时间复杂度:O(1)
返回值:字段个数。
⽰例:
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HLEN myhash
(integer) 2
HSETNX—— 在字段不存在的情况下,设置 hash 中的字段和值。
语法:
HSETNX key field value
时间复杂度:O(1)
返回值:1 表⽰设置成功,0 表⽰失败。
⽰例:
redis> HSETNX myhash field "Hello"
(integer) 1
redis> HSETNX myhash field "World"
(integer) 0
redis> HGET myhash field
"Hello"
HINCRBY —— 将 hash 中字段对应的数值添加指定的值。
语法:
HINCRBY key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值。
⽰例:
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5
HINCRBYFLOAT HINCRBY 的浮点数版本。
语法:
HINCRBYFLOAT key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值。
⽰例:
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"
redis> HINCRBYFLOAT mykey field -5
"5.6"
redis> HSET mykey field 5.0e3
(integer) 0
redis> HINCRBYFLOAT mykey field 2.0e2
"5200"
哈希的内部编码有两种:
• ziplist(压缩列表):当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认 512 个)、 同时所有值都⼩于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使⽤ ziplist 作为哈 希的内部实现,ziplist 使⽤更加紧凑的结构实现多个元素的连续存储,所以在节省内存⽅⾯⽐ hashtable 更加优秀。
• hashtable(哈希表):当哈希类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ hashtable 作为哈希 的内部实现,因为此时 ziplist 的读写效率会下降,⽽ hashtable 的读写时间复杂度为 O(1)。
下⾯的⽰例演⽰了哈希类型的内部编码,以及响应的变化。
1)当 field 个数⽐较少且没有⼤的 value 时,内部编码为 ziplist:
127.0.0.1:6379> hmset hashkey f1 v1 f2 v2
OK
127.0.0.1:6379> object encoding hashkey
"ziplist"
2)当有 value ⼤于 64 字节时,内部编码会转换为 hashtable:
127.0.0.1:6379> hset hashkey f3 "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
3)当 field 个数超过 512 时,内部编码也会转换为 hashtable:
127.0.0.1:6379> hmset hashkey f1 v1 h2 v2 f3 v3 ... 省略 ... f513 v513
OK
127.0.0.1:6379> object encoding hashkey
"hashtable"
下图为关系型数据表记录的两条⽤⼾信息,⽤⼾的属性表现为表的列,每条⽤⼾信息表现为 ⾏。
相⽐于使⽤ JSON 格式的字符串缓存⽤⼾信息,哈希类型变得更加直观,并且在更新操作上变得 更灵活。可以将每个⽤⼾的 id 定义为键后缀,多对 field-value 对应⽤⼾的各个属性
但是需要注意的是哈希类型和关系型数据库有两点不同之处:
• 哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,⽽ 关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null
• 关系数据库可以做复杂的关系查询,⽽ Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等 基本不可能,维护成本⾼。
截⾄⽬前为⽌,我们已经能够⽤三种⽅法缓存⽤⼾信息,下⾯给出三种⽅案的实现⽅法和优缺点 分析。
1. 原⽣字符串类型⸺使⽤字符串类型,每个属性⼀个键。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。
缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种 ⽅案基本没有实⽤性。
2. 序列化字符串类型,例如 JSON 格式
set user:1 经过序列化后的⽤⼾对象字符串
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内 存的使⽤效率很⾼。
缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。
3. 哈希类型
hmset user:1 name James age 23 city Beijing
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。
列表类型是⽤来存储多个有序的字符串,如下图所⽰,a、b、c、d、e 五个元素从左到右组成 了⼀个有序的列表,列表中的每个字符串称为元素(element),⼀个列表最多可以存储 个元 素。在 Redis 中,可以对列表两端插⼊(push)和弹出(pop),还可以获取指定范围的元素列表、 获取指定索引下标的元素等。列表是⼀种⽐较灵活的数据结构,它可以 充当栈和队列的⻆⾊,在实际开发上有很多应⽤场景。
列表的获取、删除
列表类型的特点:
第⼀、列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表, 例如要获取上图的第 5 个元素,可以执⾏ lindex user:1:messages 4 或者倒数第 1 个元素,lindex user:1:messages -1 就可以得到元素 e。
第⼆、区分获取和删除的区别,例如上图中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删 除,这个操作会导致列表的⻓度从 5 变成 4;但是执⾏ lindex 4 只会获取元素,但列表⻓度是不会变化 的。
第三、列表中的元素是允许重复的,例如上图中的列表中是包含了两个 a 元素的。
上图列表中允许有重复元素
LPUSH —— 将⼀个或者多个元素从左侧放⼊(头插)到 list 中。
语法:
LPUSH key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。
⽰例:
redis> LPUSH mylist "world"
(integer) 1
redis> LPUSH mylist "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello"
2) "world"
LPUSHX —— 在 key 存在时,将⼀个或者多个元素从左侧放⼊(头插)到 list 中。不存在,直接返回
语法:
LPUSHX key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。
⽰例:
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSHX mylist "Hello"
(integer) 2
redis> LPUSHX myotherlist "Hello"
(integer) 0
redis> LRANGE mylist 0 -1
1) "Hello"
2) "World"
redis> LRANGE myotherlist 0 -1
(empty array)
RPUSH —— 将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。
语法:
RPUSH key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。
⽰例:
redis> RPUSH mylist "world"
(integer) 1
redis> RPUSH mylist "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "world"
2) "hello"
RPUSHX —— 在 key 存在时,将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。
语法:
RPUSHX key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。
⽰例:
redis> RPUSH mylist "World"
(integer) 1
redis> RPUSHX mylist "Hello"
(integer) 2
redis> RPUSHX myotherlist "Hello"
(integer) 0
redis> LRANGE mylist 0 -1
1) "World"
2) "Hello"
redis> LRANGE myotherlist 0 -1
(empty array)
LRANGE —— 获取从 start 到 end 区间的所有元素,左闭右闭。
语法:
LRANGE key start stop
时间复杂度:O(N)
返回值:指定区间的元素。
⽰例:
redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LRANGE mylist 0 0
1) "one"
redis> LRANGE mylist -3 2
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist -100 100
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 5 10
(empty array)
LPOP ——从 list 左侧取出元素(即头删)。
语法:
LPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
⽰例:
redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LPOP mylist
"one"
redis> LPOP mylist
"two"
redis> LPOP mylist
"three"
redis> LRANGE mylist 0 -1
1) "four"
2) "five"
RPOP —— 从 list 右侧取出元素(即尾删)。
语法:
RPOP key
时间复杂度:O(1)
返回值:取出的元素或者 nil。
⽰例:
redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> RPOP mylist
"five"
redis> LRANGE mylist 0 -1
1) "one"
2) "two"
3) "three"
4) "four"
LINDEX —— 获取从左数第 index 位置的元素。
语法:
LINDEX key index
时间复杂度:O(N)
返回值:取出的元素或者 nil。
⽰例:
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LINDEX mylist 0
"Hello"
redis> LINDEX mylist -1
"World"
redis> LINDEX mylist 3
(nil)
LINSERT —— 在特定位置插⼊元素。
语法:
LINSERT key pivot element
时间复杂度:O(N)
返回值:插⼊后的 list ⻓度。
⽰例:
redis> RPUSH mylist "Hello"
(integer) 1
redis> RPUSH mylist "World"
(integer) 2
redis> LINSERT mylist BEFORE "World" "There"
(integer) 3
redis> LRANGE mylist 0 -1
1) "Hello"
2) "There"
3) "World"
LLEN —— 获取 list ⻓度。
语法:
LLEN key
时间复杂度:O(1)
返回值:list 的⻓度。
⽰例:
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LLEN mylist
(integer) 2
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应⾮阻塞版本的作⽤基本⼀致,除了:
• 在列表中有元素的情况下,阻塞和⾮阻塞表现是⼀致的。但如果列表中没有元素,⾮阻塞版本会理 解返回 nil,但阻塞版本会根据 timeout,阻塞⼀段时间,期间 Redis 可以执⾏其他命令,但要求执 ⾏该命令的客⼾端会表现为阻塞状态(如图 2-22 所⽰)。
• 命令中如果设置了多个键,那么会从左向右进⾏遍历键,⼀旦有⼀个键对应的列表中可以弹出元 素,命令⽴即返回。
• 如果多个客⼾端同时多⼀个键执⾏ pop,则最先执⾏命令的客⼾端会得到弹出的元素。
BLPOP LPOP —— 的阻塞版本。
语法:
BLPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。
⽰例:
redis> EXISTS list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BLPOP list1 list2 0
1) "list1"
2) "a"
BRPOP RPOP —— 的阻塞版本。
语法:
BRPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。
⽰例:
redis> DEL list1 list2
(integer) 0
redis> RPUSH list1 a b c
(integer) 3
redis> BRPOP list1 list2 0
1) "list1"
2) "c"
有关列表的命令已经介绍完毕,这 是这些命令的作⽤和时间复杂度,开发⼈员可以参考。
列表类型的内部编码有两种:
• ziplist(压缩列表):当列表的元素个数⼩于 list-max-ziplist-entries 配置(默认 512 个),同时 列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选⽤ ziplist 来作为列表的内部编码实现来减少内存消耗。
• linkedlist(链表):当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内部实现。
1)当元素个数较少且没有⼤元素时,内部编码为 ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3
OK
127.0.0.1:6379> object encoding listkey
"ziplist"
2)当元素个数超过 512 时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
3)当某个元素的⻓度超过 64 字节时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey "one string is bigger than 64 bytes ... 省略 ..."
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
消息队列
如下图所⽰,Redis 可以使⽤ lpush + brpop 命令组合实现经典的阻塞式⽣产者-消费者模型队列, ⽣产者客⼾端使⽤ lpush 从列表左侧插⼊元素,多个消费者客⼾端使⽤ brpop 命令阻塞式地从队列中 "争抢" 队⾸元素。通过多个客⼾端来保证消费的负载均衡和⾼可⽤性。
分频道的消息队列
如图 所⽰,Redis 同样使⽤ lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费 者可以通过 brpop 不同的键值,实现订阅不同频道的理念。
集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中
1)元素之间是⽆序 的
2)元素不允许重复,如图所⽰。⼀个集合中最多可以存储 个元素。
Redis 除了⽀持 集合内的增删查改操作,同时还⽀持多个集合取交集、并集、差集,合理地使⽤好集合类型,能在实 际开发中解决很多问题。
SADD —— 将⼀个或者多个元素添加到 set 中。
注意,重复的元素⽆法添加到 set 中。
语法:
SADD key member [member ...]
时间复杂度:O(1)
返回值:本次添加成功的元素个数。
⽰例:
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SADD myset "World"
(integer) 0
redis> SMEMBERS myset
1) "Hello"
2) "World"
SMEMBERS —— 获取⼀个 set 中的所有元素
注意,元素间的顺序是⽆序的。
语法:
SMEMBERS key
时间复杂度:O(N)
返回值:所有元素的列表。
⽰例:
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SMEMBERS myset
1) "Hello"
2) "World"
SISMEMBER —— 判断⼀个元素在不在 set 中。
语法:
SISMEMBER key member
时间复杂度:O(1)
返回值:1 表⽰元素在 set 中。0 表⽰元素不在 set 中或者 key 不存在。
⽰例:
redis> SADD myset "one"
(integer) 1
redis> SISMEMBER myset "one"
(integer) 1
redis> SISMEMBER myset "two"
(integer) 0
SCARD —— 获取⼀个 set 的基数(cardinality),即 set 中的元素个数。
语法:
SCARD key
时间复杂度:O(1)
返回值:set 内的元素个数。
⽰例:
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SCARD myset
(integer) 2
SPOP —— 从 set 中删除并返回⼀个或者多个元素。
注意,由于 set 内的元素是⽆序的,所以取出哪个元素实际是 未定义⾏为,即可以看作随机的。
语法:
SPOP key [count]
时间复杂度:O(N), n 是 count
返回值:取出的元素。
示例:
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myset "three"
(integer) 1
redis> SPOP myset
"one"
redis> SMEMBERS myset
1) "three"
2) "two"
redis> SADD myset "four"
(integer) 1
redis> SADD myset "five"
(integer) 1
redis> SPOP myset 3
1) "three"
2) "four"
3) "two"
redis> SMEMBERS myset
1) "five"
SMOVE —— 将⼀个元素从源 set 取出并放⼊⽬标 set 中。
语法:
SMOVE source destination member
时间复杂度:O(1)
返回值:1 表⽰移动成功,0 表⽰失败。
⽰例:
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myotherset "three"
(integer) 1
redis> SMOVE myset myotherset "two"
(integer) 1
redis> SMEMBERS myset
1) "one"
redis> SMEMBERS myotherset
1) "three"
2) "two"
SREM —— 将指定的元素从 set 中删除。
语法:
SREM key member [member ...]
时间复杂度:O(N), N 是要删除的元素个数.
返回值:本次操作删除的元素个数。
⽰例:
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myset "three"
(integer) 1
redis> SREM myset "one"
(integer) 1
redis> SREM myset "four"
(integer) 0
redis> SMEMBERS myset
1) "three"
2) "two"
交集(inter)、并集(union)、差集(diff)的概念如下图所⽰。
SINTER —— 获取给定 set 的交集中的元素。
语法:
SINTER key [key ...]
时间复杂度:O(N * M), N 是最⼩的集合元素个数. M 是最⼤的集合元素个数.
返回值:交集的元素。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SINTER key1 key2
1) "c"
SINTERSTORE —— 获取给定 set 的交集中的元素并保存到⽬标 set 中。
语法:
SINTERSTORE destination key [key ...]
时间复杂度:O(N * M), N 是最⼩的集合元素个数. M 是最⼤的集合元素个数.
返回值:交集的元素个数。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SINTERSTORE key key1 key2
(integer) 1
redis> SMEMBERS key
1) "c"
SUNION —— 获取给定 set 的并集中的元素。
语法:
SUNION key [key ...]
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SUNION key1 key2
1) "a"
2) "c"
3) "e"
4) "b"
5) "d"
SUNIONSTORE —— 获取给定 set 的并集中的元素并保存到⽬标 set 中。
语法:
SUNIONSTORE destination key [key ...]
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素个数。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SUNIONSTORE key key1 key2
(integer) 5
redis> SMEMBERS key
1) "a"
2) "c"
3) "e"
4) "b"
5) "d"
SDIFF —— 获取给定 set 的差集中的元素。
语法:
SDIFF key [key ...]
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SDIFF key1 key2
1) "a"
2) "b"
SDIFFSTORE —— 获取给定 set 的差集中的元素并保存到⽬标 set 中。
语法:
SDIFFSTORE destination key [key ...]
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素个数。
⽰例:
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SDIFFSTORE key key1 key2
(integer) 2
redis> SMEMBERS key
1) "a"
2) "b"
总结了集合类型的常⻅命令,开发⼈员可以根据⾃⾝需求进⾏选择。
集合类型的内部编码有两种:
• intset(整数集合):当集合中的元素都是整数并且元素的个数⼩于 set-max-intset-entries 配置 (默认 512 个)时,Redis 会选⽤ intset 来作为集合的内部实现,从⽽减少内存的使⽤。
• hashtable(哈希表):当集合类型⽆法满⾜ intset 的条件时,Redis 会使⽤ hashtable 作为集合 的内部实现。
1)当元素个数较少并且都为整数时,内部编码为 intset:
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"
2)当元素个数超过 512 个,内部编码为 hashtable:
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 513
127.0.0.1:6379> object encoding setkey
"hashtable"
3)当存在元素不是整数时,内部编码为 hashtable:
127.0.0.1:6379> sadd setkey a
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"
集合类型⽐较典型的使⽤场景是标签(tag)。例如 A ⽤⼾对娱乐、体育板块⽐较感兴趣,B ⽤⼾ 对历史、新闻⽐较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同⼀个标签 的⼈,以及⽤⼾的共同喜好的标签,这些数据对于增强⽤⼾体验和⽤⼾黏度都⾮常有帮助。 例如⼀个 电⼦商务⽹站会对不同标签的⽤⼾做不同的产品推荐。 下⾯的演⽰通过集合类型来实现标签的若⼲功能。
1)给⽤⼾添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
2)给标签添加⽤⼾
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:4 user:9 user:28
3)删除⽤⼾下的标签
srem user:1:tags tag1 tag5
...
4)删除标签下的⽤⼾
srem tag1:users user:1
srem tag5:users user:1
...
5)计算⽤⼾的共同兴趣标签
sinter user:1:tags user:2:tags
有序集合相对于字符串、列表、哈希、集合来说会有⼀些陌⽣。它保留了集合不能有重复成员的 特点,但与集合不同的是,有序集合中的每个元素都有⼀个唯⼀的浮点类型的分数(score)与之关 联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是⽤下标作为排序依据⽽是⽤这个 分数。
该有序集合显⽰了三国中的武将的武⼒。
有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利⽤有序集合,可 以帮助我们在实际开发中解决很多问题。
有序集合中的元素是不能重复的,但分数允许重复。类⽐于⼀次考试之后,每个⼈⼀定有⼀ 个唯⼀的分数,但分数允许相同。
ZADD —— 添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负 极限也是合法的。
ZADD 的相关选项:
• XX:仅仅⽤于更新已经存在的元素,不会添加新元素。
• NX:仅⽤于添加新元素,不会更新已经存在的元素。
• CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更 新的元素的个数。
• INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和 分数。
语法:
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member
...]
时间复杂度:O(log(N))
返回值:本次添加成功的元素个数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 1 "uno"
(integer) 1
redis> ZADD myzset 2 "two" 3 "three"
(integer) 2
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "uno"
4) "1"
5) "two"
6) "2"
7) "three"
8) "3"
redis> ZADD myzset 10 one 20 two 30 three
(integer) 0
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "uno"
2) "1"
3) "one"
4) "10"
5) "two"
6) "20"
7) "three"
8) "30"
redis> ZADD myzset CH 100 one 200 two 300 three
(integer) 3
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "uno"
2) "1"
3) "one"
4) "100"
5) "two"
6) "200"
7) "three"
8) "300"
redis> ZADD myzset XX 1 one 2 two 3 three 4 four 5 five
(integer) 0
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "uno"
4) "1"
5) "two"
6) "2"
7) "three"
8) "3"
redis> ZADD myzset NX 100 one 200 two 300 three 400 four 500 five
(integer) 2
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "uno"
4) "1"
5) "two"
6) "2"
7) "three"
8) "3"
9) "four"
10) "400"
11) "five"
12) "500"
redis> ZADD myzset INCR 10 one
"11"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "uno"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
7) "one"
8) "11"
9) "four"
10) "400"
11) "five"
12) "500"
redis> ZADD myzset -inf "negative infinity" +inf "positive infinity"
(integer) 2
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "negative infinity"
2) "-inf"
3) "uno"
4) "1"
5) "two"
6) "2"
7) "three"
8) "3"
9) "one"
10) "11"
11) "four"
12) "400"
13) "five"
14) "500"
15) "positive infinity"
16) "inf"
ZCARD 获取⼀个 zset 的基数(cardinality),即 zset 中的元素个数。 语法:
ZCARD key
时间复杂度:O(1) 返回值:zset 内的元素个数。 ⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZCARD myzset
(integer) 2
ZCOUNT —— 返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
语法:
ZCOUNT key min max
时间复杂度:O(log(N))
返回值:满⾜条件的元素列表个数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZCOUNT myzset -inf +inf
(integer) 3
redis> ZCOUNT myzset 1 3
(integer) 3
redis> ZCOUNT myzset (1 3
(integer) 2
redis> ZCOUNT myzset (1 (3
(integer) 1
ZRANGE —— 返回指定区间⾥的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。
语法:
ZRANGE key start stop [WITHSCORES]
此处的 [start, stop] 为下标构成的区间. 从 0 开始, ⽀持负数.
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "two"
4) "2"
5) "three"
6) "3"
redis> ZRANGE myzset 0 -1
1) "one"
2) "two"
3) "three"
redis> ZRANGE myzset 2 3
1) "three"
redis> ZRANGE myzset -2 -1
1) "two"
2) "three"
ZREVRANGE —— 返回指定区间⾥的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:
ZREVRANGE key start stop [WITHSCORES]
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANGE myzset 0 -1 WITHSCORES
1) "three"
2) "3"
3) "two"
4) "2"
5) "one"
6) "1"
redis> ZREVRANGE myzset 0 -1
1) "three"
2) "two"
3) "one"
redis> ZREVRANGE myzset 2 3
1) "one"
redis> ZREVRANGE myzset -2 -1
1) "two"
2) "one"
ZRANGEBYSCORE —— 返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:
ZRANGEBYSCORE key min max [WITHSCORES]
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZRANGEBYSCORE myzset -inf +inf
1) "one"
2) "two"
3) "three"
redis> ZRANGEBYSCORE myzset 1 2
1) "one"
2) "two"
redis> ZRANGEBYSCORE myzset (1 2
1) "two"
redis> ZRANGEBYSCORE myzset (1 (2
(empty array)
ZPOPMAX —— 删除并返回分数最⾼的 count 个元素。
语法:
ZPOPMAX key [count]
时间复杂度:O(log(N) * M)
返回值:分数和元素列表。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZPOPMAX myzset
1) "three"
2) "3"
BZPOPMAX—— ZPOPMAX 的阻塞版本。
语法:
BZPOPMAX key [key ...] timeout
时间复杂度:O(log(N))
返回值:元素列表。
⽰例:
redis> DEL zset1 zset2
(integer) 0
redis> ZADD zset1 0 a 1 b 2 c
(integer) 3
redis> BZPOPMAX zset1 zset2 0
1) "zset1"
2) "c"
3) "2"
ZPOPMIN —— 删除并返回分数最低的 count 个元素。
语法:
ZPOPMIN key [count]
时间复杂度:O(log(N) * M)
返回值:分数和元素列表。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZPOPMIN myzset
1) "one"
2) "1"
BZPOPMIN —— ZPOPMIN 的阻塞版本。
语法:
BZPOPMIN key [key ...] timeout
命令有效版本:5.0.0 之后 时间复杂度:O(log(N))
返回值:元素列表。
⽰例:
redis> DEL zset1 zset2
(integer) 0
redis> ZADD zset1 0 a 1 b 2 c
(integer) 3
redis> BZPOPMIN zset1 zset2 0
1) "zset1"
2) "a"
3) "0"
ZRANK —— 返回指定元素的排名,升序。
语法:
ZRANK key member
时间复杂度:O(log(N))
返回值:排名。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZRANK myzset "three"
(integer) 2
redis> ZRANK myzset "four"
(nil)
ZREVRANK —— 返回指定元素的排名,降序。
语法:
ZREVRANK key member
时间复杂度:O(log(N))
返回值:排名。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANK myzset "one"
(integer) 2
redis> ZREVRANK myzset "four"
(nil)
ZSCORE —— 返回指定元素的分数。
语法:
ZSCORE key member
时间复杂度:O(1)
返回值:分数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZSCORE myzset "one"
"1"
ZREM —— 删除指定的元素。
语法:
ZREM key member [member ...]
时间复杂度:O(M*log(N))
返回值:本次操作删除的元素个数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREM myzset "two"
(integer) 1
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "three"
4) "3"
ZREMRANGEBYRANK ——按照排序,升序删除指定范围的元素,左闭右闭。
语法:
ZREMRANGEBYRANK key start stop
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREMRANGEBYRANK myzset 0 1
(integer) 2
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "three"
2) "3"
ZREMRANGEBYSCORE —— 按照分数删除指定范围的元素,左闭右闭。
语法:
ZREMRANGEBYSCORE key min max
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREMRANGEBYSCORE myzset -inf (2
(integer) 1
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "two"
2) "2"
3) "three"
4) "3"
ZINCRBY —— 为指定的元素的关联分数添加指定的分数值。
语法:
ZINCRBY key increment member
时间复杂度:O(log(N))
返回值:增加后元素的分数。
⽰例:
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZINCRBY myzset 2 "one"
"3"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "two"
2) "2"
3) "one"
4) "3"
ZINTERSTORE —— 求出给定有序集合中元素的交集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元素对应的分数按照不同的聚合⽅式和权重得到新的分数。
语法:
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE ]
时间复杂度:O(N*K)+O(M*log(M)) N 是输⼊的有序集合中, 最⼩的有序集合的元素个数; K 是输⼊了 ⼏个有序集合; M 是最终结果的有序集合的元素个数.
返回值:⽬标集合中的元素个数
⽰例:
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZADD zset2 3 "three"
(integer) 1
redis> ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3
(integer) 2
redis> ZRANGE out 0 -1 WITHSCORES
1) "one"
2) "5"
3) "two"
4) "10"
ZUNIONSTORE —— 求出给定有序集合中元素的并集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元 素对应的分数按照不同的聚合⽅式和权重得到新的分数。
语法:
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE ]
时间复杂度:O(N)+O(M*log(M)) N 是输⼊的有序集合总的元素个数; M 是最终结果的有序集合的元素 个数.
返回值:⽬标集合中的元素个数
⽰例:
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZADD zset2 3 "three"
(integer) 1
redis> ZUNIONSTORE out 2 zset1 zset2 WEIGHTS 2 3
(integer) 3
redis> ZRANGE out 0 -1 WITHSCORES
1) "one"
2) "5"
3) "three"
4) "9"
5) "two"
6) "10"
有序集合类型的内部编码有两种:
• ziplist(压缩列表):当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个), 同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作 为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。
• skiplist(跳表):当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。
1)当元素个数较少且每个元素较⼩时,内部编码为 ziplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3
(integer) 3
127.0.0.1:6379> object encoding zsetkey
"ziplist"
2)当元素个数超过 128 个,内部编码 skiplist:
127.0.0.1:6379> zadd zsetkey 50 e1 60 e2 30 e3 ... 省略 ... 82 e129
(integer) 129
127.0.0.1:6379> object encoding zsetkey
"skiplist"
3)当某个元素⼤于 64 字节时,内部编码 skiplist:
127.0.0.1:6379> zadd zsetkey 50 "one string bigger than 64 bytes ... 省略 ..."
(integer) 1
127.0.0.1:6379> object encoding zsetkey
"skiplist"
有序集合⽐较典型的使⽤场景就是排⾏榜系统。例如常⻅的⽹站上的热榜信息,榜单的维度可能 是多⽅⾯的:按照时间、按照阅读量、按照点赞量。本例中我们使⽤点赞数这个维度,维护每天的热榜:
1)添加⽤⼾赞数 例如⽤⼾ james 发布了⼀篇⽂章,并获得 3 个赞,可以使⽤有序集合的 zadd 和 zincrby 功能:
zadd user:ranking:2022-03-15 3 james
之后如果再获得赞,可以使⽤ zincrby:
zincrby user:ranking:2022-03-15 1 james
2)取消⽤⼾赞数 由于各种原因(例如⽤⼾注销、⽤⼾作弊等)需要将⽤⼾删除,此时需要将⽤⼾从榜单中删除掉,可 以使⽤ zrem。例如删除成员 tom:
zrem user:ranking:2022-03-15 tom
3)展⽰获取赞数最多的 10 个⽤⼾ 此功能使⽤ zrevrange 命令实现:
zrevrangebyrank user:ranking:2022-03-15 0 9
4)展⽰⽤⼾信息以及⽤⼾分数 次功能将⽤⼾名作为键后缀,将⽤⼾信息保存在哈希类型中,⾄于⽤⼾的分数和排名可以使⽤ zscore 和 zrank 来实现。
hgetall user:info:tom
zscore user:ranking:2022-03-15 mike
zrank user:ranking:2022-03-15 mike
Redis 使⽤ scan 命令进⾏渐进式遍历键,进⽽解决直接使⽤ keys 获取键时可能出现的阻塞问 题。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执⾏多次 scan。整 个过程如图 所⽰。
• ⾸次 scan 从 0 开始.
• 当 scan 返回的下次位置为 0 时, 遍历结束. SCAN 以渐进式的⽅式进⾏键的遍历。
语法:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
时间复杂度:O(1)
返回值:下⼀次 scan 的游标(cursor)以及本次得到的键。
⽰例:
redis 127.0.0.1:6379> scan 0
1) "17"
2) 1) "key:12"
2) "key:8"
3) "key:4"
4) "key:14"
5) "key:16"
6) "key:17"
7) "key:15"
8) "key:10"
9) "key:3"
10) "key:7"
11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
2) "key:18"
3) "key:0"
4) "key:2"
5) "key:19"
6) "key:13"
7) "key:6"
8) "key:9"
9) "key:11"
除了 scan 以外,Redis ⾯向哈希类型、集合类型、有序集合类型分别提供了 hscan、sscan、zscan 命 令,它们的⽤法和 scan 基本类似,感兴趣的读者可以⾃⾏做扩展学习。
❗ 渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删 除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。
Redis 提供了⼏个⾯向 Redis 数据库的操作,分别是 dbsize、select、flushdb、flushall 命令, 本机将通过具体的使⽤常⻅介绍这些命令。
select dbIndex
许多关系型数据库,例如 MySQL ⽀持在⼀个实例下有多个数据库存在的,但是与关系型数据库⽤ 字符来区分不同数据库名不同,Redis 只是⽤数字作为多个数据库的实现。Redis 默认配置中是有 16 个数据库。select 0 操作会切换到第⼀个数据库,select 15 会切换到最后⼀个数据库。0 号数据库和 15 号数据库保存的数据是完全不冲突的(如图所⽰),即各种有各⾃的键值对。默认情况下,我 们处于数据库 0。
❗ Redis 中虽然⽀持多数据库,但随着版本的升级,其实不是特别建议使⽤多数据库特性。如 果真的需要完全隔离的两套键值对,更好的做法是维护多个 Redis 实例,⽽不是在⼀个 Redis 实例中维护多数据库。这是因为本⾝ Redis 并没有为多数据库提供太多的特性,其次 ⽆论是否有多个数据库,Redis 都是使⽤单线程模型,所以彼此之间还是需要排队等待命令 的执⾏。同时多数据库还会让开发、调试和运维⼯作变得复杂。所以实践中,始终使⽤数据 库 0 其实是⼀个很好的选择。
flushdb / flushall 命令⽤于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数 据库。
❗ 永远不要在线上环境执⾏清除数据的操作,除⾮你想体验⼀把 “从删库到跑路” 的操作。
1. Redis 提供了 5 种数据结构类型,每种数据结构都可以对应不同的内部编码。
2. 纯内存操作、IO 多路复⽤、单线程模型造就了 Redis 的⾼性能。
3. 由于 Redis 的单线程模型,所以命令都是排队执⾏的,如果⼀个命令发⽣阻塞,会导致后续所有命 令都⽆法执⾏。
4. 了解常⻅命令的功能、时间复杂度有助于在实际中更好地使⽤ Redis。