Redis 内部逻辑浅析
数据库操作
数据库服务器: redisServer结构 - db、dbnum属性
服务器上数据库: redisDb结构 - dict、expires属性
客户端:redisClient结构 - db属性
- 属性db是一个数组,保存着服务器上所有的数据库。
- 属性dbnum为服务器上数据库数量,默认值为16。
切换数据库:redisClient结构中db属性记录了客户端的目标数据库(本质上它是一个redisDb结构的指针)。
切换数据库
使用 SELECT <目标数据库号码>,本质就是改变db指针内容。
键空间
键空间,即:redisDb结构的字典dict属性,保存数据库中所有键值对.
对键的增删改查操作都是通过对健空间字典进行操作实现的。
读取键空间时维护操作
- 根据键是否存在,来更新服务器的键空间命中次数或不命中次数,可以使用INFO stats命令查看keyspace_hits属性和keyspace_misses属性。
- 更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间。可以使用OBJECT idletime
命令查看key的闲置空间。 - 检查键是否过期,过期会删除该键,然后才执行余下的操作。
- 当有客户端使用WATCH命令监控了某个键,当该键被修改后,要标记该键,让事务程序可以识别出。
- 每次修改键后,服务器会对dirty计数器的值+1;该计数器可以用来触发服务器持久化以及复制操作。
- 如果开启数据库同通知功能,需要按配置发送相应的数据库通知。
过期时间
设置过期时间
一般主要包括4种处理过期方,其中expire都是以秒为单位,pexpire都是以毫秒为单位的。
1 | EXPIRE key seconds //将key的生存时间设置为ttl秒 |
虽然有多种不同单位和不同形式的设置命令,但是实际都是使用PEXPIREAT命令实现的。
命令将的转换如下:
保存过期时间
通过过期字典保存 - redisDb结构的字典expires属性
计算并返回剩余生存时间
使用TTL/PLLT,通过计算键的过期时间和当前时间的差来实现。
过期键
过期键删除策略
定时删除:占用太多CPU时间,影响服务器的响应时间和吞吐量
惰性删除策略:浪费太多内存,有内存泄露的危险
定期删除:难点在于确定删除操作执行的时长和频率
Redis过期键删除策略: 惰性删除 + 定期删除
惰性删除
惰性删除由expireIfNeeded()函数实现,所有读写数据库命令在执行前都会调用该函数。
定期删除
定期删除由activeExpireCycle()函数实现,在规定时间内,分多次遍历各个数据库,每次都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中过期键。
注:
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下次调用时使用。
- 当current_db变量被重置为0,说明所有数据都被检查了一遍。
RDB对过期键的处理
- 生成RDB文件
在执行SAVE/BGSAVE命令创建一个新的RDB文件时,已过期的键不会被保存到文件中。 - 载入RDB文件
主服务器模式运行,会检查,过期键不会被载入到数据库中。
从服务器模式运行,不会检查,所有键都会被载入到数据库中。但是由于后期数据要进行主从同步操作,因此过期键对载入RDB文件的从服务器也不会造成影响。
AOF对过期键的处理
- AOF重写
执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。 - AOF写入
当一个过期键被删除后,服务器会追加一条DEL命令到现有AOF文件的末尾,显示删除过期键。
主从服务器对过期键的处理
- 当主服务器删除一个过期键后,它会将所有从服务器发送一个DEL命令,显示删除过期键。
- 从服务器要等待主节点发来DEL命令,才能删除过期键;否则即使发现过期键,也不会删除它。即:一切行动听主服务器指挥。
数据库通知
分类
- 键空间通知: 关注”某个键执行了什么命令“
- 键事件通知: 关注“某个命令被什么键执行了”
发送通知
- 服务器可以通过配置notify-keyspace-events选项来决定发送通知的类型。
AKE:所有类型所有通知
AK:所有类型键空间通知
AE:所有类型键事件通知
K$:字符串键的键空间通知
E1:列表键的键事件通知 等等 - 由notifyKeyspaceEvent()实现,参数type是要所发送的通知类型,程序会判断是否是服务器配置notify-keyspace-events选项,从而决定是否发送通知。
Redis 真的是单线程吗?
redis6.* 版本,引用了IO Threads概念,处理数据还是单线程的,但是IO读写引入了另外两个线程,提高了在多核机器的CPU利用率。
单线程基本模型
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有到达服务端的命令都不会立刻执行,所有的命令都会进入一个队列中,然后逐个执行,并且多个客户端发送的命令的执行顺序是不确定的,但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
为什么不采用多进程或多线程处理?
1.多线程处理可能涉及到锁2.多线程处理会涉及到线程切换而消耗CPU
Redis不存在线程安全问题?
Redis采用了线程封闭的方式,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis操作(即:多个Redis操作命令)的复合操作来说,依然需要锁,而且有可能是分布式锁
单线程处理的缺点?
1.耗时的命令会导致并发的下降,不只是读并发,写并发也会下降2.无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善
Redis的单线程为什么这么快?
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
使用多路I/O复用模型,非阻塞I/O;
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 I/O 的时间消耗)Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;