目录
1. 基本全局命令
KEYS
EXISTS
DEL
EXPIRE
TTL
TYPE
2. 数据结构和内部编码
3. 单线程架构
Redis 提供了5种数据结构,理解每种数据结构的特点对于 Redis 开发运维非常重要,同时掌握每种数据结构的常见命令,会在使用 Redis 的时候做到游刃有余。本章内容如下:
- 预备知识:几个全局(generic)命令,数据结构和内部编码,单线程式机制分析。
- 5 种数据结构的特点、命令使用、应用场景示例。
- 键遍历、数据库管理。
在正式介绍5种数据结构之前,了解一下 Redis 的一些全局命令、数据结构和内部编码、单线程命令处理机制是十分必要的,它们能为后面内容的学习打下一个良好的基础.
主要体现在两个方面:
1) Redis 的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解 Redis 的一些机制,会发现这些命令有很强的通用性。
2) Redis不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对 Redis 本身或者应用本身造成致命伤害。
1. 基本全局命令
Redis有5种数据结构,但它们都是键值对种的值,对于键来说有一些通用的命令。
KEYS
返回所有满足样式 (pattern) 的 key。支持如下统配样式。
语法
KEYS pattern
时间复杂度:0(N)
返回值:匹配 pattern 的所有 key。
示例:
127.0.0.1:6379> keys *name*
1) "lastname"
2) "firstname"
127.0.0.1:6379> keys a??
1) "age"
127.0.0.1:6379> keys *
1) "lastname"
2) "age"
3) "firstname"
EXISTS
判断某个 key 是否存在。
语法:
EXISTS key [key ...]
返回值:key存在的个数。
时间复杂度:0(1)
示例:
127.0.0.1:6379> set key1 "hello"
OK
127.0.0.1:6379> exists key1
(integer) 1
127.0.0.1:6379> exists nosuchkey
(integer) 0
127.0.0.1:6379> set key2 "world"
OK
127.0.0.1:6379> exists key1 key2 nosuchkey
(integer) 2
DEL
删除指定的 key。
语法:
DEL key [key ...]
返回值:删除掉的 key 的个数。
时间复杂度:0(1)
示例:
127.0.0.1:6379> set key1 "hello"
OK
127.0.0.1:6379> set key2 "world"
OK
127.0.0.1:6379> del key1 key2 key3
(integer) 2
EXPIRE
为指定的 key 添加秒级的过期时间(Time To Live TTL)
语法:
EXPIRE key seconds
时间复杂度:0O(1)
返回值:1表示设置成功。0表示设置失败。
示例:
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> ttl key
(integer) 8
TTL
获取指定 key 的过期时间,秒级。
语法:
TTL key
时间复杂度:0(1)
返回值:剩余过期时间。-1表示没有关联过期时间,-2 表示 key不存在。
示例:
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> set key "hello"
OK
127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> ttl key
(integer) 9
127.0.0.1:6379> ttl nokey
(integer) -2
关于键过期机制,可以参考下图所示。
键的过期机制
Redis 的 key 的过期删除策略是怎样的?
惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
Redis 中同时使用了惰性过期和定期过期两种过期策略:
- 每隔 100ms 就随机抽取一定数量的key来检查和删除的。
- 在获取某个key的时候,redis 会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。
TYPE
返回 key 对应的数据类型。
语法:
TYPE key
时间复杂度:0(1)
返回值: none,string,list, set,zset,hash,stream
示例:
127.0.0.1:6379> set key1 "value"
OK
127.0.0.1:6379> lpush key2 value
(integer) 1
127.0.0.1:6379> sadd key3 value
(integer) 1
127.0.0.1:6379> type key1
string
127.0.0.1:6379> type key2
list
127.0.0.1:6379> type key3
set
本小结只是抛砖引玉,给出几个通用的命令,为5种数据结构的使用做一个热身,后续章节将对
键管理做一个更为详细的介绍。
2. 数据结构和内部编码
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列
表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是Redis 对外的数据结构,如图所示。
Redis的5种数据类型
实际上 Redis 针对每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样 Redis 会
在合适的场景选择合适的内部编码.
Redis 数据结构和内部编码
可以看到每种数据结构都有至少两种以上的内部编码实现,例如 list 数据结构包含了 linkedlist 和
ziplist 两种内部编码。同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现,可以通过 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"
可以看到 hello 对应值的内部编码是 embstr,键 mylist 对应值的内部编码是 ziplist。
Redis 这样设计有两个好处:
1) 可以改进内部编码,而对外的数据结构和命令没有任何影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了一种更为优秀的内部编码实现,而对用户来说基本无感知。
2) 多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但是在列表元素比较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为
linkedlist,整个过程用户同样无感知。
3. 单线程架构
Redis 使用了单线程架构来实现高性能的内存数据库服务,本节首先通过多个客户端命令调用的例
子说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型为什么性能如此之高,最终给出为什么理解单线程模型是使用和运维 Redis 的关键。
引出单线程模型
现在开启了三个 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 去执行命令的,但微观角度,这些命令还是采用线性方式去执行的,只是原则上命令的执行顺序是不确定的,但一定不会有两条命令被同步执行,如图 2-3、2-4、2-5 所示,可以想象 Redis内部只有一个服务窗口,多个客户端按照它们达到的先后顺序被排队在窗口前,依次接受 Redis 的服务,所以两条 incr 命令无论执行顺序,结果一定是2,不会发生并发问题,这个就是 Redis 的单线程执行模型。
2.为什么单线程还能这么快
通常来讲,单线程处理能力要比多线程差,例如有 10000 公斤货物,每辆车的运载能力是每次
200 公斤,那么要 50 次才能完成;但是如果有 50 辆车,只要安排合理,只需要依次就可以完成任
务。那么为什么 Redis 使用单线程模型会达到每秒万级别的处理能力呢? 可以将其归结为三点:
a. 纯内存访问。Redis 将所有数据放在内存中,内存的响应时长大约为 100纳秒,这是 Redis 达到每秒万级别访问的重要基础。
b. 非阻塞 IO。Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型
将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间,如图 2-6所示。
c. 单线程避免了线程切换和竞态产生的消耗。单线程可以简化数据结构和算法的实现,让程序
型更简单: 其次多线程避免了在线程竞争同一份共享数据时带来的切换和等待消耗。
虽然单线程给 Redis 带来很多好处,但还是有一个致命的问题: 对于单个命令的执行时间都是有
要求的。如果某个命令执行过长,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客户端的阻塞,对于 Redis 这种高性能的服务来说是非常严重的,所以 Redis 是面向快速执行场景的数据库。