本文正在参加「金石计划 . 分割6万现金大奖」
引言
咱们都知道Redis是微服务架构中重要的基础数据库中间件,经过Redis能够将数据库中的数据缓存到内存中,当服务端有数据查询恳求的时分,能够直接从内存中获取数据。如此,一方面服务端能够获得比较快的数据恳求呼应,另一方面降低了后端关系数据库的事务恳求压力。可是正所谓尺有所短,寸有所长,Redis最大的优势便是内存数据也是最大的劣势,由于一旦服务器宕机或许服务器重启,内存中缓存的数据也会丢掉。针对这样的场景,Redis供给了三种数据耐久化机制,别离是AOF、RDB以及混合耐久化来应对这种反常状况。本文首要从Redis完结耐久化遇到的问题出发,站在规划者的视点考虑相关问题的解决思路。

AOF耐久化
AOF耐久化方法,即Append Only File,Redis经过记载履行修正操作指令这种记小本本的方法进行内存数据耐久化。当需求经过AOF日志进行康复数据时,Redis服务端发动后能够从日志文件中回放履行指令来完结内存数据康复。当然了,AOF日志中记载的都是修正的指令,查询指令不会修正数据所以不需求进行记载。
或许咱们都比较熟悉WAL(Write Ahead Log),即日志预写机制,它是数据库十分常用的确保数据操作原子性以及耐久性的技术手段。拿Mysql举栗子,Mysql的WAL体现在undo log以及redo log等这些日志文件中,数据库在履行修正操作的时分并不是马上将数据更新到磁盘上,而是先记载在日志中,首要意图是假定呈现反常,能够直接从redo log中进行数据康复,也便是说让Mysql知道上次意外产生的时分操作到底有没有成功,别的还能够将Mysql的随机写转换为顺序写,提高IO功能。可是AOF却不同,它是在Redis将数据写入内存之后,再将相关的操作指令写入AOF文件中。

那么问题来了,为什么Redis要采纳这种独特的数据记载方法,而不是业界常用的WAL的方法呢?其实能够从以下两个层面考虑原因。
(1)AOF文件中保存了履行缓存的指令,以便于确保在需求康复数据的时分能够进行指令重放康复数据,因而需求确保履行指令的合法性,而经过先缓存数据再进行指令追加日志的方法能够确保追加到AOF文件中的的指令都是合法有用的,redis在康复数据的时分不需求再去检查指令是否有用,进一步提高内存数据康复的功率。
(2)别的由于是在修正操作指令之后进行日志记载,日志记载的时分需求进行磁盘IO操作,因而不会堵塞其时的修正指令。
AOF文件内容是什么?
在搞清楚Redis为什么选用AOF文件记载修正指令之后,咱们再来看看AOF文件中到底包含了哪些内容。
redis> SET mufeng handsome
OK
Redis客户端与服务端之间选用RESP协议进行通讯,它是一种应用层协议,关于Redis这种以功率为寻求目标的中间件,通讯协议必定要简略高效。就上面一条缓存操作指令来说:set mufeng handsome 对应的RESP报文便是*33set3set6mufeng$8handsome,为了便利检查进行了手动换行。

咱们来拆解下报文中各个属性的意义,“*3”代表本次操作指令将由三个分布组成,每一部分都是经过”数字”的方法作为开端,后边为对应的指令、键或许值。如此处的”数字”的方法作为开端,后边为对应的指令、键或许值。如此处的”6″就表示后边的指令是一个6个字节的键值。所以,appenonly.aof文件中实践保存的便是这种格式的内容。

