概要
上一节我们介绍了redis的发展历史,redis的版本,redis的核心原理等等,这一节我将继续通过3个模块去学习redis
在正式开始学习之前我先点破两个非常关键的道理:
一是 Redis 的命令有上百个,纯靠死记硬背很难记住,但如果理解了底层的机制,会发现这些命令有很强的通用性 —— 学会几个对所有键都通用的命令,后面学每种数据结构的具体命令时就能举一反三
二是 Redis 不是万金油,有些数据结构和命令只能在特定场景下使用,一旦用错,可能对 Redis 本身或者应用本身造成致命伤害
由此来引出我们这节要学习的3个内容:全局(generic)命令、数据结构和内部编码、单线程模式机制分析
一.全局命令:先学会操作 "键"
Redis 有 5 种数据结构,但它们都是键值对里的值。对于 "键" 本身,有一批对所有键通用的命令,先把这几个掌握住,后面的具体数据结构命令就好理解了
1.1KEYS pattern—— 返回所有满足样式(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([a-b] 匹配区间内字符)
语法:
KEYS pattern有效版本 :1.0.0 之后
时间复杂度 :O (N)
返回值是匹配 pattern 的所有 key
示例:
1.2.EXISTS—— 判断某个 key 是否存在,返回存在的个数
语法:
EXISTS key [key ...]有效版本 :1.0.0 之后
时间复杂度 :O (1)
返回值:key存在的个数
示例:
1.3.DEL key [key ...]—— 删除指定的 key,返回删除掉的 key 的个数
语法:
DEL key [key ...]有效版本 :1.0.0 之后
时间复杂度 :O (1)
返回值:删除掉的key的个数
示例:
1.4.EXPIRE key seconds—— 为指定 key 添加秒级的过期时间(TTL)
语法:
EXPIRE key seconds有效版本 :1.0.0 之后
时间复杂度 :O (1)
返回值:1表⽰设置成功。0表示设置失败
示例:
1.5.TTL key—— 获取指定 key 的过期时间(秒级)
语法:
TTL key有效版本 :1.0.0 之后
时间复杂度 :O (1)
返回值:剩余过期时间。-1表⽰没有关联过期时间,-2表⽰key不存在
示例:
注意:EXPIRE 和 TTL 都有对应的毫秒级版本 ——PEXPIRE 和 PTTL
键过期的整体机制,可以参考下面这张时间轴示意:
1.6.TYPE key—— 返回 key 对应的数据类型。
有效版本 :1.0.0 之后
时间复杂度 :O (1)
返回值:可能是 none、string、list、set、zset、hash、stream
示例:
中场小结:这一节只是抛砖引玉,给几个通用命令,为 5 种数据结构的使用做热身,键管理后面还有专门章节细讲
二. 数据结构和内部编码
TYPE 命令实际返回的,就是当前键的数据结构类型:string(字符串)、list(列表)、hash(哈希)、set(集合)、zset(有序集合),但要注意一个关键区分:这些只是 Redis对外呈现的数据结构,如下图所示.
实际上Redis针对每种数据结构都有自己的底层内部编码实现,而且是多种实现,这样Redis会 在合适的场景选择合适的内部编码,如下图所示
可以看到每种数据结构都有⾄少两种以上的内部编码实现,例如list数据结构包含了linkedlist和 ziplist 两种内部编码。同时有些内部编码,例如ziplist,可以作为多种数据结构的内部实现,可以通 过objectencoding命令查询内部编码
Redis 这样设计有两个好处:
1.可以改进内部编码,而对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码, ⽆需改动外部数据结构和命令,例如Redis3.2提供了quicklist,结合了ziplist和linkedlist两者的优 势,为列表类型提供了⼀种更为优秀的内部编码实现,而对用户来说基本无感知
2.多种内部编码实现可以在不同场景下发挥各自的优势,例如ziplist比较节省内存,但是在列表元素 ⽐较多的情况下,性能会下降,这时候Redis会根据配置选项将列表类型的内部实现转换为 linkedlist,整个过程用户同样无感知
三. 单线程模型:为什么能做到每秒万级
3.1示例
先看一个例子:客户端 2 和客户端 3 同时对 counter 做自增操作(incr counter)
一条命令从客户端发出,要经历三个阶段:发送命令、执行命令、返回结果,其中要重点关注的是第 2 步 —— 执行命令
所谓 "Redis 是单线程模型执行命令",指的是:虽然三个客户端看起来是同时要求 Redis 执行命令,但从微观角度看,这些命令是线性执行的 —— 执行顺序不确定,但一定不会有两条命令被同步执行。可以想象 Redis 内部只有一个服务窗口,多个客户端按到达的先后顺序排队,依次接受服务。所以两条 incr 命令无论谁先谁后,结果一定是 2,不会发生并发问题
如下图所示
宏观上同时要求服务的客户端:
微观上客户端发送命令的时间有先后次序的:
Redis的单线程模型:
3.2单线程为什么还能这么快
通常来说,单线程处理能力要比多线程差。举个类比:有 10000 公斤货物,每辆车运载能力是 200 公斤,那么要 50 次才能运完;但如果有 50 辆车,安排合理的话一次就能完成
那为什么 Redis 用单线程模型,还能达到每秒万级别的处理能力?可以归结为三点:
纯内存访问。Redis 把所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是达到每秒万级访问的重要基础
非阻塞 IO。Redis 使用 epoll 作为 I/O 多路复用技术的实现,再配合自身的事件处理模型,把 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多时间
如下图:
单线程避免了线程切换和竞态产生的消耗。一方面单线程可以简化数据结构和算法的实现,让程序模型更简单;另一方面避免了多线程竞争同一份共享数据时带来的切换和等待消耗
3.3单线程的一个致命短板
虽然单线程给 Redis 带来很多好处,但还是有一个致命问题:对单个命令的执行时间是有要求的。如果某个命令执行过长,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客户端的阻塞 —— 对于 Redis 这种高性能服务来说是非常严重的
所以结论很明确:Redis 是面向快速执行场景的数据库
四.重点回顾
这一节的要点,拢成几条:
全局命令:KEYS(按样式找 key,O (N))、EXISTS(判断存在,返回个数)、DEL(删除,返回个数)、EXPIRE(设秒级过期)、TTL(查剩余过期时间)、TYPE(返回数据类型)
通配符五种:? 单字符、* 任意数量、[] 集合内任一、[^] 集合外、[a-b] 区间
键过期:EXPIRE 设秒级,TTL 查过期;-1 是没设过期、-2 是 key 不存在;毫秒级用 PEXPIRE / PTTL
对外数据结构 vs 内部编码:TYPE 返回的 string /list/hash /set/zset 只是对外类型,内部编码是另一套
单线程模型:命令经历 "发送 → 执行 → 返回",微观上线性执行、绝不同步并行,像只有一个窗口在排队
单线程为什么快:纯内存访问、epoll 非阻塞 IO、没有线程切换和竞态
短板:一条慢命令会卡住所有命令,所以 Redis 只适合快速执行场景
本期博客到此结束,记得多总结,多回顾!!!