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