AOF有没有丢数据的危险?
上文提到Redis经过AOF文件完结内存数据耐久化,那么是不是就代表缓存数据保存就万无一失了?这样的耐久化方法还有没有数据丢掉的危险呢?咱们能够设想一下假定在操作完Redis之后,还没来得及将指令写入AOF文件就宕机了,那么这个操作指令就会丢掉,对应的缓存数据最新值也会丢掉。由于即使宕机反常康复之后,也没办法从AOF文件中履行丢掉的操作指令了。因而,写入AOF缓冲区的数据什么时分进行耐久化落盘,直接决议着AOF耐久化方法缓存数据丢掉的危险巨细。
三种AOF落盘战略
针对AOF缓存中的数据在什么机遇写入磁盘,Redis供给了三种AOF日志写入战略供用户进行挑选,经过后台线程履行不同机遇的AOF文件数据同步操作,在redis.conf装备文件中的装备项appendfsync能够进行装备。
【appendfsync:no】
Redis不用管AOF缓冲区的数据什么时分写入磁盘,将AOF缓冲区同步数据的操作权交给操作系统,操作系统决议什么时分将缓冲区的数据写入磁盘中。
【appendfsync:everysec】
当Redis将数据写入AOF缓冲区后,每隔1s将缓冲区的数据进行磁盘写入。
【appendfsync:always】
每履行一个修正指令,都需求将修正的指令进行落盘操作。
尽管Redis供给了这三种AOF日志落盘战略供用户进行挑选,可是这三种战略实践上各有优缺陷。
【appendfsync:no】假定设置了由操作系统进行AOF缓冲区数据写入,那么就相当于写数据的机遇完全交由操作系统来决议,此刻redis关于缓冲区数据并不能够控制。
【appendfsync:everysec】假定设置成每隔一秒进行缓存数据写入,尽管不会像同步写入那样存在必定的功能耗费,可是由于存在一秒的时间距离,假定在此期间呈现服务器宕机,那么就会损失这一秒的缓存数据。
【appendfsync:always】尽管能够基本完结数据不丢掉,可是由于每次进行内存数据修正都要进行落盘操作,因而在必定程度上会影响主线程功能。
详细采纳怎样的装备战略仍是要依据实践的事务场景来决议,一般引荐运用第二种装备战略【appendfsync:everysec】,在可靠性以及功能方面相对平衡一点。
AOF文件会越来越大吗?
在了解了AOF日志磁盘写入机遇之后,咱们持续来考虑下一个问题。无论采纳什么样的同步数据战略,终究都是要将修正指令写入AOF文件中,因而跟着时间的推移,这个文件必定会越来越大。那么假定文件变得很大之后,无论是文件数据新写入仍是Redis经过AOF文件进行数据康复,大文件的操作都会造成IO功能损耗。假定你是Redis的规划者,假定遇到这种状况你会怎样进行规划优化呢?我想无非有两个优化思路,一个是化整为零,一个是想办法缩小大文件。
化整为零
当单个文件过大时,咱们很容易想到的优化方法便是将这个大文件拆分为若干个小文件。这就比如系统中一旦呈现过千万数据库表的时分,咱们就要结合实践的事务场景考虑要不要进行分库分表了。所以假定单个AOF文件太大,那么是不是能够考虑将其依照固定巨细进行拆分,这样能够防止单个AOF文件过大的问题。那么Redis小于7.0版别为什么没有选用这种计划呢?首要是这种计划并不契合Redis寻求简略高效的规划思想。假定选用了这种数据分块的方法,那必定需求完结文件巨细检测、文件创立、文件索引维护等等一系列技术细节问题,关于低版别的Redis来说这些都太繁琐了,还不如一个AOF文件来的爽快。
PS:在最新的Redis 7.0版别中,Redis现已支撑多AOF文件分片机制,原始的单个AOF文件会被拆分为一个基础文件以及多个增量文件。新版别中之所以开端支撑多文件存储,我想也是跟着事务开展内存数据或许会很庞大,Redis规划者发现假定仍是运用单文件存储,大AOF文件操作以及数据康复都是一个挑战。

AOF重写
已然进行文件切割太繁琐了,那么就单个AOF文件来说怎样才干减小文件巨细呢?那就要从AOF文件的记载内容入手,经过上文咱们了解到AOF文件中实践存储了修正内存数据的操作指令,因而咱们在剖析完这些操作指令之后发现,当多条指令操作同一个key的时分,实践咱们需求的是最新的一条操作指令,除此之外的历史操作指令咱们并不需求关心。比如【set mufeng handsome】、【set mufeng cool】,假定先后履行了这两个指令,那么在终究康复数据的时分,只需康复【set mufeng cool】即可。因而AOF重写的本质便是兼并指令,也便是说将多条对同一key进行操作的指令进行兼并,实践便是运用最新的key值操作指令来替代之前一切关于这个key值的指令。
Redis经过fork子进程来完结AOF文件重写,因而在讲AOF重写进程之前,咱们需求先了解下什么是fork子进程的原理,这样愈加有利于咱们后边了解AOF文件重写的进程。
什么是fork?
fork函数是linux内核供给给用户创立进程的API,应用程序经过调用fork函数创立子进程,这个子进程能够和本来父进程干相同的工作,也能够和本来主进程干不同的工作,这首要取决于对应的参数。这个进程就比如孙悟空拔了一根自己的猴毛变出来一个和自己如出一辙的孙悟空。
因而在fork子进程的进程之中,子进程仿制了父进程的代码段、数据段、堆栈、页表等,一起子进程具有独立的虚拟内存空间(当然是从父进程那里仿制过来的)。如下所示,实践上fork()终究调用的是内核copy_process方法仿制进程。

