Redis(二):RDB 、AOF原理细解

RDB概述
  RDB是Redis默认的持久化机制,RDB模式下每间隔一定时间,Redis就会将内存中的数据集快照(snapshot)写入到磁盘文件,文件存储路径由dir + dbfilename决定。当redis重启或需要恢复数据时,同样可以根据dir + dbfilename找到对应rdb文件,把快照数据加载进内存。默认有以下三种情况会自动触发RDB操作,此外bgsave、save、flushall(flushdb不会)、shutdown、sync主从复制等命令同样会触发RDB操作。

# RDB持久化策略: save  
# 默认有三个触发条件:900秒内有1个key发生变化、300秒内10个key发生变化、60秒内10000个key发生变化。
# 通过save ""或不配置save均可禁用该功能
save 900 1
save 300 10
save 60 10000

# 是否采用LZF算法对快照数据进行压缩存储,默认YES会消耗额外的CPU资源,设为no可节省资源,但是会导致快照比较大。
rdbcompression yes

# 是否对rdb进行数据校验,默认YES且不建议关闭。校验会增加大约10%的性能消耗,如果对性能比较重视,可以设为no
rdbchecksum yes

dbfilename dump6379.rdb

dir ./

  对于bgsave,Redis会fork一个新的子进程(父进程的副本)来创建快照,父进程继续处理客户端命令,在子进程创建快照的过程中,如果父进程有新的写入操作,这些操作只能在下次创建快照时再写入,save配置对应的命令就是bgsave 。 
  对于save,Redis不会产生新的子进程,在执行完快照存储之前会一直阻塞redis,不会响应任何其它命令,除非在没有足够内存执行bgsave,或者阻塞影响不大的情况下才应考虑用save,不然一般都不建议使用。  
  对于shutdown命令,Redis会执行一个save命令,阻塞所有客户端,不再执行任何的客户端命令,并在执行完save之后关闭服务。在开启了AOF的情况下,在关闭之前会调用fsync()立即把数据写入aof文件中。flushall对应执行的也是save命令,只是会先清空内存中的所有数据,然后再保存。
  当Slave服务器连接到Master服务器时,如果Master没有正在执行bgsave而且并非刚刚创建完快照,那么Master就会执行bgsave,具体的主从复制在下篇博客中将作详细说明。

 关于触发bgsave的相关源码如下,server结构体位于server.h,触发RDB的源码位于server.c/serverCron()

struct redisServer { 
    ...
    /* AOF persistence */
    ...  
    int aof_rewrite_perc;           /* aof重写的比例条件 */
    off_t aof_rewrite_min_size;     /* aof重写的大小条件. */
    off_t aof_rewrite_base_size;    /* 上次重写后aof文件的大小. */
    off_t aof_current_size;         /* AOF当前大小 */    
    ...
    sds aof_buf;                    /* aof缓冲区 */
    ...
    /* RDB persistence */
    long long dirty;                /* 上次保存后,数据库被更新次数,包括对值的修改 */
    ...
    struct saveparam *saveparams;   /* 触发条件,对应配置的save 900 100等等 */
    ...
    time_t lastsave;                /* 上一次执行save/bgsave的时间 */
    ...
};
/* Redis每秒调用10次serverCron(100毫秒一次),来执行一些需要周期性或及时的检测 */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ......

    /* Check if a background saving or AOF rewrite in progress terminated. */
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        ...
    } else {
        /* 迭代配置的save参数列表 */
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;

            /* 校验key更新的个数,和最后一次保存至今的时间,满足任意一个条件则执行bgsave */
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 CONFIG_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == C_OK))
            {
                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, (int)sp->seconds);
                rdbSaveBackground(server.rdb_filename);
                break;
            }
         }
         /* 从这里也可以看出aof重写与rdb不会同时执行 */
         /* 校验aof文件的大小及条件,如有必要则重写aof */
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc &&
             server.aof_current_size > server.aof_rewrite_min_size)
         {
            long long base = server.aof_rewrite_base_size ?
                            server.aof_rewrite_base_size : 1;
            long long growth = (server.aof_current_size*100/base) - 100;
            if (growth >= server.aof_rewrite_perc) {
                serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                rewriteAppendOnlyFileBackground();
            }
         }
    }
    ......
}

触发源码基本可概括如下:
   Redis每100毫秒调用一次serverCron()函数,获取自上次保存至当前时间的时间差,及数据库被更新的次数,如果时间差和次数都匹配其中一个配置的save条件,则执行一次bgsave。

