一、备份机制
AOF
、RDB
和复制功能对于过期键的处理:
①RDB对过期键的处理机制
在执行SAVE
命令或者BGSAVE
命令创建一个新的RDB
文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB
文件中。
举个例子,如果数据库中包含三个键k1、k2、k3
,并且k2
已经过期,那么当执行SAVE
命令或者BGSAVE
命令时,程序只会将k1
和k3
的数据保存到RDB
文件中,而k2
则会被忽略。
因此,数据库中包含过期键不会对生成新的RDB
文件造成影响。
载入RDB
文件:
在启动Redis
服务器时,如果服务器开启了RDB
功能,那么服务器将对RDB
文件进行载入:
- 如果服务器以主服务器模式运行,那么在载入
RDB
文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB
文件的主服务器不会造成影响; - 如果服务器以从服务器模式运行,那么在载入
RDB
文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB
文件的从服务器也不会造成影响;
综上所述:持久化时已过期的键不会保存到新生成的rdb
文件中;
redis
服务器(主服务器)启动时,载入持久化的数据到内存时,已过期的键不会载入到内存;
redis
服务器(从服务器)启动时,载入持久化的数据到内存时,已过期的键也会载入到内存,但是当与主服务器进行数据同步时,从服务器的数据会被清空。
因此不管是写入rdb
文件还是读取rdb
文件的数据过期键都是无效的。
②AOF
对过期键的处理机制
当服务器以AOF
持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF
文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者定期删除之后,程序会向AOF
文件追加(append
)一条DEL
命令,来显式地记录该键已被删除。
举个例子,如果客户端使用GET message
命令,试图访问过期的message
键,那么服务器将执行以下三个动作:
1)从数据库中删除message
键。
2)追加一条DEL message
命令到AOF
文件。(根据AOF
文件增加的特点,AOF
只有在客户端进行请求的时候才会有这个DEL
操作)
3)向执行GET
命令的客户端返回空回复。
这部分就是Redis
中的惰性删除策略中expireIfNeeded
函数的使用。关于惰性删除策略这一部分在Redis
惰性删除策略一篇中有讲。所以这里就不赘述了。
需要提示一下的是:expireIfNeeded
函数是在db.c/lookupKeyRead()
函数中被调用,lookupKeyRead
函数用于在执行读取操作时取出键key
在数据库db
中的值。
AOF
重写:
和生成RDB
文件时类似,在执行AOF
重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF
文件中。
举个例子,如果数据库中包含三个键k1、k2、k3
,并且k2
已经过期,那么在进行重写工作时,程序只会对k1
和k3
进行重写,而k2
则会被忽略。这一部分如果掌握了AOF
重写的方法的话,那就自然理解了。
综上所述:因为aof
保存的操作命令,所以键的读写操作命令都会被保存在aof
文件中,当键过期时,通过惰性删除或者定期删除之后,aof
会保存相应的删除命令,显式记录该键已经被删除。
redis
服务器通过aof
文件方式将持久化数据load
到内存时,遇到过期键的处理机制和rdb
方式将持久化数据load
到内存时的处理方式一样。
因此不管时写入aof
文件还是读取aof
文件的数据过期键都是无效的。
③复制功能对过期键的处理机制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个
DEL
命令,告知从服务器删除这个过期键; - 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键;
- 从服务器只有在接到主服务器发来的
DEL
命令之后,才会删除过期键。
举个例子,有一对主从服务器,它们的数据库中都保存着同样的三个键message
、xxx
和yyy
,其中message
为过期键,如图所示:
如果这时有客户端向从服务器发送命令GET message
,那么从服务器将发现message
键已经过期,但从服务器并不会删除message
键,而是继续将message
键的值返回给客户端,就好像message
键并没有过期一样。
假设在此之后,有客户端向主服务器发送命令GET message
,那么主服务器将发现键message
已经过期:主服务器会删除message
键,向客户端返回空回复,并向从服务器发送DEL message
命令,如图所示:
从服务器在接收到主服务器发来的DEL message
命令之后,也会从数据库中删除message
键,在这之后,主从服务器都不再保存过期键message
了,如图所示:
综上所述:当服务器运行在复制模式下,从服务器的过期键的删除动作有主服务器控制;
主服务器在删除一个过期键之后,会显式的向所有从服务器发送一条DEL
命令,告知从服务器删除这个过期键
从服务器在处理客户端发送过来的查询命令时,即使碰到过期键也不会将过期键删除,而是像处理未过期键一样来处理这个过期键
从服务器只有在接收到主服务器发送过来的删除命令之后,才会删除过期键。
RDB持久化:
RDB
持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB
文件中。RDB
持久化功能所生成的 RDB
文件是一个经过压缩的二进制文件,通过该文件可以还原生成 RDB
文件时的数据库状态。
有两个 Redis
命令可以用于生成 RDB
文件,一个是 SAVE
,另一个是 BGSAVE
:
-
SAVE
命令会阻塞Redis
服务器进程,直到RDB
文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求; -
BGSAVE
命令会派生出一个子进程,然后由子进程负责创建RDB
文件,父进程继续处理命令请求。
RDB 文件的载入工作是在服务启动时自动执行的,所以 Redis 并没有专门用于载入 RDB 文件的命令,只要 Redis 服务器在启动时检测到 RDB 文件存在,它就会自动载入 RDB 文件
因为 AOF 文件的更新频率通常比 RDB 文件的更新频率高,所以如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态
只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态
SAVE
命令和BGSAVE
命令执行时的服务器状态:
①SAVE 命令执行时的服务器状态:
- 当 SAVE 命令执行时,Redis 服务器会被阻塞,所以当 SAVE 命令正在执行时,客户端发送的所有命令请求都会被拒绝;
- 只有在服务器执行完 SAVE 命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。
②BGSAVE 命令执行时的服务器状态:
- 子进程创建 RDB 文件的过程中,Redis 服务器仍然可以继续处理客户端的命令请求,但是,在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同;
- 首先,在 BGSAVE 命令执行期间,客户端发送的 SAVE 命令会被服务器拒绝,服务器禁止 SAVE 命令和 BGSAVE 命令同时执行是为了避免父进程和子进程同时执行两个 rdbSave 调用,防止产生竞争条件;
- 其次,在 BGSAVE 命令执行期间,客户端发送的 BGSAVE 命令会被服务器拒绝,因为同时执行两个 BGSAVE 命令也会产生竞争条件;
- 最后,BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行
- 如果 BGSAVE 命令正在执行,那么客户端发送的 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行
- 如果 BGREWRITEAOF 命令正在执行,那么客户端发送的 BGSAVE 命令会被服务器拒绝
- 因为 BGREWRITEAOF 和 BGSAVE 两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑一一并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作
间隔性自动保存:
因为 BGSAVE 命令可以在不阻塞服务器进程的情况下执行,所以 Redis 允许用户通过设置服务器配置的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令
用户可以通过 save 选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 BGSAVE 命令
举个例子,如果我们向服务器提供以下配置:
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,BGSAVE 命令就会被执行:
- 服务器在 900 秒之内,对数据库进行了至少 1 次修改
- 服务器在 300 秒之内,对数据库进行了至少 10 次修改
- 服务器在 60 秒之内,对数据库进行了至少 10000 次修改
设置保存条件:
通过设置 Redis 服务器配置的 save 选项,可以让服务器每隔一段时间自动执行一次 BGSAVE 命令;可以配置多个规则,只要满足其中一个规则就执行
服务器程序会根据 save 选项设置服务器状态 redisServer 结构的 saveparams 属性
检查保存条件是否满足:
Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查 save 选项所设置的保存条件是否已经满足,如果满足的话,就执行 BGSAVE 命令
算法实现:
遍历并检查 saveparams 数组中所有的保存条件,逐一与服务器状态的 dirty 属性和 lastsave 属性进行比较,只要有任意一个条件满足就执行 BGSAVE 命令
AOF持久化:
RDB持久化是将进程数据写入文件,而AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。
与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案。
Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:
appendonly yes//开启AOF
执行流程:
由于需要记录Redis的每条写命令,因此AOF不需要触发,下面介绍AOF的执行流程。AOF的执行流程包括:
命令追加(append):将Redis的写命令追加到缓冲区aof_buf;
文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;
文件重写(rewrite):定期重写AOF文件,达到压缩的目的。
(1)命令追加:
Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。
命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点;具体格式略。在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。
(2)文件写入和文件同步:
Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数,说明如下:
为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。
AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:
always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。
no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。
everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。
二、删除机制
1、键的生存时间和过期时间
设置生存时间或者过期时间:
①expire expire key 5
该键值对秒后过期,pexpire
以毫秒精度为数据库中的某个键设置生存时间;
②expireat expireat key XXX
该键值对在某时某刻过期,时间戳,pexpireat
将键key
的过期时间设置为timestamp
所指定的毫秒数时间戳;
③通过TTL/PTTL
查询该键值对剩余的生存时间(单位分别是秒和毫秒)。
设置时间戳的底层实现:expire
pexpire
expireat
都是转换为pexpireat
来实现的。
当键值对从dict
中删除时候,也要删除该键值对的过期 expire
:
persist
命令移除一个键值对的过期值,pexpierat
的反操作。
2、过期键的删除策略
如何判定一个键已经过期:
①检查给定的键值对(键)是否存在于过期字典中,如果不存在直接返回,如果存在,则获取过期时间
②检查当前时间UNIX
是否大于给定时间,如果大于则过期;
③通过TTL/PTTL
获取时间戳,查看该键值对所对应的值是否大于0,如果是则代表存活 。
实际过程中,访问过期字典会比执行一个命令稍微快一些。
过期键的删除策略:
a.定时删除:
在设置键过期时间的同时,创建一个定时器(Timer),让定时器在键的过期时间来临时,立即执行对键的删除操作;
b.定期删除:
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键值对;
c.懒惰删除:
放置不理,但每次查找时候,都会首先检查该键值对是否设定过期,如果设定则判断是否过期,如果过期删除键值对 以及时间过期。然后就返回空。
三种删除策略的特点:
定时删除是对内存非常友好,但是对CPU
不友好,降低吞吐量;
懒惰删除是对内存最不友好,但对CPU
最友好,如果某一个键值对一直没有访问会造成内存溢出问题;
定期删除是前两种策略的折中。因为定期删除每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对于CPU时间的影响;除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
分析完三种删除策略的特点后,redis
实际中对于键的删除策略是惰性删除和定期删除两种。通过配合使用这两种删除策略,服务器可以很好的合理使用CPU时间和避免浪费内存空间之间取得平衡。
三、淘汰机制
为什么会有淘汰?
Redis
可以看作是一个内存数据库,可以通过Maxmemory
指令配置Redis
的数据集使用指定量的内存。设置maxmemory
为0
,则表示无限制(这是64
位系统的默认行为,而32
位系统使用3GB
内隐记忆极限)。
当内存使用达到maxmemory
极限时,需要使用某种淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
如果需要设置100mb
的内存,则需要这么设置maxmemory
:
maxmemory 100mb
当redis
设置了合适的maxmemory
时,就需要选择一个淘汰算法策略(置换策略)。
常用的淘汰算法:
FIFO
:First In First Out
,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰;
LRU
:Least Recently Used
,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰;
LFU
:Least Frequently Used
,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
Redis3.0
提供的淘汰策略:
noeviction
: 不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都会返回error
;allkeys-lru
: 优先删除掉最近最不经常使用的key
,用以保存新数据;volatile-lru
: 只从设置失效(expire set
)的key
中选择最近最不经常使用的key
进行删除,用以保存新数据;allkeys-random
: 随机从all-keys
中选择一些key
进行删除,用以保存新数据;volatile-random
: 只从设置失效(expire set
)的key
中,选择一些key
进行删除,用以保存新数据;volatile-ttl
: 只从设置失效(expire set
)的key
中,选出存活时间(TTL
)最短的key
进行删除,用以保存新数据。
通常,根据经验会有这些置换方法:
- 在所有的
key
都是最近最经常使用,那么就需要选择allkeys-lru
进行置换最近最不经常使用的key
,如果你不确定使用哪种策略,那么推荐使用allkeys-lru
; - 如果需要循环读写所有的
key
, 或者各个key
的访问频率差不多,那么可以选用allkeys-random
策略去置换数据; - 假如要让
Redis
根据TTL
来筛选需要删除的key
, 请使用 volatile-ttl 策略。
置换策略工作原理:
- 客户端执行一条新命令,导致数据库需要增加数据(比如set key value)
- Redis会检查内存使用,如果内存使用超过 maxmemory,就会按照置换策略删除一些 key
- 新的命令执行成功
我们持续的写数据会导致内存达到或超出上限 maxmemory
,但是置换策略会将内存使用降低到上限以下。
如果一次需要使用很多的内存(比如一次写入一个很大的set
),那么,Redis
的内存使用可能超出最大内存限制一段时间。
近似LRU
算法:
Redis
中的 LRU
不是严格意义上的LRU
算法实现,是一种近似的 LRU
实现,主要是为了节约内存占用以及提升性能。Redis
有这样一个配置 —— maxmemory-samples
,Redis
的 LRU
是取出配置的数目的key
,然后从中选择一个最近最不经常使用的key 进行置换,默认的
5`,如下:
maxmemory-samples 5
Redis
不采用真正的 LRU
实现的原因是为了节约内存使用。虽然不是真正的 LRU
实现,但是它们在应用上几乎是等价的。下图是Redis
的近似LRU
实现和理论LRU
实现的对比:
测试开始首先在 Redis
中导入一定数目的key
,然后从第一个key
依次访问到最后一个key
,因此根据 LRU
算法第一个被访问的 key
应该最新被置换,之后再增加50%
数目的 key
,导致 50%
的老的key
被替换出去。
在上图中你可以看到三种类型的点,组成三种不同的区域:
- 淡灰色的是被置换出去的key
- 灰色的是没有被置换出去的key
- 绿色的是新增加的key
理论LRU
实现就像我们期待的那样,最旧的 50%
数目的 key
被置换出去,Redis
的LRU
将一定比例的旧key
置换出去。
可以看到在样本数为5
的情况下,Redis3.0
要比Redis2.8
做的好很多,Redis2.8
中有很多应该被置换出去的数据没有置换出去。在样本数为10
的情况下,Redis3.0
很接近真正的LRU
实现。
LRU
是一个预测未来我们会访问哪些数据的模型,如果我们访问数据的形式接近我们预想——幂律,那么近似 LRU
算法实现将能处理的很好。
在模拟测试中我们可以发现,在幂律访问模式下,理论 LRU
和 Redis
近似LRU
的差距很小或者就不存在差距。
如果你将 maxmemory-samples
设置为10
,那么 Redis
将会增加额外的CPU
开销以保证接近真正的 LRU
性能,可以通过检查命中率来查看有什么不同。
通过 CONFIG SET maxmemory-samples
动态调整样本数大小,做一些测试验证你的猜想。