父进程fork子进程的时分,子进程具有独立的虚拟内存空间,那么对应的物理内存空间是不是也是独立的呢?咱们都知道在计算机中,内存归于十分名贵的系统资源,所以大佬们在规划的时分都尽或许的削减内存空间占用然后提高系统资源利用率。fork子进程进程中用到的Copy-On-Write便是典型的内存资源管理优化机制,假定子进程仅仅读取数据不进行任何的数据写入,那么就和父进程共用内存空间。当子进程需求进行数据写入的时分,发现没有内控空间能够写入,此刻会触发一个系统中断来分配内存空间给子进程进行数据写入。

什么机遇触发AOF重写?
履行bgrewriteaof 指令
当咱们在客户端手动履行bgrewriteaof 指令后,能够触发AOF文件进行重写,对应Redis源码中进行重写的bgrewriteaofCommand 函数会检测检测是否满意进行重写的条件,首要检测以下两个条件:
【Condition1】:检测其时是否存在现已在履行的AOF重写子进程,假定存在的话Redis将不再履行AOF文件重写。
【Condition2】:检测其时是否存在现已在创立RDB文件的子进程,假定存在的话Redis将AOF文件重写任务置为待调度状况,后续假定满意了重写条件,则持续履行AOF文件重写任务。
也便是说,Redis检测到其时既没有AOF重写子进程也没有RDB文件创立子进程,那么就能够进行AOF文件重写。对应源码如下:
//of_child_pid(aof rewrite进程pid)、rdb_child_pid(rdb dump进程pid)
void bgrewriteaofCommand(redisClient *c) {
if (server.aof_child_pid != -1) {
//假定正在aof rewrite,回来过错信息
addReplyError(c,"Background append only file rewriting already in progress");
} else if (server.rdb_child_pid != -1) {
//假定正在rdb dump,为了防止磁盘压力,将aof重写计划状况置为1,后期再进行rewrite;
server.aof_rewrite_scheduled = 1;
addReplyStatus(c,"Background append only file rewriting scheduled");
}
//假定其时没有aof rewrite和rdb dump在进行,则调用rewriteAppendOnlyFileBackground开端aof rewrite。
else if (rewriteAppendOnlyFileBackground() == REDIS_OK) {
addReplyStatus(c,"Background append only file rewriting started");
} else {
//呈现反常回来过错。
addReply(c,shared.err);
}
}
超出装备阈值
假定Redis实例开启了AOF装备,一起装备了auto-aof-rewrite-percentage以及auto-aof-rewrite-min-size,假定超出了阈值会触发AOF重写。
//没有rdb子进程、没有aof重写子进程、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();
}
}
aof_rewrite_scheduled被设置为待调度状况
在bgrewriteaofCommand函数中,假定其时正在履行RDB dump操作,那么对应的aof待调度aof_rewrite_scheduled状况就会被置为1,其时RDB dump完结之后,会持续履行AOF重写操作。
AOF重写进程是怎样的?
经过上文的描述,咱们知道了Redis触发AOF重写的机遇,那么当触发重写之后的详细事务是怎样的呢?咱们一同看下AOF重写的大致流程:
(1)Redis主进程首要检查是不是存在rdb dump进程或许aof重写进程正在运转,假定不存在Redis主进程fork子进程进行aof文件重写;
(2)fork出来的子进程和本来的Redis主进程具有相同的内存数据,子进程遍历此刻的内存数据一起将内存数据写入到暂时的AOF文件中;
(3)主进程此刻依然能够接收客户端恳求,一起将新的缓存操作写入aof_buf以及aof_rewrite_buf中,依据对应的同步战略,将buf中的数据别离写入旧AOF文件以及暂时AOF文件中;
(4)重写完结之后,暂时AOF文件将替换原有的老的AOF文件,然后完结整个AOF重写。

