预备知识
1.全局命令
1.1 查看所有键
keys * 命令将所有的键输出。该命令会遍历所有键,所以它的时间复杂度是 O(n),当 Redis 保存了大量的键时,线上环境禁止使用。
1.2 键总数
dbsize 命令会返回当前数据库中键的总数。该命令在计算键总数时不会遍历所有的键,而是直接获取 Redis 内置的键总数变量,所以 dbsize 命令的事件复杂度是 O(1)。
1.3 检查键是否存在
exists key 如果键存在就返回 1,反之则返回 0.
1.4 删除键
del key [ key ... ] 这是一个通用命令,无论值是什么数据类型结构,都可以将其删除,而且支持删除多个键。返回结果为成功删除的键的个数,假设删除一个不存在的键,则返回 0。
1.5 键过期
expire key seconds 对键添加过期时间,当超过过期时间后,会自动删除键。添加成功返回 1.
ttl key 查看键的剩余过期时间,它有 3 种返回值:
# 大于等于 0 的整数:键剩余的过期时间
# -1 :键没设置过期时间
# -2 :键不存在
1.6 键的数据结构类型
type key 键存在就返回键的数据结构类型,否则返回 none。
2.数据结构与内部编码
type 命令时间返回的就是当前键的数据结构类型,但是这些只是 Redis 对外的数据结构。实际上每种数据结构都有自己底层的内部编码实现,可以通过 object encoding key 命令查询键的内部编码。
# string :raw、int、embstr
# hash :hashtable、ziplist
# list : linkedlist、ziplist
# set :hashtable、intset
# zset :skiplist、ziplist
Redis 这样设计有两个好处:一是,可以改进内部编码,而对外的数据结构和命令没有影响。例如 Redis 3.2 提供了 quicklist ,结合了ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更优秀的内部编码实现,而对外部用户来说基本感知不到。二是,多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会有所下降,这时候Redis 会更具配置选项将列表类型的内部实现转为 linkedlist。
3.单线程架构
Redis 使用了单线程架构和 I/O 多路复用模型来实现高性能的内存数据库服务。Redis 客户端调用都经历了发送命令、执行命令、返回结果三个过程,执行命令是 Redis 服务端负责的,因为是单线程来处理命令,当多个客户端调用,每条命令到达服务端都会进入一个队列中,然后逐个执行。
为什么单线程还能这么快?
第一,纯内存访问,内存的响应时长大约为 100 纳秒,这是Redis 达到每秒万级别访问的重要基础。
第二,非阻塞 I/O,Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。
第三,单线程避免了线程切换和竞态产生的消耗。
单线程带来的两个好处:一是,可以简化数据结构和算法的实现;二是,避免了线程切换和竞态产生的消耗,对于服务端开发来说,锁和线程切换通常是性能杀手。
单线程也有一个问题:对于每个命令的执行时间是有要求的。 如果某个命令执行时间过长(如上文查看所有键的情况),会造成其他命令的阻塞,对于 Redis 这种高性能的服务来说是致命的,所以 Redis 是面向快速执行场景的数据库。
字符串
字符串是Redis 最基础的数据结构,首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的。字符串类型的值可以是字符串(简单的字符串、复杂的字符串(如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512 MB。
1.命令
1.1 设置值
set key value [ex seconds] [px milliseconds] [nx|xx]
以上可知set 命令有几个选项:
# ex seconds :为键设置秒级过期时间;
# px milliseconds :为键设置毫秒级过期时间;
# nx :键必须不存在才可以设置成功,用于添加;
# xx :与 nx 相反,键必须存在才可以设置成功,用于更新;
Redis 还提供了以下两个命令:
setex key seconds value
setnx key value
set 命令设置成功返回 OK ;setex 命令成功返回 OK;setnx 命令成功返回1,失败返回0。由于 Redis 的单线程命令处理机制,如果有多个客户端执行setnx 命令,setnx 可以作为分布式锁的一种实现方式。
1.2 获取值
get key 如果键不存在就返回 nil
1.3 批量设置值
mset key value [ key value ... ] 设置成功返回 OK
1.4 批量获取值
mget key [ key ... ] 如果有些键不存在,它的返回值为 nil ,结果是按照传入键的顺序返回的
批量操作可以有效地提高开发效率,减少网络延时。但是要注意每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成 Redis 阻塞或者网络拥塞。
1.5 计数
incr key 自增
decr key 自减
incrby key 自增指定数字
decrby key 自减指定数字
incrbyfloat key 自增浮点数
用于对值做自增操作,返回结果分为三种情况:
# 值不是整数,返回错误;
# 值是整数,返回自增后的结果;
# 键不存在,按照值为0自增,返回结果为 1;
以上都是一些常用的命令,还有一些不常用命令如下:
1.6 追加值
append key value 向字符串尾部追加值,操作成功返回字符串长度
1.7 字符串长度
strlen key 返回字符串长度,注意一个中文占用三个字节
1.8 设置并返回原值
getset key value 如果key 不存在就返回 nil ,存在就返回原来的 value
1.9 设置指定位置的字符
setrange key offeset value 把指定位置的字符替换,返回字符串长度;偏移量从0开始;
1.10 获取部分字符串
getrange key start end 返回指定偏移量之间的字符,偏移量从 0 开始计算;如果end 小于 start 返回 “” ,end 允许大于字符串长度,start 大于 字符串长度返回 ""
2.内部编码
字符串类型的内部编码有3 种,Redis 会根据当前值的类型和长度决定使用哪种内部编码实现。
# int:8个字节的长整型;
# embstr:小于等于39个字节的字符串;
# raw:大于39个字节的字符串;
3.典型使用场景
3.1 缓存功能
作为web 端和数据库之间的缓存层,由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
对于键名的设置,比较推荐的方式是使用 “业务名:对象名:id:[属性]”,分号也可以换成其他的符号,在能描述键名含义的前提下适当减少键的长度,从而减少由于键过长的内存浪费。
3.2 计数
Redis 可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。如视频系统的用户播放次数。
3.3 共享 Session
在一个分布式 web 服务应用中,出于负载均衡的考虑,用户的访问可能会路由到不同的服务器上,用户刷新一次访问可能会发现需要重新登录,这是无法容忍的。使用 Redis 将用户的 Session 进行集中管理,只要保证 Redis 是高可用和可扩展的,每次用户更新或者查询登录信息直接从Redis 中直接获取。
3.4 限速
为了应用的某些接口不被频繁访问,会限制用户或某个IP某个时间段里的访问次数,可以通过键过期和自增命令来实现。
哈希
几乎所有的编程语言都提供了hash 类型,它们的叫法可能是哈希、字典、关联数组。在 Redis 中,哈希类型是指键值本身又是一个键值对结构。
1.命令
1.1 设置值
hset key field value 设置成功返回 1 ,反之会返回 0;此外 Redis 提供了 hsetnx 命令 ,但作用域为 field
1.2 获取值
hget key field 如果键不存在,会返回 nil
1.3 删除 field
hdel key field [ field ... ] 会删除一个或多个field,返回结果为成功删除 field 的个数
1.4 计算 field 的个数
hlen key 如果键不存在,返回 0
1.5 批量设置或获取 field-value
hmget key field [ field ... ] 不存在返回 nil ,存在返回结果顺序和 field 顺序一致
hmset key field value [ field value ... ] 成功添加返回 OK
1.6 判断 field 是否存在
hexists key field 当key 包含 field 返回 1,否则返回 0;
1.7 获取所有 field
hkeys key 键不存在时返回 (empty list or set)
1.8 获取所有 value
hvals key 键不存在时返回 (empty list or set)
1.9 获取所有的 field-value
hgetall key 键不存在时返回 (empty list or set)
如果哈希元素个数比较多,该命令会存在阻塞 Redis 的可能。如果只需要获取部分 field,可以使用 hmget,如果一定要获取全部 field-value,可以使用 hscan 命令,该命令会渐进式遍历哈希类型。
1.10 计数
hincrby key field
hincrbyfloat key field
1.11 计算 value 的字符串长度
hstrlen key field 需要 Redis 3.2 以上
2.内部编码
哈希类型有两种内部编码:
# ziplist:压缩列表,当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认是 512个),同时所有值都小于 hash-max-ziplist-value 配置(默认是 64字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
# hashtable:哈希表,当哈希类型无法满足ziplist 的条件时, Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1)。
3.使用场景
在缓存一些对象信息时,使用哈希类型相比于使用字符串序列化对象信息,更加直观,并且在更新操作上更加便捷。对象信息总的来说有三种方式来存储,下面比较一下优缺点:
# 原生字符串类型:每个属性一个键,优点:简单直观,每个属性都支持更新操作;缺点:占用过多的键,内存占用量大,同时对象信息内聚性较差,一般不会再生产环境使用。
# 序列化字符串类型:将对象信息序列化后用一个键保存,优点:简化编程,如果合理的使用序列化可以提高内存的使用效率;缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据进行反序列化,更新后再序列化到 Redis 中。
# 哈希类型:每个用户属性使用一对 field-value ,但是只用一个键保存,优点:简单直观,如果使用合理可以减少内存空间的使用;缺点:要控制哈希在ziplist 和 hashtable 两种内部编码的转换,hashtable会消耗更多的内存。
列表
列表是用来存储多个有序的字符串,一个列表最多可以存储 2的 32次方减 1 个元素。在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表类型有两个特点:
# 列表中的元素是有序的
# 列表中的元素可以重复
1.命令
1.1 添加操作
rpush key value [ value ... ] 从右边插入元素,返回插入元素的个数
lpush key value [ value ... ] 从左边插入元素
linsert key before | after spelem value 向某个元素(如:spelem)前或后插入元素,返回元素个数
1.2 查找
lrange key start end 获取指定范围内的元素列表,经常使用 lrange key 0 -1来查询列表所有元素。
lindex key index 获取列表指定索引下标的元素
llen key 获取列表长度 你
列表所有下标有两个特点:一是,索引下标从左到右分别是 0 到 N - 1,但是从右到左分别是 -1 到 -N。二是,lrange 中的 end 选项包含了自身,这和很多编程语言不包含end 不太相同。
1.3 删除
lpop key 从列表左侧弹出元素
rpop key 从列表右侧弹出
lrem key count value 删除指定元素
ltrim key start end 按照索引范围修剪列表
lrem 命令会从列表中找到等于value 的元素进行删除,根据 count 的不同分为三种情况:
# count > 0:从左到右,删除多个 count 个元素;
# count < 0:从右到左,删除最多 count 绝对值个元素;
# count = 0:删除所有。
1.4 修改
lset key index newValue 修改指定索引下标的元素
1.5 阻塞操作
blpop key [ key ... ] timeout
brpop key [ key ... ] timeout
列表为空时,如果timeout = 3,那么客户端要等到 3 秒后返回,如果 timeout = 0,那么客户端一直阻塞等下去,但在阻塞期间添加了元素,客户端立即返回。列表不为空时,客户端立即返回。
在使用brpop 时,需要注意两点:第一,如果是多个键,那么brpop 会从左至右遍历键,一旦有一个键能弹出来元素,客户端立即返回该键和其元素;第二,如果多个客户端对同一个键执行 brpop ,那么最先执行 brpop 命令的客户端可以获取到弹出的值,其他客户端继续阻塞。
2.内部编码
列表类型有两种内部编码:
# ziplist:压缩列表,当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个),同时列表中每个元素的值都小于 list-max-ziplist-value 配置时(默认 64 个字节),Redis 会选用 ziplist减少内存的使用。
# linkedlist:链表,当列表类型无法满足 ziplist 的条件时, Redis 会使用 linkedlist 作为列表的内部实现。
Redis 3.2 提供了 quicklist 内部编码,简单地说它是以一个 ziplist 为节点的 linkedlist ,它结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更为优秀的内部编码。经测试,元素大小不影响内部编码,都是 quicklist。
3.使用场景
3.1 消息队列
Redis 的 lpop + brpop 命令组合即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的 “抢” 列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
3.2 文章列表
每个用户有属于自己的文章列表,现需要分页显示文章列表。此时可以考虑使用列表,因为列表不单是有序的,同时支持按照索引范围获取元素。实现思路:一,每篇文章使用哈希结构存储;二,向用户文章列表添加文章;三,分页获取用户文章列表。
使用列表类型保存和获取文章列表会存在的两个问题。一,如果每次分页的文章个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 Pipeline 批量获取,或者考虑将文章序列化为字符串类型,使用 mget 批量获取。二,分页获取文章列表时,lrange 命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差,此时可以考虑将列表做二级拆分,或者使用 Redis 3.2 的quicklist 内部编码实现,它获取列表中间范围的元素时也可以高效完成。
在实际的使用场景中列表用的挺多,在选择时可以参考一下口诀:
# lpush + lpop = Stack(栈);
# lpush + rpop = Queue(队列);
# lpush + ltrim = Capped Collection(有限集合);
# lpush + brpop = Message Queue(消息队列);
集合
集合类型也是用来保存多个字符串元素,但它不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储 2 的32 次方减 1 个元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
1.命令
1.1 添加元素
sadd key element [ element ... ] 返回结果为成功添加的元素,重复的添加不成功,返回 0
1.2 删除元素
srem key element [ element ... ] 返回结果为成功删除的个数,删除不存在的元素,返回 0
1.3 计算元素个数
scard key 时间复杂度为 O(1),它不会遍历集合的所有元素,而是直接用Redis 内部的变量
1.4 判断元素是否存在集合中
sismember key element 存在集合中返回 1,否则返回 0
1.5 随机从集合返回指定个数元素
srandmember key [ count ] 个数是可选参数,默认为 1
1.6 从集合随机弹出元素
spop key 弹出的元素从集合中删除, 从3.2 版本开始支持 [count]
1.7 获取所有元素
smembers key 返回的结果是无序的,如果元素过多存在阻塞 Redis 的可能性
以上都是集合内部的操作,下面是集合间的操作
1.8 多个集合的交集
sinter key [ key ... ]
1.9 求多个集合的并集
sunion key [ key ... ]
1.10 求多个集合的差集
sdiff key [ key ... ]
1.11 将交集、并集、差集的结果保存
sinterstore destination key [key...]
sunionstore destination key [ key ... ]
sdiffstore destination key [ key ... ]
集合间的运算在元素较多的情况下会比较耗时,所以 Redis 提供了上面三个命令将结果保存在 destination key 中,destination 本身也是集合类型。
2.内部编码
集合类型的内部编码有两种:
# intset:整数集合,当集合中的元素都是整数且元素个数小于set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 inset 来作为集合的内部实现,从而减少内存的使用。
# hashtable:哈希表,当集合类型无法满足 inset 的条件时,Redis 会使用hashtable 作为集合的内部实现。
3.使用场景
集合类比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育等感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。
有序集合
保留了集合不能有重复元素的特性,但不同的是有序集合中的元素可以排序,它给每个元素设置一个分数(score,不同于爱书的score 可相同)作为排序的依据。
1.命令
1.1 添加元素
zadd key score member [ score member ... ] 返回结果代表成功添加的个数
有两点需要注意:
# Redis 3.2 为 zadd 添加了 nx、xx、ch、incr 四个选项
nx:member 必须不存在,才可以设置成功,用添加;
xx:member 必须存在,才可以设置成功,用于更新;
ch:返回此操作后,有序集合元素和分数发生变化的个数;
incr:对 score 做增加,相当于后面介绍的 zincrby。
# 有序集合相比集合提供了排序字段,但是也产生了代价,zadd 的时间复杂度为 O(log(n)),sadd 的时间复杂度为O(1)。
1.2 计算成员个数
zcard key 时间复杂度为O(1)
1.3 计算某个成员的分数
zscore key member 如果元素不存在,返回 nil
1.4 计算成员的排名
zrank key member 按分数从低到高返回排名(排名从 0 开始算)
zrevrank key member 按分数从高到低返回排名
1.5 删除成员
zrem key member [ member ... ] 返回结果为成功删除的个数
1.6 增加成员的分数
zincrby key increment member
1.7 返回指定排名范围的成员
zrange key start end [withscores] 按照分值排名,从低到高返回元素和其分数
zrevrange key start end [withscores]
1.8 返回指定分数范围的成员
zrangebyscore key min max [withscore] [limit offset count] 按照分数从低到高返回
zrevrangescore key max min [withscore] [limit offset count]
withscore 选项会同时返回每个成员的分数。[limit offset count] 选项可以限制输出的起始位置和个数,同时min 和max 还支持开区间(小括号)和闭区间(中括号),-inf 和+inf 分别代表无限小和无限大。
1.9 返回指定分数范围成员个数
zcount key min max
1.10 删除指定排名内的升序元素
zremrangebyrank key start end
1.11 删除指定分数范围的元素
zremrangebyscore key min max
1.12 交集
zinterstore destination numkeys key [ key .. ] [ weights weigth [ weigth .. ] ] [aggregate sum|min|max ]
参数说明:
# destination:交集计算结果保存到这个键;
# numkeys:需要做交集计算键的个数;
# weights:每个键的权重,在做交集计算时,每个键中的每个 member 会将自己分数乘以这个权重,每个键的权重默认是1;
# aggregate:计算交集后,分值可以按照sum、min、max做汇总,默认值是 sum。
1.13 并集
zunionstore destination numkeys key [ key ..] [ weights weigth [ weigth .. ] ] [aggregate sum|min|max ]
2.内部编码
有序集合类型的内部编码有两种:
# ziplist:压缩列表,当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
# skiplist:当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的读写效率会下降。
3.使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、播放量、获得的赞数。
参考资料:
《Redis 开发与运维》