第二部分 单机数据库的实现
第九章 数据库
- Redis将所有数据库都保存在redisServer结构体的db数组中,db数组的每一项都是一个redisDb结构,代表一个数据库。
struct redisServer{
//...
redisDb * db;
int dbnum //要创建的服务器数量,默认16
//...
}
- 每个redis客户端都有自己的target数据库,默认情况下都是0号库,可以使用
SELECT X
来切换数据库。 - redis是一个键值对数据库服务器,每个数据库对应一个redisDb结构体,redisDb结构体中的dict字典保存了所有的键值对,称为键空间。键空间的键是字符串对象,值可以是5大对象中的任意一个。对数据库的增删改查其实就是对这个dict字典进行增删改查。
当使用redis命令对数据库读写时,不仅会对键空间进行指定的操作,还会执行一些额外操作:
- 在读取一个键之后,会根据键是否存在来更新服务器键空间中的命中次数和不命中次数
- 在读取一个键之后,服务器会更新键的LRU时间
- 如果服务器在读取一个键的时候发现该键已经过期,则会先删除这个过期的键
- 如果有客户端使用WATCH监视了某个键,那么服务器会对被监视的键进行修改后,将其标记为dirty来让事务程序注意到键已被修改
- 服务器每修改一个键都会对dirty计数器+1,会触发持久化和复制操作
通过
EXPIRE
和PEXPIRE
可以用秒或者毫秒对key设置生存时间,服务器会自动删除生存时间(TTL)为0的key。
还可以通过EXPIREAT
和PEXPIREAT
对key设置过期时间,过期时间是UNIX时间戳,如1377257300,当时间到了就自动删除key。可以使用PERSIST
移除一个key的过期时间。
以上4个关于过期时间的命令实际上都是通过PEXPIREAT
来实现的。- redisDb结构中的expires字典保存了所有键的过期时间,称为过期字典。
- 过期键的删除策略
- 定时删除:在设置key的过期时间的同时,创建一个定时器,让定时器在过期时间到的时候删除。属于主动删除
- 惰性删除:过期就过期了,但是每次在键空间要访问该key时检查是否过期,如果过期了就删除。属于被动删除
- 定期删除:每个一定时间对数据库检查一次,删除过期的键。属于主动删除
- 定时删除:对内存最友好,但是对CPU时间不友好,会影响吞吐量和响应时间,而且创建定时器是O(n)的操作,所以大规模的定时删除是不现实的。
- 惰性删除:对CPU时间最友好,就是费内存,而且如果某些过期键不会被访问的话就永远不会被删除了,相当于内存泄漏。
- 定期删除是前两种方法的折中,难点是怎么确定定期删除的时长和频率。如果删除操作太频繁,就退化成定时删除了,如果执行的太少,就会浪费内存。
- redis的过期键删除策略:
- redis实际使用的是惰性删除和定期删除两种策略,通过结合使用这两种能在cpu时间和内存方面取得平衡的效果。
- 惰性删除没啥特殊的,就是在访问某key的时候如果过期了就由
expireIfNeeded
函数删除,然后返回不存在,如果没过期就正常访问。 - 定期删除:
activeExpireCycle
函数会周期执行,在规定时间内多次遍历服务器中的每个数据库,在expires字典中随机检查一部分key的过期时间,并删除过期的。
- AOF、RDB和复制功能对过期键的处理(RDB,AOF等之后会详细讲):
- 执行SAVE或者GBSAVE命令会创建一个新的RDB文件,程序会对数据库中的key进行检查,不会把过期的键保存在RDB文件中。当启动redis服务器时,如果开启了RDB功能,则会对RDB文件进行载入,如果是主服务器(master),程序会对文件中保存的key进行检查,未过期的key才会载入到数据库,所以未过期的key对master是不会造成影响的。如果是从服务器(slave),不论是否过期,所有的key都会被载入。不过因为master在数据同步的时候slave会被清空,所以说到底也没啥影响。
- 当服务器已AOF持久化模式运行时,如果某个键已经过期,但是还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生影响。当这个键被删除后,程序会向AOF文件append一条
DEL
命令来记录这个键已经被删除。在执行AOF重写时,会对数据库的键进行检查,过期的键不会保存到重写后的AOF文件中。 - 当服务器运行在复制模式下时,slave的过期删除动作由master控制。master在删除一个过期键后,会显式的向所有slave发送一个DEL命令来删除这个过期键。slave在执行客户端的读命令时,就算键过期了也不会删除,只有收到master的
DEL
命令之后才会删除。这样可以保证主从服务器的数据一致性。
第十章 RDB持久化
- 因为redis是内存数据库,所以一旦退出进程,数据库状态就没了,所以需要持久化。
- RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行。经过RDB持久化将生成一个RDB二进制文件,是通过压缩的,可以还原生成RDB文件时的数据库状态。
SAVE
和BGSAVE
两个命令都可以生成RDB文件。SAVE
命令会阻塞当前redis进程,直到RDB文件创建完毕,在阻塞期间,不能处理任何命令。BGSAVE
会创建一个子进程在后台创建RDB文件,此时redis主进程能继续处理请求。这两个命令都是通过rdbSave
函数完成。- redis服务器启动时就会自动载入RDB文件,所以没有专门的载入RDB文件的命令。
- 因为AOF文件的更新频率比RDB文件高,所以如果开启了AOF持久化功能的话会优先使用AOF文件来还原数据库状态。如果AOF功能关闭的话会使用RDB文件。
- 执行
BGSAVE
期间SAVE
命令会被拒绝,为了避免父进程和子进程都执行rdbSave
函数产生竞态条件。执行BGSAVE
期间BGSAVE
命令会被拒绝,同样因为竞态条件。执行BGSAVE
期间BGWRITEAOF
命令会阻塞,如果BGSAVE
在执行,则BGWRITEAOF
要等待BGSAVE
执行完,如果BGWRITEAOF
在执行,则BGSAVE
会被拒绝。 - 服务器在载入RDB文件时是出于阻塞态的。
- 用户可以设置多个保存条件来让服务器自动执行
BGSAVE
。比如save 900 1//表示如果900秒内发生了1次修改则执行BGSAVE
。这些条件保存在redisServer结构的saveparams
数组中,每个数组项就是一个saveparam,包含修改次数和时间。 - 除了saveparams数组,redisServer结构还维护一个dirty计数器和lasesave属性,dirty计数器记录上一次成功执行SAVE或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改。lastsave属性是一个时间戳,记录上一次执行SAVE或者BGSAVE命令的时间。
- RDB文件结构:了解一哈,没必要记。
第十一章 AOF持久化
- AOF(append only file)是通过保存redis服务器所执行的写命令来记录数据库状态的。
- 在服务器启动时,可以通过载入和执行AOF文件中保存的命令还远之前的数据库状态。
- AOF持久化功能可以分为append,文件写入,文件同步三个步骤。
- append:当AOF功能打开的状态下,服务器美执行一个写命令,都会以协议格式将被执行的写命令追加到
aof_buf缓冲区
的末尾。 - 文件写入和同步:服务器在每结束一个事件循环之前,都会调用
flushAppendOnlyFile
函数来考虑是否将aof_buf中的内容写入AOF文件中。flushAppendOnlyFile
函数的行为由服务器配置的appendfsync
选项来决定,具体有always
表示将aof_buf中所有内容都写入,并同步AOF文件;evertsec
表示将aof_buf所有内容写入,如果上次同步AOF文件时间距离当前超过1S,则再次同步;no
表示全部写入,但是不同步。默认的选项是evertsec
。
- append:当AOF功能打开的状态下,服务器美执行一个写命令,都会以协议格式将被执行的写命令追加到
- 现代操作系统调用
write
时一般都会讲数据写内存缓冲,知道缓冲区满或者大小超出才会真正写磁盘,这样可以提高效率。 appendfsync
的配置直接影响AOF的效率和安全性:如果是always
,那肯定是最安全的,因为每个命令都会写入并同步,但是效率会很慢;everysec
的话如果出现问题也就丢失一秒钟,可以接受,而且不会频繁的同步,效率也还行;no
的模式下什么时候对文件进行同步依赖于OS,所以安全性非常差,而且因为会在缓存中积累很多写入数据,所以单词的同步时间最久,而且很容易丢失很多命令,不过写入效率是最快的。- AOF重写:因为AOF是通过保存被执行的写命令来记录状态的,因此文件内容会越来越多,太大的话会影响性能。因为AOF重写功能可以创建一个新的AOF文件来替代现有的文件,而且不会包含冗余的命令。
- AOF重写并不需要对现有的AOF文件进行分析或者读取,而是根据当前的数据库状态来实现的。比如在list中先添加1个元素,在添加一个元素,初始的AOF文件中这就是2条命令,然而这两条命令可以压缩成直接添加2个元素的一条命令,所以AOF重写就是直接从数据库中读取list的键值,用一条语句就把当前的数据全部保存了。这就是AOF重写的原理。生成的AOF重写文件叫做aof_rewrite
- 当然,如果某些列表或者集合的元素太多了,超出了64(可以配置的选项),那么也不会强行用一条命令就全部搞定,可以用多条命令来记录,防止单条命令太长了。
- AOF重写是在子进程中执行的,主要目的是:子进程在AOF重写时,服务器进程还能继续处理请求;子进程是带有服务器进程的数据副本的,不用线程是因为这样可以避免加锁。
- AOF的存在问题是子进程和主进程并发的,会存在数据不一致性,因为主进程可以在子进程重写的时候又对数据库状态做修改了。为了解决这个,redis服务器设置了一个AOF重写缓冲区,当redis执行完一个写命令之后,会将这个写命令同时发送给AOF缓冲区和AOF重写缓冲区。这样可以保证AOF缓冲区的内容会被定期写入和同步到AOF文件,对现有AOF文件的处理工作照常进行;从创建子进程来时,所执行的所有写命令会被记录到AOF重写缓冲区中,当子进程完成重写后,会通知主进程,将AOF重写缓冲区中的所有内容写入新的AOF文件中,所以当前新的AOF文件保存的命令就和当前的数据库状态一致了。只有会对新的AOF文件进行改名,并替换当前的旧AOF文件(原子操作),整个重写过程就OK了。
第十二章 事件
- redis服务器主要处理两类事件:文件事件和时间事件。
- redis服务器通过套接字与client连接,文件事件就是对套接字操作的抽象,server和client质检的通信会产生相应的文件事件,而server就通过监听并处理这些事件来完成网络通信操作。
- server中的一些操作比如
serverCron
函数需要在指定的时间点运行,时间事件就是对这类定时操作的抽象。 - Redis基于Reactor模式来开发自己的网络事件处理器,称为文件事件处理器:
- 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行accept、read、write、closr等操作时,文件事件就会产生,文件事件处理器就会调用这些事件处理器来处理事件。
- 虽然文件事件处理器是以单线程的方式运行的,但是通过使用I/O多路复用程序监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好的与redis服务器中的其他单线程模块进行对接。
- I/O多路复用程序负责监听多个套接字,冰箱文件时间分派器传送那些产生了事件的套接字。尽管多个文件事件可能会并发的出现,但I/O多路复用程序会将所有产生事件的套接字都放入一个队列,通过这个队列以有序、同步、一次一个套接字的方式向分派器传送套接字,当这个套接字处理完毕,I/O多路复用程序才会分派下一个套接字。
- I/O多路复用程序的实现:通过包装常见的
select
、epoll
、evport
和kqueue
这些库函数实现的。 - redis的时间事件可以分为定时事件和周期事件。一般只执行
serverCron
这一个函数。 - 文件事件和时间事件是合作关系,会轮流处理这两种事件,不会发生抢占
第十三章 客户端
没啥特别的
第十四章 服务器
没啥特别的