@TOC
目录
特性(优点)
存储方式
功能丰富
客户端语言多
数据存储位置
支持集群
支持主从复制
速度快
应用场景
数据库
排行榜系统
计数器应用
消息队列系统
redis客户端
基本全局命令
Keys
EXISTS
DEL
EXPIRE
TTL
过期策略:
定期删除
惰性删除
基于优先级队列/堆的定时器
基于时间轮实现的定时器
type
数据结构和内部编码
redis单线程模型
Redis是一个在内存中存储数据的中间件,用于作为数据库,用于作为数据缓存,在分布式系统中能够大展拳脚。
特性(优点)
存储方式
MySQL主要是通过“表”的方式来存储组织数据的"关系型数据库"
Redis主要是通过“键值对”的方式来存储组织数据的“非关系型数据库”
注:key的数据类型都是string
value的数据类型有多种:string, hash, list, set, sorted set, stream,等
功能丰富
提供了键过期功能,可以⽤来实现缓存。
• 提供了发布订阅功能,可以⽤来实现消息系统。• ⽀持Lua脚本功能,可以利⽤Lua创造出新的Redis命令。
• 提供了简单的事务功能,能在⼀定程度上保证事务特性。
• 提供了流⽔线(Pipeline)功能,这样客⼾端能将⼀批命令⼀次性传到Redis,减少了⽹络的开
销。
客户端语言多
可以在Redis原有的功能基础上再进行扩展,Redis提供了一组API, 通过C++, C, Rust 可以编写Redis的扩展(本质上就是一个动态链接库)
数据存储位置
Redis把数据存储在内存中,内存的数据是易失的,比如在进程退出和系统重启的时候,内存中的相关数据就会丢失,因此Redis会把数据存储在硬盘中,内存为主,硬盘为辅,如果Redis重启,就会在重启时加载硬盘中的备份数据,使Redis的内存恢复到重启前的状态。
支持集群
Redis作为一个分布式系统中的中间件,能够支持集群是很关键的, 类似于“分库分表”,一个Redis能够存储的数据是有限的(内存空间有限)引入多个主机,部署多个Redis节点,每个Redis存储数据的一部分。
支持主从复制
Redis自身也是支持“主从”结构的,从节点就相当于主节点的备份。
速度快
Redis数据在内存中,就比访问硬盘的数据库要快得多。Redis核心功能都是比较简单的逻辑, 核心功能都是比较简单的操作内存的数据结构。在网络角度上,Redis使用了IO多路复用的方式(epoll)
Redis 使用了单线程模型,这样的单线程模型,减少了不必要的线程之间的竞争开销(2020年发布的Redis6.0引入了多线程IO,但是多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程)因为多线程提高效率的前提是CPU密集型任务,使用多个线程可以充分的利用CPU多核资源,而Redis的核心任务,主要就是操作内存的数据结构, 不会吃太多CPU
应用场景
数据库
把redis当做数据库,大多数情况下,考虑到数据存储,优先考虑的就是“大”,但是仍然有一些场景, 考虑的是“快”。比如搜索引擎,搜索引擎对性能要求非常高,需要使用Redis在内存中查找,才能满足要求,但是这样对硬件的要求非常的高,所以可以将Redis和Mysql结合起来,将热点数据放到Redis中,这么就能达到一个很好的效果。
排行榜系统
排行榜系统几乎存在于所有的网站,例如按照热点排名的排行榜,按照发布时间和排行榜,按照各种复杂维度计算出的排行榜,redis提供了列表和有序列表的结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
计数器应用
计数器在网站中地作用至关重要,例如视频网站有播放数,电商网站有浏览数,为了保证数据地实时性,每一次播放和浏览都要做+1操作,如果并发量很大,对于传统关系型数据的性能是一个挑战。redis天然支持计数功能而且计数器地功能也非常好,可以说是计数器的最佳选择。
消息队列系统
消息队列系统可以说是⼀个⼤型⽹站的必备基础组件,因为其具有业务解耦、⾮实时业务削峰等特性。Redis提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列⽐还不够⾜够强⼤,但是对于⼀般的消息队列功能基本可以满足。
redis客户端
redis是一个客户端-服务器结构的程序。
redis 客户端和服务器可以在同一个主机上,也可以在不同的主机上,redis客户端用来提出请求,Redis服务器进行响应,负责存储和管理数据。
在安装好Redis之后,可以直接通过命令
redis-cli
打开命令行客户端,还可以指定主机号和访问的端口
redis-cli -h 主机号 -p 端口
上面提到redis的快,是相对于MySQL来说的,因为前面提到了这是一个客户端-服务器结构的程序,所以redis的速度取决于网络传输的速度和内存访问的速度,所以都是键值对的存储形式,redis要比哈希表慢得多。
基本全局命令
get : 根据key来取value
set :把key和value存储起来
如下:
get命令直接输入key就能得到value
如果当前key不存在,会返回nil。 nil和NULL一个意思
Redis有5种数据结构:字符串,哈希表,列表,集合,有序列表。
Keys
返回所有满足样式的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
语法:
keys pattern
时间复杂度O(N)
返回值:匹配pattern的所有key
注:生产环境上的key可能会非常多,而redis是一个单线程的服务器,执行keys * 的时间非常长, 就使redis 服务器被阻塞了,无法给其他客户端提供服务。
EXISTS
判断某个key是否存在。
语法:
EXISTS key [key...]
时间复杂度:O(1)
返回值:key存在的个数
DEL
删除指定的key
语法:
DEL key [key...]
时间复杂度:O(1)
返回值:删除掉的key的个数
EXPIRE
为指定的key添加秒级的过期时间
EXPIRE key seconds
时间复杂度:O(1)
返回值:1表示设置成功,0表示设置失败。
注:pexpire和expire作用一样,但是设置的时间是毫秒级别的
TTL
获取指定key的过期时间,秒级
语法:
TTL key
时间复杂度:O(1)
返回值:剩余过期时间。-1表⽰没有关联过期时间,-2表⽰key不存在。
注:pttl和ttl作用一样,但是设置的时间是毫秒级别的
过期策略:
一个redis中可能同时存在很多很多key, 这些key中可能有很大一部分都有过期时间,此时,redis 服务器咋知道哪些key已经过期要被删除,哪些key还没有过期 ?
如果直接遍历所有的key,效率太低了
redis的整体策略是
定期删除
每次抽取一部分,进行验证过期时间,保证这个抽取检查的过程,足够的快。
因为redis是单线程的程序,主要的任务就是处理每个命令的任务,如果扫描过期key消耗的时间太久了,就可能导致正常处理请求命令就被阻塞了,
惰性删除
假设一个key已经过期了,但是暂时还没删掉它,key还存在,紧接着,后面有一个访问,正好访问的是这个key,于是这次访问就会让redis服务器触发删除key的操作,同时再返回一个nil。
除了上述之外,redis还提供了一系列的内存淘汰策略。
1. redis中并没有采取定时器的方式来实现过期key删除
2. 如果有多个key过期,也可以通过一个定时器来高效/节省CPU的前提下处理多个key
这里的定时器可以通过优先级队列和时间轮都可以实现比较高效的定时器
基于优先级队列/堆的定时器
正常的队列是先进先出
优先级队列则是按照指定的优先级先出
在redis过期key的场景中,可以通过“过期时间越早,优先级越高”。
现在假设有很多key设置了过期时间
就可以把这些key加入到一个优先级队列中,指定优先级规则是过期时间早的, 先出队列
队首元素就是最早过期的key
此时定时器中只要分配一个线程,让这个线程取检查队首元素,看是否过期即可,如果队首元素还没过期,后续元素一定没过期。
此时,扫描线程就不需要遍历所有的key,只需要盯着一个队首元素即可。
基于时间轮实现的定时器
把时间划分成很多小段(划分的粒度,看实际需求)
每一个小段都挂着一个链表,每个链表都代表一个要执行的任务。(相当于一个函数指针,以及对应的参数绑定在一起)
每次走到一个格子,就会把这个格子上链表的任务尝试执行一下
对于时间轮来说,每个格子是多少时间,一共有多少个格子,都是需要根据实际场景,灵活调配的
这只是高效定时器的实现方法,redis并没有尝试这些方案,因为redis是属于单线程的,上面的都需要一个线程进行监听。
type
返回key对应的数据类型。
语法:
TYPE key
时间复杂度:O(1)
返回值:none, string, list, set, zset, hash 和 stream
数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型,他们分别是string(字符串),list(列表), hash(哈希表), set(集合), zset(有序集合),但是这些只是Redis对外的数据结构。
Redis底层在实现上述数据结构的时候,会在源码层面,针对上述实现进行了特定的优化,这样Redis会在合适的场景选择合适的内部编码。
Redis数据结构和内部编码
数据结构 | 内部编码 |
string | raw(最基本的字符串) |
int(redis通常也可以用来实现一些计数功能, 当value是整数的时候,此时可能redis会直接使用int来保存) | |
embstr(针对短字符串进行的特殊优化) | |
hash | hashtable(最基本的哈希表) |
ziplist(在哈希表元素比较少的时候,可能就优化成ziplist) | |
list | linkedlist |
ziplist(压缩列表) | |
set | hashtable |
intset(集合中存的都是整数) | |
zset | skiplist(跳表)[跳表也是链表,不同于普通的链表,每个节点上有多个指针域] |
ziplist |
那么我们如何查看实际value到底是什么呢
可以通过object encoding [key]来查看key对应value的实际编码方式。
redis单线程模型
redis只使用了一个线程,处理所有的命令请求,不是说一个redis服务器进程内部真的就只有一个线程。其实也有多个线程,多个线程是在处理网络IO。
我们上面说过redis是客户端服务端结构的程序,那么必然会有多线程的问题,在处理网络IO请求的时候,先是多线程处理,而redis服务器是单线程的模型,所以多线程只是将这些请求搞成串行执行,多个请求同时到达redis服务器,也是要先在队列中排队,再等待redis服务器一个一个的取出里面的命令再执行,微观上讲,redis服务器是串行/顺序执行这么多命令的。
redis能够使用单线程模型的原因主要是在于redis的核心业务逻辑,都是短平快的, 不太消耗cpu资源也就不太吃多核了
所以在进行redis命令的时候,一定要注意不能让某个操作占用时间长,就会阻塞其他命令的执行。
redis在处理网络IO的时候,使用了epoll这样的IO多路复用机制
一个线程,就可以就可以管理多个socket,针对TCP,服务器这边每次要服务一个客户端,都需要给这个客户端安排一个socket.