RDB:bgsave与save应用
   当redsi存储的数据量只有几个G的时候,一般快照的效率还是可以的,但当Redis的数据量越来大的时候(比如几十上百G),bgsave在生成快照方面花费的时间就会越来越长,一方面是由于bgsave通过复制父进程的方式fork出一个子进程,在这么大数据的情况下,仅这个复制过程可能就会花费好几甚至上十秒;另一方面是由于大量的磁盘读写,而且存在父子进程抢夺资源频繁切换的情况,所以在这种场景下bgsave可能导致系统停顿的时间很长,客户端响应延迟甚至超时。
   这种情况下,对于持久化数据导致的停顿,优化空间并不大,但从业务上来说,通过减少bgsave的次数,把bgsave的执行时间设置为客户端请求最少的某个时间点可以降低整体影响,具体做法比如禁用save配置,以脚本的方式指定执行bgsave的时机。这种场景下更建议用save替换bgsave,首先save不会有fork子进程的消耗,就不会有父子进程抢夺资源的情况,所以save整个快照过程会比bgsave要快,而且由于选定的时间点操作的客户端很少,一定的阻塞也是可以接受的。
  出于性能及竞争的考虑,Redis不允许同时执行两个bgsave,或同时执行save。(而且bgsave与bgrewriteaof也不会同时执行,bgsave执行期间,对于bgrewriteaof会延迟到bgsave执行完毕之后; bgrewriteaof执行期间会拒绝bgsave命令)

RDB优缺点
优点:适合大规模的数据的保存与恢复,对数据完整性和一致性要求不高的情况下可以采用。
缺点:当系统出现故障的时候,将丢失最后一次快照数据创建之后的数据。比如假设redis在10:00的时候已经创建完了一次快照,在10:30-35之间会开始并执行完第二次快照,如果系统在35之前崩溃导致Redis无法完成快照操作,10:00之后所有的写入数据都会丢失。


AOF概述
  由于RDB方式存在丢失最后一次数据的可能性,所以Redis提供了AOF机制来弥补这一问题,AOF以日志形式记录对Redis的所有写操作,当Redis处理一个写操作时,会将写操作以协议的方式追加到aof_buffer(aof缓冲区中)和写入aof文件中,但并不一定马上同步磁盘文件,而是根据配置的fsync策略决定同步时机。Redis在重启之时通过读取该日志文件重构最新数据。
       AOF文件的写入与同步并不是同一概念,写入并不一定就同步了磁盘,在现代操作系统中为了提高写效率,当用户向某个文件中写入数据时,系统通常会先将写入的数据保存在一个内存缓冲区中,当缓冲区满或有其它的触发条件触发的情况下,才会同步到磁盘。这就相当于你打开一个“记事本”,写入文本但实际上并没有立即保存,而是需要你手动执行一下“Ctrl+S”一样。
  AOF默认为关闭状态,通过以下配置可以启用、设置aof文件名、持久化时机、重写机制等

appendonly yes

appendfilename "appendonly6379.aof"

# AOF持久化时机(fsync()函数可以通知操作系统立刻向硬盘写数据)
# always: 每次写操作都调用fsync()将缓冲区中数据写入并同步到aof文件中,由于需要同步磁盘,所以这种方式相对缓慢,但安全性要高。
# everysec:每秒执行一次fsync写入aof文件,等待磁盘同步,即能应对较高并发,且只存在丢失最后一秒数据的情况。
# no:不执行fsync,但通知操作系统,由系统在需要的把缓冲区中的数据写入aof中,不等待磁盘同步,所以这种方式是最快的,但安全性却也最差,是否丢失及丢失的数据量无法确定。
appendfsync everysec


# 理解这个参数需要先了解bgrewriteao重写机制,和bgsave类似bgrewriteao会在一个子进程中去进行aof的重写,从而不会阻塞主进程对其它命令的处理。
# 但是由于bgrewriteaof通常都会涉及到大量的磁盘操作,而且持续时间也比较长,当父进程也需要操作磁盘时,两者就可能产生竞争,从而可能导致父进程停顿,这个参数的出现就是为了解决这个问题。
# 设为yes的情况下相当于将appendfsync设置为no,并不会立刻执行磁盘操作,而只是写入了缓冲区,因此这样并不会造成阻塞,但是如果这个时候redis挂掉,就会丢失数据。(在Linux系统中默认最多会丢失30s的数据)
# 默认no的情况下,不会产生丢失,但却有可能阻塞。目前官方建议除非延迟到影响性能的程度,否则应该采用默认no。
no-appendfsync-on-rewrite no


# 这里配置的AOF重写的两个触发条件:1)当文件内容是上一次rewrite后的一倍 2)且文件大小大于64M时触发。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb


# 设置为yes, Redis在加载aof文件时,如果aof文件中的最后一条命令不完整或者有误,redis会自动截取只成功加载前面正确的数据。
# 设置为no,那么Redis将启动失败,这种情况需要手动用redis-check-aof 工具对aof文件进行修复。
aof-load-truncated yes

  配置中其实已经说明了触发场景,这里简单的说一下aof文件的生成,在开启了AOF模式的情况下,Redis在启动时会检测是否存在了aof文件(默认为appendonly.aof),如果没有则创建一个新aof文件,否则读取该日志文件重构最新数据。启动之后的写操作会再次追加到原有aof文件中。值得注意的是aof文件是在Redis启动的过程中创建的,在运行过程中Redis不会创建aof文件的,所以如果在启动之后删除了aof文件,那么所有命令都将丢失。

AOF重写原理
  由于 AOF以命令方式保存数据,所以很容易导致文件过大,因此Redis提供 了重写机制,当aof文件超过指定的阔值时(或通过bgrewriteaof命令),Redis将fork出一个子进程,由子进程将其内存中的数据以命令的形式保存到一个新的aop文件中,再覆盖掉旧的aof文件。由于重写aof并不会读取旧aof文件中的内容,而是把当前内存中的数据,以命令的形式记录到新的aof文件,所以新的aof文件中只会保留可恢复数据的最小指令集。整个过程其实和bgsave很类似,只是AOF以命令方式存储,而RDB存储的是数据而已。
  由于bgrewriteaof会涉及到大量的磁盘操作,而且持续时间一般比较长,在子进程往新aof文件中写入数据的过程中,如果父进程有新的写操作也需要写入到原aof中,就可能产生两个问题:1新的写操作无法被子进程得知;2两者由于竞争I/O可能导致停顿延迟,对了父进程而言由于还要处理其它命令相对来说延迟影响更加大。
  为了解决这种情况,Redis为aof的重写也提供了重写缓冲区,以及一个配置no-appendfsync-on-rewrite来控制在rewrite过程中,父进程如何处理写操作。在rewrite过程中,Redis会把写操作同时追加到aof缓冲区和aof重写缓冲区,aof缓冲区中的内容仍会根据fsync同步策略被同步到原aof磁盘文件中,对原有aof文件的处理照常进行。对于重写缓冲区中的数据,在重写完成后子进程会向父进程发送一个信号,父进程接收到该信号后再将aof重写缓冲区中的内容写入新aof文件中,最后重命名新的aof文件覆盖原有aof。(注:重写过程中,对于列表类型的数据,当项多于64个时,会分多次追加SADD或RPUSH等命令)

# 由于bgrewriteaof通常都会涉及到大量的磁盘操作,而且持续时间相对较长,当父子进程同行进行文件操作时将产生I/o竞争,仍然避免不了阻塞,这个参数可以控制rewrite过程中父进程的写行为方式。
# 设为yes的情况下相当于将appendfsync设置为no,并不会立刻执行磁盘操作,而只是写入了缓冲区,因此这样并不会造成阻塞,但是如果这个时候redis挂掉,就会丢失rewrite过程中的所有数据。(在Linux系统中默认最多会丢失30s的数据)
# 默认no的情况下,不会产生丢失,但却有可能阻塞。目前官方建议除非延迟到影响性能的程度,否则应该采用默认no。
no-appendfsync-on-rewrite no

AOF优缺点
优点:数据完整性和一致性很高,通过配置可以控制只存在丢失最后一秒的情况。
据点:由于存储的是命令,所以容易导致日志文件过大,重启恢复时速度也比rdb方式要慢。

RDB与AOF组合使用
  共存的情况下,出于数据完整性的考虑,Redis会以aof文件为主,通过加载aof文件来重构数据,如果aof文件出错则启动失败。aof出错的原因有很多种,比如生产过程中突然中断,写到一半网络断了,丢包等导致数据不完整,写入的格式不对等等,这种情况下可以通过redis-check-aof对aof文件进行格式检查即修复(同样dump文件也可以通过redis-check-dump进行修复)。


# 设置为yes, Redis在加载aof文件时,如果aof文件中的最后一条命令不完整或者有误,redis会自动截取只成功加载前面正确的数据。
# 设置为no,那么Redis将启动失败,这种情况需要手动用redis-check-aof 工具对aof文件进行修复。
aof-load-truncated yes

  对于RDB,官方认为可以充当一个备份数据库的角色,应对AOF可能存在的潜在Bug,启动失败时可以通过RDB快速重启。

性能建议
  使用RDB作为备份库,且建议只需在Slave上执行RDB(一般15分钟备份一次)。
  AOF重写后的最后一步(父进程将rewrite过程中产生的新数据写到新文件)造成的阻塞几乎是不可避免的,所以应该尽量减少AOF rewrite的频率,AOF重写的大小阈值64M对于生产环境来说一般远远不够,具体值可以根据并发量和系统环境来确定,基本上很多大型项目都会设置在5G以上。对于数据备份还可以通过Master-Slave来实现数据复制,这样也能减少IO消费,主从复制会在后面的博客中说明。

你可能感兴趣的:(Java高级,Redis,NoSql,Java中间件)