AOF形式长处
1、AOF的耐久化战略愈加丰厚些,能够依据实践事务需求进行装备,因而相对来说在数据可靠性方面要愈加有优势一点。
2、AOF文件内容比较好了解,愈加便利了解事务缓存数据。
AOF形式缺陷
1、通常状况下,相同的缓存数据,AOF文件比RDB文件巨细要大一些。
2、在文件康复场景下,AOF要比DRB康复数据慢一些。
RDB耐久化
RDB(Redis Data Base),所谓的Redis内存数据快照便是某一时间Redis存于内存中的一切缓存数据,这就比如用手机相机拍照,记载其时的夸姣画面。Redis能够完结在固定时间距离后将内存中的缓存数据耐久化保存起来。这样即使是服务器宕机或许重启了,只需RDB快照文件还存在,快照文件中对应的缓存数据就不会丢掉,Redis重新发动后会重新加载RDB文件到内存中,快速康复缓存数据,经过这样的方法保障了缓存数据的可靠性。
RDB文件生成进程
咱们以bgsave为例子来看下Redis生成RDB文件的大致进程是怎样的。
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
long long start;
// 假定现已存在aof重写子进程以及rdb生成子进程则直接回来过错
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
...
// fork子进程进行RDB文件生成
if ((childpid = fork()) == 0) {
...
// 生成RDB文件
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
// 告诉父进程RDB文件生成结束
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
//子进程退出
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
//父进程事务逻辑
...
}
return C_OK;
}
(1)Redis主进程首要判别其时是否存在现已在履行的aof重写子进程以及rdb文件生成子进程,假定存在的话则直接进行回来。为什么要进行这样的判别呢?首要仍是从服务器功能方面进行考量,假定服务器有多个子线程在进行RDB耐久化操作,那么必定会对磁盘造成比较大的IO压力,假定服务器中还部署了其他服务甚至会影响其他服务的正常运转。
(2)Redis主进程fork子进程进行RDB文件生成操作,在fork的进程中,此刻的Redis主进程是堵塞的,不能呼应客户端恳求,子进程fork完结之后能够持续呼应客户端恳求。
(3)fork出来的子进程遍历内存数据进行RDB文件生成操作。
(4)假定此刻客户端的恳求需求修正缓存数据,那么如上面fork子进程的原理,经过COW机制,操作系统会开辟新的内存空间给Redis主进程进行新的缓存数据写入。
(5)子进程快照数据生成完结之后,替换本来老的RDB文件。

RDB触发机遇
Redis首要支撑两种耐久化操作来生成RDB文件,别离是save、bsave指令方法手动生成以及在装备文件中装备时间距离主动进行RDB文件生成。
手动指令触发
客户端连接到redis之后咱们能够经过save以及bsave指令进行RDB文件的当即创立,两者的差异如下:
save:经过主线程触发,会堵塞Redis事务,假定内存数据比较多的话,会导致长时间不能呼应外部恳求;
bsave:客户端履行bsave指令进行RDB耐久化,Redis主线程会fork子线程出来进行RDB文件耐久化操作,这样防止了主线程的堵塞即使正在耐久化操作依然能够呼应外部数据缓存恳求。
不过这儿值得注意的是,尽管fork子进程之后不会堵塞主进程,可是在fork的进程中会堵塞主进程,尤其是在内存数据比较大的时分,堵塞主进程的时间会更长。
装备主动触发
别的在Redis的装备文件redis.conf中,咱们能够装备依照必定的时间距离来进行RDB耐久化操作。如下装备:
save 900 1 save 300 10 save 60 10000
其他的触发RDB文件生成的操作这儿不再赘述了,像从节点履行全量数据同步的时分,也会触发主节点生成RDB文件发送给从节点。
RDB有没有丢数据的危险?
咱们不妨考虑下经过RDB文件进行缓存数据耐久化会有什么问题?存不存在丢掉缓存数据的危险?这种方法看上去是个还不错的耐久化解决计划,可是实践上隐藏着一些丢掉缓存数据的危险。为什么这么说呢?经过剖析RDB文件生成的机制咱们能够发现有两个地方存在缓存数据丢掉的或许性。
场景1:
由于Redis保存RDB快照文件的战略是依照装备的时间距离进行耐久化保存,也便是每隔一个时间距离Redis就会保存一个RDB文件。因而在内存数据有更新可是RDB保存时间没有到来的这段时间假定存在服务器宕机或许服务器重启的状况,此刻内存的数据就会存在丢掉的危险,由于Redis还没来得及将数据耐久化到RDB文件中。
场景1中最大的问题就RDB文件耐久化存在时间距离,而这个时间距离导致了新增的缓存数据存在丢掉的危险。那么是不是将时间距离降低到最小就能够了,比如一秒钟,即使在这一秒钟期间呈现反常状况,那缓存数据也仅仅丢掉这一秒钟的缓存数据,相对来说数据丢掉的状况可控一点。可是问题是假定真的每隔1s就保存一个RDB文件到服务器磁盘中,那不论是对Redis自身仍是Redis所在的服务器磁盘IO都是一种担负。

