官方文档 RedisCommands
1)Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。
2)Redis 不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对 Redis 本身或者应用本身造成致命伤害。
基本全局命令
Redis 有 5 种数据结构,但它们都是键值对种的值,对于键来说有一些通用的命令。
KEYS
返回所有满足样式(pattern)的 key。支持如下统配样式。
• h?llo 匹配 hello , hallo 和 hxllo (匹配任意一个字符)
• h*llo 匹配 hllo 和heeeello (匹配0个或多个字符)
• h[ae]llo 匹配hello 和hallo 但不匹配 hillo (只匹配a字符和e字符)
• h[^e]llo 匹配hallo , hbllo , ... 但不匹配 hello (只匹配非e字符的其他字符)
• h[a-b]llo 匹配hallo 和 hbllo (匹配a-b之间的所有字符)
语法:
KEYS pattern
命令有效版本:1.0.0 之后
时间复杂度:O(N)
返回值:匹配 pattern 的所有 key。
EXISTS
判断某个 key 是否存在。
语法:
EXISTS key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:key 存在的个数。
DEL
删除指定的 key。
语法:
DEL key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:删除掉的 key 的个数。
EXPIRE
为指定的 key 添加秒级的过期时间(Time To Live TTL)
语法:
EXPIRE key seconds
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:1 表示设置成功。0 表示设置失败。
TTL
获取指定 key 的过期时间,秒级。
TTL key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:剩余过期时间。-1 表示没有关联过期时间,-2 表示 key 不存在。
💡 EXPIRE 和 TTL 命令都有对应的支持毫秒为单位的版本:PEXPIRE 和 PTTL
键的过期机制
TYPE
返回 key 对应的数据类型。
语法:
TYPE key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值: none , string , list , set , zset , hash and stream .。
数据结构和内部编码
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构。
实际上 Redis 针对每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样 Redis 会在合适的场景选择合适的内部编码
数据结构 | 内部编码 |
string | raw(最基本的字符串) int(计数) embstr(针对段字符串进行的特殊优化) |
hash | hashtable(哈希表) ziplist(压缩列表,在哈希表里面元素比较少的时候,可能就优化成ziplist) |
list | linkedlist(链表) ziplist(压缩列表) |
set | hashtable(哈希表) intset(集合中存的都是整数) |
zset | skiplist(跳表) ziplist(压缩列表) |
可以看到每种数据结构都有至少两种以上的内部编码实现,例如 list 数据结构包含了 linkedlist 和ziplist 两种内部编码。同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现,可以通过 object encoding 命令查询内部编码:
String字符串
字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:
1)首先 Redis 中所有的键的类型都是字符串类型,而且其他几种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。
2)字符串类型的值实际可以是字符串,包含一般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚至是二进制流数据,例如图片、音频、视频等。不过一个字符串的最大值不能超过 512 MB。
由于 Redis 内部存储字符串完全是按照二进制流的形式保存的,所以 Redis 是不处理字符集编码问题的,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码
典型使用场景
缓存(Cache)功能
Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
Redis + MySQL 组成的缓存存储架构
- 假设业务是根据用户uid获取用户信息
UserInfo getUserInfo(long uid)
- 首先从 Redis 获取用户信息,我们假设用户信息保存在 "user:info:<uid>" 对应的键中:
// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执行命令:get key;
// 如果缓存命中(hit)
if (value != null) {
// 假设我们的用户信息按照 JSON 格式存储
UserInfo userInfo = JSON 反序列化(value);
return userInfo;
}
- 如果没有从 Redis 中得到用户信息,及缓存 miss,则进一步从 MySQL 中获取对应的信息,随后写入缓存并返回:
// 如果缓存未命中(miss)
if (value == null) {
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid =
<uid>
// 如果表中没有 uid 对应的用户信息
if (userInfo == null) {
响应 404
return null;
}
// 将用户信息序列化成 JSON 格式
String value = JSON 序列化(userInfo);
// 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
Redis 执行命令:set key value ex 3600
// 返回用户信息
return userInfo;
}
通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次 MySQL 查询,极大地提升了查询效率,也降低了 MySQL 的访问数。
计数(Counter)功能
许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源。例如视频网站的视频播放次数可以使用Redis 来完成:用户每播放一次视频,相应的视频播放数就会自增 1。
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
key = "video:" + vid;
long count = Redis 执行命令:incr key
return counter;
}
// 实际中要开发一个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按
// 照不同维度计数、避免单点问题、数据持久化到底层数据源等。
共享会话(Session)
一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。
Session 分散存储
为了解决这个问题,可以使用 Redis 将用户的 Session 信息进行集中管理,如图 2-13 所示,在这种模式下,只要保证 Redis 是高可用和可扩展性的,无论用户被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。
Redis 集中管理 Session
手机验证码
很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过 5 次。
短信验证码
此功能可以用以下伪代码说明基本实现思路:
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使用 NX,只在不存在 key 时才能设置成功
bool r = Redis 执行命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该手机的验证码了
long c = Redis 执行命令:incr key
if (c > 5) {
// 说明超过了一分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
String validationCode = 生成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执行命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过手机短信发送给用户
return validationCode ;
}
// 验证用户输入的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执行命令:get validationKey;
if (value == null) {
// 说明没有这个手机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
常见命令
SET
将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,无论原来的数据类型是什么。之前关于此 key 的 TTL (获取指定key的过期时间)也全部失效。
SET key value [expiration EX seconds|PX milliseconds] 1 [NX|XX]
set key value ex 10 相当于 set key value expire key 10
// 但是单个命令是原子的,保证这条语句是在同一时间执行的,分开写不能保证语句是同一时间执行的
命令有效版本:1.0.0 之后
时间复杂度:O(1)
SET 命令支持多种选项来影响它的行为:
• EX seconds —— 使用秒作为单位设置 key 的过期时间。
• PX milliseconds —— 使用毫秒作为单位设置 key 的过期时间。
• NX —— 只在 key 不存在时才进行设置,即如果 key 之前已经存在,设置不执行。
• XX —— 只在 key 存在时才进行设置,即如果 key 之前不存在,设置不执行。
注意:由于带选项的 SET 命令可以被 SETNX 、SETEX 、PSETEX 等命令代替,所以之后的版本中,Redis 可能进行合并。
返回值:
• 如果设置成功,返回 OK。
• 如果由于 SET 指定了 NX 或者 XX 但条件不满足,SET 不会执行,并返回 (nil)。
GET
获取 key 对应的 value。如果 key 不存在,返回 nil。如果 value 的数据类型不是 string,会报错。
语法:
GET key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:key 对应的 value,或者 nil 当 key 不存在。
MGET和MSET
MGET 一次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。
MSET 一次性设置多个 key 的值。
MGET key [key ...]
MSET key value [key value ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N) N 是 key 数量
MGET 返回值:对应 value 的列表
MSET返回值:永远是 OK
多次get和单词mget
使用 mget / mset 由于可以有效地减少了网络时间,所以性能相较更高。会使用批量操作,可以有效提高业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是无节制的,否则可能造成单一命令执行时间过长,导致 Redis 阻塞。
SETNX
设置 key-value 但只允许在 key 之前不存在的情况下。
SETNX key value
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:1 表示设置成功。0 表示没有设置。
计数命令
INCR
将 key 对应的 string 表示的数字加一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。
INCR key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:integer 类型的加完后的数值。
INCRBY
将 key 对应的 string 表示的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。
INCRBY key decrement
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:integer 类型的加完后的数值。
DECR
将 key 对应的 string 表示的数字减一。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。
DECR key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:integer 类型的减完后的数值。
DECYBY
将 key 对应的 string 表示的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是一个整型或者范围超过了 64 位有符号整型,则报错。/dmk
DECRBY key decrement
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:integer 类型的减完后的数值。
INCRBYFLOAT
将 key 对应的 string 表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是一个浮点数,则报错。允许采用科学计数法表示浮点数。
INCRBYFLOAT key increment
命令有效版本:2.6.0 之后
时间复杂度:O(1)
返回值:加/减完后的数值。
很多存储系统和编程语言内部使用 CAS 机制实现计数功能,会有一定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执行。
其他命令
APPEND
如果 key 已经存在并且是一个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。
APPEND KEY VALUE
命令有效版本:2.0.0 之后
时间复杂度:O(1). 追加的字符串一般长度较短, 可以视为 O(1).
返回值:追加完成之后 string 的长度。
GETRANGE
返回 key 对应的 string 的子串,由 start 和 end 确定(左闭右闭)。可以使用负数表示倒数。-1 代表倒数第一个字符,-2 代表倒数第二个,其他的与此类似。超过范围的偏移量会根据 string 的长度调整成正确的值。
GETRANGE key start end
命令有效版本:2.4.0 之后
时间复杂度:O(N). N 为 [start, end] 区间的长度. 由于 string 通常比较短, 可以视为是 O(1)
返回值:string 类型的子串
SETRANGE
覆盖字符串的一部分,从指定的偏移开始。
SETRANGE key offset value
命令有效版本:2.2.0 之后
时间复杂度:O(N), N 为 value 的长度. 由于一般给的 value 比较短, 通常视为 O(1).
返回值:替换后的 string 的长度。
STRLEN
获取 key 对应的 string 的长度。当 key 存放的类似不是 string 时,报错。
STRLEN key
命令有效版本:2.2.0 之后
时间复杂度:O(1)
返回值:string 的长度。或者当 key 不存在时,返回 0。
字符串类型命令
Hash哈希
几乎所有的主流编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组、映射。在 Redis 中,哈希类型是指值本身又是一个键值对结构,形如 key = "key",value = { {field1, value1 }, ..., {fieldN, valueN } }
字符串和哈希类型对比
哈希类型中的映射关系通常称为 field-value,用于区分 Redis 整体的键值对(key-value),注意这里的 value 是指 field 对应的值,不是键(key)对应的值,请注意 value 在不同上下文的作用。
命令
HSET/HGET
HSET key field value [field value ...]
HGET key field
HEXISTS
判断 hash 中是否有指定的字段。
HEXISTS key field
命令有效版本:2.0.0 之后
时间复杂度:O(1)
返回值:1 表示存在,0 表示不存在。
HDEL
删除 hash 中指定的字段。
HDEL key field [field ...]
命令有效版本:2.0.0 之后
时间复杂度:删除一个元素为 O(1). 删除 N 个元素为 O(N).
返回值:本次操作删除的字段个数。
HKEYS
获取hash中的所有字段
HKEYS key
命令有效版本:2.0.0 之后
时间复杂度:O(N), N 为 field 的个数.
返回值:字段列表。
HVALS
获取hash中所有的值
HVALS key
命令有效版本:2.0.0 之后
时间复杂度:O(N), N 为 field 的个数.
返回值:所有的值。
HGETALL
获取hash中所有字段以及对应的值
HGETALL key
命令有效版本:2.0.0 之后
时间复杂度:O(N), N 为 field 的个数.
返回值:字段和对应的值。
HMGET
一次获取hash中多个字段
HMGET key field [field ...]
命令有效版本:2.0.0 之后
时间复杂度:只查询一个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.
返回值:字段对应的值或者 nil。
在使用 HGETALL 时,如果哈希元素个数比较多,会存在阻塞 Redis 的可能。如果开发人员只需要获取部分 field,可以使用 HMGET,如果一定要获取全部 field,可以尝试使用 HSCAN命令,该命令采用渐进式遍历哈希类型
HLEN
获取hash中所有字段的个数
HLEN key
命令有效版本:2.0.0 之后
时间复杂度:O(1)
返回值:字段个数。
HSETNX
在字段不存在的情况下,设置 hash 中的字段和值。
HSETNX key field value
命令有效版本:2.0.0 之后
时间复杂度:O(1)
返回值:1 表示设置成功,0 表示失败。
HINCRBY
将 hash 中字段对应的数值添加指定的值。
HINCRBY key field increment
命令有效版本:2.0.0 之后
时间复杂度:O(1)
返回值:该字段变化之后的值。
HINCRBYFLOAT
HINCRBY 的浮点数版本。
HINCRBYFLOAT key field increment
命令有效版本:2.6.0 之后
时间复杂度:O(1)
返回值:该字段变化之后的值。
内部编码
哈希的内部编码有两种:
• ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable 更加优秀。
• hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(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"
- 当有 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"
- 当 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 对应用户的各个属性,类似如下伪代码:
UserInfo getUserInfo(long uid) {
// 根据 uid 得到 Redis 的键
String key = "user:" + uid;
// 尝试从 Redis 中获取对应的值
userInfoMap = Redis 执行命令:hgetall key;
// 如果缓存命中(hit)
if (value != null) {
// 将映射关系还原为对象形式
UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
return userInfo;
}
// 如果缓存未命中(miss)
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid = <uid>
// 如果表中没有 uid 对应的用户信息
if (userInfo == null) {
响应 404
return null;
}
// 将缓存以哈希类型保存
Redis 执行命令:hmset key name userInfo.name age userInfo.age city
userInfo.city
// 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
Redis 执行命令:expire key 3600
// 返回用户信息
return userInfo;
}
但是需要注意的是哈希类型和关系型数据库有两点不同之处:
• 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null,如图 2-18 所示。
• 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。
List列表
列表类型是用来存储多个有序的字符串,a、b、c、d、e 五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储 个元素。在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
命令
LPUSH/RPUSH
将一个或者多个元素从左侧放入(头插)到 list 中。
LPUSH key element [element ...]
RPUSH key element [element ...]
命令有效版本:1.0.0 之后
时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
返回值:插入后 list 的长度。
LPUSHX/RPUSHX
在 key 存在时,将一个或者多个元素从左/右侧侧放入(头/尾插)到 list 中。不存在,直接返回
LPUSHX key element [element ...]
RPUSHX key element [element ...]
命令有效版本:2.0.0 之后
时间复杂度:只插入一个元素为 O(1), 插入多个元素为 O(N), N 为插入元素个数.
返回值:插入后 list 的长度。
LRANGE
获取从 start 到 end 区间的所有元素,左闭右闭。
LRANGE key start stop
命令有效版本:1.0.0 之后
时间复杂度:O(N)
返回值:指定区间的元素。
L/RPOP
从 list 左/右侧取出元素(即头/尾删)。
LPOP key
RPOP key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:取出的元素或者 nil
LINDEX
获取从左数第 index 位置的元素。
LINDEX key index
命令有效版本:1.0.0 之后
时间复杂度:O(N)
返回值:取出的元素或者 nil。
LINSERT
在特定位置插入元素。
LINSERT key <BEFORE | AFTER> 1 pivot element
命令有效版本:2.2.0 之后
时间复杂度:O(N)
返回值:插入后的 list 长度。
LLEN
获取 list 长度。
LLEN key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:list 的长度。
阻塞版本命令
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本一致,除了:
• 在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理解返回 nil,但阻塞版本会根据 timeout,阻塞一段时间,期间 Redis 可以执行其他命令,但要求执行该命令的客户端会表现为阻塞状态(如图 2-22 所示)。
• 命令中如果设置了多个键,那么会从左向右进行遍历键,一旦有一个键对应的列表中可以弹出元素,命令立即返回。
• 如果多个客户端同时多一个键执行 pop,则最先执行命令的客户端会得到弹出的元素。
阻塞版本的 blpop 和 非阻塞版本 lpop 的区别
列表命令小结
内部编码
列表类型的内部编码有两种:
• ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的长度都小于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选用ziplist 来作为列表的内部编码实现来减少内存消耗。
• linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。
- 当元素个数较少且没有大元素时,内部编码为 ziplist:
127.0.0.1:6379> rpush listkey e1 e2 e3
OK
127.0.0.1:6379> object encoding listkey
"ziplist"
- 当元素个数超过 512 时,内部编码为 linkedlist:
127.0.0.1:6379> rpush listkey e1 e2 e3 ... 省略 e512 e513
OK
127.0.0.1:6379> object encoding listkey
"linkedlist"
- 当某个元素的长度超过 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 阻塞消息队列模型
分频道的消息队列
Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。
微博 Timeline
每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
- 每篇微博使用哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
- 向用户 Timeline 添加微博,user:<uid>:mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
- 分页获取用户的 Timeline,例如获取用户的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
Set集合
集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中 元素之间是无序的,元素不允许重复,。一个集合中最多可以存储 个元素。Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。
集合类型
普通命令
SADD
将一个或者多个元素添加到 set 中。注意,重复的元素无法添加到 set 中。
SADD key member [member ...]
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:本次添加成功的元素个数。
SMEMBERS
获取一个 set 中的所有元素,注意,元素间的顺序是无序的。
SMEMBERS key
命令有效版本:1.0.0 之后
时间复杂度:O(N)
返回值:所有元素的列表。
SCARD
获取一个 set 的基数(cardinality),即 set 中的元素个数。
SCARD key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:set 内的元素个数。
SPOP
从 set 中删除并返回一个或者多个元素。注意,由于 set 内的元素是无序的,所以取出哪个元素实际是未定义行为,即可以看作随机的。
SPOP key [count]
命令有效版本:1.0.0 之后
时间复杂度:O(N), n 是 count
返回值:取出的元素。
SMOVE
将一个元素从源 set 取出并放入目标 set 中。
SMOVE source destination member
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:1 表示移动成功,0 表示失败。
SREM
将指定的元素从 set 中删除。
SREM key member [member ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N), N 是要删除的元素个数.
返回值:本次操作删除的元素个数。
集合间的操作
交集(inter)、并集(union)、差集(diff)
SINTER
获取给定 set 的交集中的元素。
SINTER key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N * M), N 是最小的集合元素个数. M 是最大的集合元素个数.
返回值:交集的元素。
SINTERSTORE
获取给定 set 的交集中的元素并保存到目标 set 中。
SINTERSTORE destination key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N * M), N 是最小的集合元素个数. M 是最大的集合元素个数.
返回值:交集的元素个数。
SUNION
获取给定 set 的并集中的元素。
SUNION key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素。
SUNIONSTORE
获取给定 set 的并集中的元素并保存到目标 set 中。
SUNIONSTORE destination key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素个数。
SDIFF
获取给定 set 的差集中的元素。
SDIFF key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素。
SDIFFSTORE
获取给定 set 的差集中的元素并保存到目标 set 中。
SDIFFSTORE destination key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素个数。
集合命令小结
内部编码
• intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
• hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。
- 当元素个数较少并且都为整数时,内部编码为 intset:
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 4
127.0.0.1:6379> object encoding setkey
"intset"
- 当元素个数超过 512 个,内部编码为 hashtable:
127.0.0.1:6379> sadd setkey 1 2 3 4
(integer) 513
127.0.0.1:6379> object encoding setkey
"hashtable"
- 当存在元素不是整数时,内部编码为 hashtable:
127.0.0.1:6379> sadd setkey a
(integer) 1
127.0.0.1:6379> object encoding setkey
"hashtable"
使用场景
集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。 例如一个电子商务网站会对不同标签的用户做不同的产品推荐。
下面的演示通过集合类型来实现标签的若干功能。
- 给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
- 给标签添加用户
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
- 删除用户下的标签
srem user:1:tags tag1 tag5
...
- 删除标签下的用户
srem tag1:users user:1
srem tag5:users user:1
...
- 计算用户的共同兴趣标签
sinter user:1 1:tags user:2:tags
Zset有序集合
有序集合相对于字符串、列表、哈希、集合来说会有一些陌生。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有一个唯一的浮点类型的分数(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 ...]
命令有效版本:1.2.0 之后
时间复杂度:O(log(N))
返回值:本次添加成功的元素个数。
ZCARD
获取一个 zset 的基数(cardinality),即 zset 中的元素个数。
ZCARD key
命令有效版本:1.2.0 之后
时间复杂度:O(1)
返回值:zset 内的元素个数。
ZCOUNT
返回分数在 min 和 max 之间的元素个数。默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
ZCOUNT key min max
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))
返回值:满足条件的元素列表个数。
ZRANGE
返回指定区间里的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。
ZRANGE key start 1 stop [WITHSCORES]
此处的 [start, stop] 为下标构成的区间. 从 0 开始, 支持负数.
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
ZREVRANGE
返回指定区间里的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。
这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
ZREVRANGE key start stop [WITHSCORES]
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
ZRANGEBYSCORE
返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
ZRANGEBYSCORE key min 1 max [WITHSCORES]
命令有效版本:1.0.5 之后
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
ZPOPMAX
删除并返回分数最高的 count 个元素。
ZPOPMAX key [count]
命令有效版本:5.0.0 之后
时间复杂度:O(log(N) * M)
返回值:分数和元素列表。
BZPOPMAX
ZPOPMAX 的阻塞版本。
BZPOPMAX key [key ...] timeout
命令有效版本:5.0.0 之后
时间复杂度:O(log(N))
返回值:元素列表。
ZPOPMIN
删除并返回分数最低的 count 个元素。
ZPOPMIN key [count]
命令有效版本:5.0.0 之后
时间复杂度:O(log(N) * M)
返回值:分数和元素列表。
BZPOPMIN
ZPOPMIN 的阻塞版本。
BZPOPMIN key [key ...] timeout
命令有效版本:5.0.0 之后
时间复杂度:O(log(N))
返回值:元素列表。
ZRANK
返回指定元素的排名,升序。
ZRANK key member
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))
返回值:排名。
ZREVRANK
返回指定元素的排名,降序。
ZREVRANK key member
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))
返回值:排名。
ZSCORE
返回指定元素的分数。
ZSCORE key member
命令有效版本:1.2.0 之后
时间复杂度:O(1)
返回值:分数。
ZREM
删除指定的元素。
ZREM key member [member ...]
命令有效版本:1.2.0 之后
时间复杂度:O(M*log(N))
返回值:本次操作删除的元素个数。
ZREMRANGEBYRANK
按照排序,升序删除指定范围的元素,左闭右闭。
ZREMRANGEBYRANK key start stop
命令有效版本:2.0.0 之后
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
ZREMRANGEBYSCORE
按照分数删除指定范围的元素,左闭右闭。
ZREMRANGEBYSCORE key min max
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
ZINCRBY
为指定的元素的关联分数添加指定的分数值。
ZINCRBY key increment member
命令有效版本:1.2.0 之后
时间复杂度:O(log(N))
返回值:增加后元素的分数。
ZINTERSTORE
求出给定有序集合中元素的交集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]
命令有效版本:2.0.0 之后
时间复杂度:O(N*K)+O(M*log(M)) N 是输入的有序集合中, 最小的有序集合的元素个数; K 是输入了
几个有序集合; M 是最终结果的有序集合的元素个数.
返回值:目标集合中的元素个数
2 表示后面会指定 2 个有序集合 作为输入集合,分别是 zset1 和 zset2。 如果你有更多的有序集合参与交集计算,可以增加这个数字,并在后面列出这些集合的名称。
ZUNIONSTORE
求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]
命令有效版本:2.0.0 之后
时间复杂度:O(N)+O(M*log(M)) N 是输入的有序集合总的元素个数; M 是最终结果的有序集合的元素
个数.
返回值:目标集合中的元素个数
内部编码
- ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
- skiplist(跳表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时ziplist 的操作效率会下降。
当元素个数较少且每个元素较小时,内部编码为 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"
当元素个数超过 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"
当某个元素大于 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"
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:
假如用户A发布了一篇文章,并获得了3个赞,可以使用有序集合的zadd和zincrby功能,之后在获得赞继续使用zincrby,如果用户删除文章了,需要将用户从榜单中删除掉,可以使用zrem。展示获赞最多的10个用户,可以使用zrevrange命令实现
渐进式遍历
Redis 使用 scan 命令进行渐进式遍历键,进而解决直接使用 keys 获取键时可能出现的阻塞问题。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执行多次 scan。
首次 scan 从 0 开始.
当 scan 返回的下次位置为 0 时, 遍历结束.
SCAN
以渐进式的方式进行键的遍历。
SCAN cursor [MATCH pattern] [COUNT 1 count] [TYPE type]
命令有效版本:2.8.0 之后
时间复杂度:O(1)
返回值:下一次 scan 的游标(cursor)以及本次得到的键。
渐进性遍历 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 并没有为多数据库提供太多的特性,其次无论是否有多个数据库,Redis 都是使用单线程模型,所以彼此之间还是需要排队等待命令的执行。同时多数据库还会让开发、调试和运维工作变得复杂。所以实践中,始终使用数据库 0 其实是一个很好的选择。
清除数据库
flushdb / flushall 命令用于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数据库。