目录
[1.1 前言](#1.1 前言)
[1.1.1 基本全局命令](#1.1.1 基本全局命令)
[1.1.1.1 SET](#1.1.1.1 SET)
[1.1.1.2 GET](#1.1.1.2 GET)
[1.1.1.3 KEYS](#1.1.1.3 KEYS)
[1.1.1.4 EXISTS](#1.1.1.4 EXISTS)
[1.1.1.5 DEL](#1.1.1.5 DEL)
[1.1.1.6 EXPIRE](#1.1.1.6 EXPIRE)
[1.1.1.7 TTL](#1.1.1.7 TTL)
[1.1.1.8 Redis的key的过期策略是怎么实现的?(一个经典的面试题)](#1.1.1.8 Redis的key的过期策略是怎么实现的?(一个经典的面试题))
[1.1.1.9 定时器的实现原理](#1.1.1.9 定时器的实现原理)
[1.1.1.10 TYPE](#1.1.1.10 TYPE)
[1.1.1.11 全局命令小结](#1.1.1.11 全局命令小结)
[1.1.2 数据类型和编码方式](#1.1.2 数据类型和编码方式)
[1.1.3 单线程架构](#1.1.3 单线程架构)
一、Redis常见数据类型
Redis 提供了 5 种数据结构,理解每种数据结构的特点对于 Redis 开发运维⾮常重要,同时掌握每
种数据结构的常⻅命令,会在使⽤ Redis 的时候做到游刃有余。
1.1 前言
在正式介绍 5 种数据结构之前,了解⼀下 Redis 的⼀些全局命令、数据结构和内部编码、单线程
命令处理机制是⼗分必要的,它们能为后⾯内容的学习打下⼀个良好的基础.
主要体现在两个⽅⾯:
1)Redis 的命令有上百个,如果纯靠死记硬背⽐较困难,但是如果理解 Redis 的⼀些机制,会发现这些命令有很强的通⽤性。
2)Redis 不是万⾦油,有些数据结构和命令必须在特定场景下使⽤,⼀旦使⽤不当可能对 Redis 本⾝或者应⽤本⾝造成致命伤害。
1.1.1 基本全局命令
Redis 有 5 种数据结构,但它们都是键值对种的值,对于键来说有⼀些通⽤的命令。

1.1.1.1 SET
SET
把key和value存进去。
1.1.1.2 GET
GET
根据key来取value

1.1.1.3 KEYS
KEYS
返回所有满⾜样式(pattern)的 key。⽀持如下统配样式。
- h?llo 匹配 hello , hallo 和 hxllo
- h*llo 匹配 hllo 和 heeeello
- haello 匹配 hello 和 hallo 但不匹配 hillo
- h\^ello 匹配 hallo , hbllo , ... 但不匹配 hello
- ha-bllo 匹配 hallo 和 hbllo
KEYS pattern
命令有效版本:1.0.0 之后
时间复杂度:O(N)
返回值:匹配 pattern 的所有 key。


这些匹配规则不用特意去背。
注意:
keys命令的时间复杂度是O(N)
所以在生产环境上面一般都会禁止使用keys命令,尤其是keys *(查询redis中所有的key!!!)
生成环境上面key可能会非常多!!而redis是一个单线程的服务器,执行keys * 的事件非常长,就使redis服务器被阻塞了,无法给其它客户端提供服务!!
1.1.1.4 EXISTS
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

图解:

1.1.1.5 DEL
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


1.1.1.6 EXPIRE
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

1.1.1.7 TTL
TTL:用来查看当前key的过期时间还剩下多少。
语法:TTL key
时间复杂度:O(1)
返回值:剩余过期时间。-1 表⽰没有关联过期时间,-2 表⽰ key 不存在。

1.1.1.8 Redis的key的过期策略是怎么实现的?(一个经典的面试题)
一个redis中可能同时存在很多很多key。这些key中有很大可能都有过期时间。此时,redis服务器咋知道哪些key已经过期要被删除,哪些key还没过期??
如果直接遍历所有的key,显然是行不通的,,效率非常低。
redis整体的策略是:
1.定期删除
每次抽取一部分,进行验证过期时间,保证这个抽取的过程,足够快!!(有点像数学概率里面的抽样检测)
那么为啥对于时间有着明确的要求呢?
因为redis是一个单线程的程序。
主要的任务(处理每个命令的任务,刚才扫描过期key。。。。)
如果扫描过期key消耗的时间太多了,就可能导致正常处理请求命令被阻塞了。(产生了类似于keys *这样的效果)
2. 惰性删除
假设这个key已经到了过期时间了,但是暂时还没有删他,key还存在。
紧接着,后面有一次访问,正好用到了这个key,于是这次访问就会让redis服务器触发删除key的操作,同时再返回一个nil
图解:

1.1.1.9 定时器的实现原理
1. 基于优先级队列/堆
正常的队列是先进先出。优先级队列则是按照指定的优先级,先出。
啥叫优先级高?自定义的~~
在redis过期key的场景中,就可以通过"过期时间越早,就是优先级越高"
现在假定有很多key设置了过期时间.
就可以把这些key加入到一个优先级队列中,指定优先级规则是过期时间早的,先出队列。
队首元素,就是最早的要过期的key!!
此时定时器中只要分配一个线程,让这个线程去检查对首元素,看是否过期即可!!
如果对手元素还么有过期,后续元素一定没过期!
此时扫描线程不需要遍历所有key只盯住这一个队首元素即可!!
另外在扫描线程检查队首元素过期时间的时候,也不能检查的太频繁
此时做法就是可以根据当前时刻和队首元素的过期时间,设置一个等待时间
当一个时间差不多了,系统再唤醒这个线程。
此时扫描线程不需要高频扫描队首元素,把cpu的开销也节省下来了。
万一再线程休眠的时候,来了一个新任务要执行。
可以再新任务添加的时候唤醒一下刚才的线程~~重新检查一下队首的元素,再根据时间差距重新调整阻塞时间即可。
2.基于时间轮实现的定时器
把时间分成很多小段(具体怎么划分,看实际需求)

当设定的过期时间特别长,就如上面的例子,可能小段没有那么多,但是还是要继续往后面数,比如上面就八段,但是还是要数九、十、十一、一直数到三十,在后面加上该任务。
此处我们一定要注意!!
redis并没有采取上面的方案!!但是我们需要了解这两种方法,都是属于高效的定时器实现方式,很多场景都有可能会用到。
在redis源码里面有一个比较核心的机制,是事件循环。
1.1.1.10 TYPE
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"

1.1.1.11 全局命令小结
图解:

1.1.2 数据类型和编码方式
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列
表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构
Redis 的 5 种数据类型


实际上 Redis 针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现,这样 Redis 会
在合适的场景选择合适的内部编码
|--------|------------|
| 数据结构 | 内部编码 |
| string | raw |
| string | int |
| string | embstr |
| hash | hashtable |
| hash | ziplist |
| list | linkedlist |
| list | ziplist |
| set | hashtable |
| set | intset |
| zset | skiplist |
| zset | ziplist |
图解:


可以看到每种数据结构都有⾄少两种以上的内部编码实现,例如 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"

redis会自动根据当前实际情况选择内部的编码方式,自动适应的。
Redis 这样设计有两个好处:
1)可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码,⽆需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,⽽对⽤⼾来说基本⽆感知。
2)多种内部编码实现可以在不同场景下发挥各⾃的优势,例如 ziplist ⽐较节省内存,但是在列表元素⽐较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为
linkedlist,整个过程⽤⼾同样⽆感知。
1.1.3 单线程架构
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 的单线程执⾏模型。



redis能够使用单线程模型很好的工作,原因主要在于redis的核心业务逻辑,都是短平快的
不太消耗cpu资源也就不太吃多核了!!!
弊端!!!
redis必须要特别小心,某个操作占用时间特别长,就会阻塞其它命令的执行!!!
为什么单线程还能这么快(经典面试题)
通常来讲,单线程处理能⼒要⽐多线程差,例如有 10000 公⽄货物,每辆⻋的运载能⼒是每次200 公⽄,那么要 50 次才能完成;但是如果有 50 辆⻋,只要安排合理,只需要依次就可以完成任务。那么为什么 Redis 使⽤单线程模型会达到每秒万级别的处理能⼒呢?可以将其归结为三点:
a. 纯内存访问。Redis 将所有数据放在内存中,内存的响应时⻓⼤约为 100 纳秒,这是 Redis 达到每秒万级别访问的重要基础。
b. ⾮阻塞 IO。Redis 使⽤ epoll 作为 I/O 多路复⽤技术的实现,再加上 Redis ⾃⾝的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在⽹络 I/O 上浪费过多的时间,如下图。
c. 单线程避免了线程切换和竞态产⽣的消耗。单线程可以简化数据结构和算法的实现,让程序模型更简单;其次多线程避免了在线程竞争同⼀份共享数据时带来的切换和等待消耗。

举个栗子:
方源,商心慈,谢含沫今晚要出去吃饭,
方源要吃方正小吃,商心慈要吃黑土小吃,谢含沫要吃人族蛊师小吃。
有三种解决方法。
一:
先买方正小吃,然后等待。
再买黑土小吃,然后等待。
再买人族蛊师小吃,等待。
各等各的。这是效率最低的方案。
二:
三个人各自去买各自的小吃。效率大大提升了,但是系统开销大了。
三:
商心慈一个人去,先买方正小吃,在等的过程中,去买黑土小吃,等的过程中去买人族蛊师小吃,这三样东西,哪个先做好了,对应的老板就可以喊一嗓子。(这就是epoll事件通知/回调机制)此时一个线程同时做三件事情,能高效的完成的前提是,这三件事的交互都不太频繁,大部分都在等!!!
如果这三件事都是交互频繁的,还是老老实实多搞几个线程靠谱~~一个线程容易忙不过来。