场景2:
跟着事务的不断开展,内存中的数据必定会越来越大,因而在fork子进程来生成RDB文件的进程中,需求仿制的数据会相同越来越多,耗费的时间也会越来越多,进而堵塞主进程的时间也会越来越多。假定呈现长时间堵塞主进程的状况,那么Redis实例必定无法呼应客户端的数据操作恳求,终究导致内存数据没有进行及时更新,然后呈现丢掉缓存数据的危险。
RDB形式长处
1、相比AOF在康复数据的时分需求一条条回放操作指令,经过RDB文件康复数据功率更高;
2、合适全量备份内存数据场景。
3、相同规模的内存数据,RDB文件数据愈加紧凑,磁盘空间占用更小。
4、能够依据不同的时间距离保存RDB文件,在康复数据的时分能够愈加灵敏地挑选对应版别数据进行康复。
RDB形式缺陷
1、由于RDB数据保存存在必定的时间距离,因而存在丢掉缓存数据的危险;
2、fork子进程进行RDB文件生成,由于是一次性生成一个内存快照文件,关于服务器磁盘IO以及Redis自身来说都归于重操作,或许会对服务器的磁盘IO造成压力。
混合耐久化
已然AOF以及RDB耐久化都有这样或许那样的缺乏,那么有没有一种耐久化计划能够兼顾二者的长处来扬长避短呢?从4.0版别开端,Redis支撑混合耐久化的方法来兼顾功率以及数据可靠性。在Redis装备文件redis.conf中装备混合耐久化:
aof‐use‐rdb‐preamble yes
假定装备了混合耐久化,那么Redis主进程在fork子进程进行耐久化操作的时分,原先的将内存数据转换为操作指令的进程将替换为运用进行AOF重写时对应的RDB文件内容直接放入到重写后的暂时文件中,后边再有新的操作指令,都追加到暂时aof文件中,重写完结后运用暂时aof文件替换旧的文件。

混合耐久化形式长处
1、一起具有RDB以及AOF机制的长处,在数据可靠性以及数据康复功率上面达到了很好的平衡。
混合耐久化形式缺陷
1、由于Redis从4.0版别才开端支撑混合耐久化,假定其时渠道中的Redis版别低于4.0,那么就无法运用这个耐久化机制,因而兼容性不够友爱;
总结
本文首要剖析了Redis AOF、RDB以及混合耐久化的内存数据耐久化的机制原理,一起剖析了两种耐久化方法的长处以及缺陷。我想只要了解了中间件的特性机制原理,知道了特性的长处以及缺乏咱们才干规划合适咱们渠道的缓存数据耐久化战略,然后提高渠道的稳定性。
别的在一些优异中间件的学习和运用进程中,咱们不能仅仅停留在会用的层面,更应该深化底层体会其架构和完结机制的规划思路,只要搞明白规划思路,时间站在规划者的视点来看待遇到的问题,那么在咱们的实践工作中,假定遇到类似的问题咱们能够借鉴这些优异中间件的解决思路来进行问题剖析。
本文正在参加「金石计划 . 分割6万现金大奖」