★4.AOF日志

假如有人问你:“你会把 Redis 用在什么事务场景下?”

我想你大概率会说:“我会把它当作缓存运用,由于它把后端数据库中的数据存储在内存中,然后直接从内存中读取数据,呼应速度会十分快。”

没错,这确实是 Redis 的一个普遍运用场景,可是,这里也有一个绝对不能忽略的问题:一旦服务器宕机,内存中的数据将悉数丢掉。

咱们很容易想到的一个解决方案是,从后端数据库康复这些数据,但这种办法存在两个问题.

一是,需求频频访问数据库,会给数据库带来巨大的压力;

二是,这些数据是从慢速数据库中读取出来的,功用必定比不上从 Redis 中读取,导致运用这些数据的使用程序呼应变慢。

所以,对 Redis 来说,完结数据的耐久化,防止从后端数据库中进行康复,是至关重要的。目前,Redis 的耐久化首要有两大机制,即 AOF(Append Only File)日志和 RDB 快照。在接下来的两节课里,咱们就别离学习一下吧。这节课,咱们先要点学习下 AOF 日志。

AOF 日志是怎么完结的?

★写后日志:先指令再日志,和WAL相反

提到日志,咱们比较了解的是数据库的写前日志(Write Ahead Log, WAL),也便是说,在实践写数据前,先把批改的数据记到日志文件中,以便毛病时进行康复。

不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先履行指令,把数据写入内存,然后才记载日志,如下图所示:

13.AOF日志

那 AOF 为什么要先履行指令再记日志呢?要回答这个问题,咱们要先知道 AOF 里记载了什么内容。

AOF日志内容:指令

传统数据库的日志,例如 redo log(重做日志),记载的是批改后的数据。

而 AOF 里记载的是 Redis 收到的每一条指令,这些指令是以文本办法保存的。

咱们以 Redis 收到“set testkey testvalue”指令后记载的日志为例,看看 AOF 日志的内容。

13.AOF日志

“*3”表明指令有三部分,每部分由[“$+数字”开头,后面紧跟着具体的指令、键或值]。

“数字”表明这部分中的指令、键或值一共有多少字节。

例如,“$3 set”表明这部分有 3 个字节,也便是“set”指令。

优点:指令履行成功再记日志,防止记载过错指令

可是,为了防止额外的检查开支,Redis 在向 AOF 里边记载日志的时分,并不会先去对这些指令进行语法检查。

所以,假如先记日志再履行指令的话,日志中就有或许记载了过错的指令,Redis 在运用日志康复数据时,就或许会出错。

而写后日志这种办法,便是先让体系履行指令,只有指令能履行成功,才会被记载到日志中,否则,体系就会直接向客户端报错。所以,Redis 运用写后日志这一办法的一大优点是,能够防止出现记载过错指令的状况。

除此之外,AOF 还有一个优点:它是在指令履行后才记载日志,所以不会堵塞当时的写操作。

危险1:写后日志或许会丢掉

不过,AOF 也有两个潜在的危险。

首先,假如刚履行完一个指令,还没有来得及记日志就宕机了,那么这个指令和相应的数据就有丢掉的危险。 假如此刻 Redis 是用作缓存,还能够从后端数据库重新读入数据进行康复,可是,假如 Redis 是直接用作数据库的话,此刻,由于指令没有记入日志,所以就无法用日志进行康复了。

所幸咱们一般状况下不会将Redis作为数据库运用。

危险2:写后日志会堵塞下一个操作

其次,AOF 尽管防止了对当时指令的堵塞,但或许会给下一个操作带来堵塞危险。这是由于,AOF 日志也是在主线程中履行的,假如在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,从而导致后续的操作也无法履行了。

仔细剖析的话,你就会发现,这两个危险都是和 AOF 写回磁盘的机遇相关的。这也就意味着,假如咱们能够操控一个写指令履行完后 AOF 日志写回磁盘的机遇,这两个危险就解除了。

★写回战略:Always、Everysec、No

其实,对于这个问题,AOF 机制给咱们供给了三个挑选,也便是 AOF 装备项 appendfsync 的三个可选值。

Always,同步写回:每个写指令履行完,立马同步地将日志写回磁盘;
​
Everysec,每秒写回:每个写指令履行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
​
No,操作体系操控的写回:每个写指令履行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作体系决定何时将缓冲区内容写回磁盘。

针对防止主线程堵塞和减少数据丢掉问题,这三种写回战略都无法做到两全其美。咱们来剖析下其间的原因。

★Always同步写回

主线程功用差,能够确保不丢数据

★Everysec每秒写回

功用折中,或许丢掉2秒数据:上一秒的数据还未落盘,下一秒的数据也未落盘
 同步写回”能够做到基本不丢数据,可是它在每一个写指令后都有一个慢速的落盘操作,不可防止地会影响主线程功用;
​
 尽管“操作体系操控的写回”在写完缓冲区后,就能够持续履行后续的指令,可是落盘的机遇现已不在 Redis 手中了,只需 AOF 记载没有写回磁盘,一旦宕机对应的数据就丢掉了。
​
 “每秒写回”选用一秒写回一次的频率,防止了“同步写回”的功用开支,尽管减少了对体系功用的影响,可是假如产生宕机,上一秒内未落盘的指令操作仍然会丢掉。所以,这只能算是,在防止影响主线程功用和防止数据丢掉两者间取了个折中。

★No操作体系操控写回

功用好,丢掉数据较多

我把这三种战略的写回机遇,以及优缺点汇总在了一张表格里,以方便你随时检查。

13.AOF日志

到这里,咱们就能够依据体系对高功用和高可靠性的要求,来挑选运用哪种写回战略了。

总结一下便是:想要获得高功用,就挑选 No 战略;假如想要得到高可靠性确保,就挑选 Always 战略;

假如允许数据有一点丢掉,又期望功用别受太大影响的话,那么就挑选 Everysec 战略。

★AOF日志文件过大

可是,依照体系的功用需求选定了写回战略,并不是“高枕无忧”了。究竟,AOF 是以文件的办法在记载接纳到的一切写指令。跟着接纳的写指令越来越多,AOF 文件会越来越大。这也就意味着,咱们一定要当心 AOF 文件过大带来的功用问题。

这里的“功用问题”,首要在于以下三个方面:

★文件巨细有限制

一是,文件体系自身对文件巨细有限制,无法保存过大的文件;

★追加功率变差

二是,假如文件太大,之后再往里边追加指令记载的话,功率也会变低;

★宕机康复过慢

三是,假如产生宕机,AOF 中记载的指令要一个个被重新履行,用于毛病康复,假如日志文件太大,整个康复进程就会十分缓慢,这就会影响到 Redis 的正常运用。所以,咱们就要采纳一定的操控手段,这个时分,AOF 重写机制就上台了。

解决日志文件过大:AOF重写机制

简单来说,AOF 重写机制便是在重写时,Redis 依据数据库的现状创建一个新的 AOF 文件,也便是说,读取数据库中的一切键值对,然后对每一个键值对用一条指令记载它的写入。

比方读取了键值对“testkey”: “testvalue”后,重写机制会记载 set testkey testvalue 这条指令。

这样,当需求康复时,能够重新履行该指令,完结“testkey”: “testvalue”的写入。

为什么重写机制能够把日志文件变小呢?

实践上,重写机制具有“多变一”功用。所谓的“多变一”,也便是说,旧日志文件中的多条指令,在重写后的新日志中变成了一条指令。咱们知道,AOF 文件是以追加的办法,逐个记载接纳到的写指令的。当一个键值对被多条写指令重复批改时,AOF 文件会记载相应的多条指令。

可是,在重写的时分,是依据这个键值对当时的最新状况,为它生成对应的写入指令。这样一来,一个键值对在重写日志中只用一条指令就行了,而且,在日志康复时,只用履行这条指令,就能够直接完结这个键值对的写入了。

13.AOF日志

当咱们对一个列表先后做了 6 次批改操作后,列表的最后状况是[“D”, “C”, “N”],此刻,只用 LPUSH u:list “N”, “C”, “D”这一条指令就能完结该数据的康复,这就节约了五条指令的空间。对于被批改过成百上千次的键值对来说,重写能节约的空间当然就更大了。

不过,尽管 AOF 重写后,日志文件会缩小,可是,要把整个数据库的最新数据的操作日志都写回磁盘,仍然是一个十分耗时的进程。

这时,咱们就要持续重视另一个问题了:重写会不会堵塞主线程?

★AOF日志重写不会堵塞主线程:一复制两日志

和 AOF 日志由主线程写回不同,重写进程是由后台子进程 bgrewriteaof 来完结的。

这也是为了防止堵塞主线程,导致数据库功用下降。

我把重写的进程总结为“一个复制,两处日志”。

★1处复制:主进程bgrewriteaof去fork子进程

“一个复制”便是指,每次履行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此刻,fork 会把主线程的内存复制一份给 bgrewriteaof 子进程,这里边就包括了数据库的最新数据。然后,bgrewriteaof 子进程就能够在不影响主线程的状况下,逐个把复制的数据写成操作,记入新的AOF重写日志。

★1处日志:旧的正在运用的AOF日志

由于主线程未堵塞,仍然能够处理新来的操作。此刻,假如有写操作,榜首处日志便是指正在运用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,能够用于康复。

★2处日志:新的 AOF 重写日志

第二处日志,便是指新的 AOF 重写日志。这个操作会被另外的线程写到重写日志的缓冲区。 这样,重写日志也不会丢掉最新的操作。比及复制数据的一切操作记载重写完结后,重写日志记载的这些最新操作也会写入新的 AOF 文件,以确保数据库最新状况的记载。此刻,咱们就能够用新的 AOF 文件替代旧的AOF文件了。

13.AOF日志

AOF重写完结详解

AOF重写并不需求对原有AOF文件进行任何的读取,写入,剖析等操作,这个功用是经过读取服务器当时的数据库状况来完结的。

当时列表键list在数据库中的值就为[“C”, “D”, “E”, “F”, “G”]。要运用尽量少的指令来记载list键的状况,最简单的办法不是去读取和剖析现有AOF文件的内容。那么直接读取list键在数据库中的当时值,然后用一条RPUSH list “C” “D” “E” “F” “G”替代前面的6条指令即可。

原理

首先从数据库中读取键现在的值,然后用一条指令去记载键值对,替代之前记载该键值对的多个指令; 伪代码表明如下;

def AOF_REWRITE(tmp_tile_name):
​
 f = create(tmp_tile_name)
​
 # 遍历一切数据库
 for db in redisServer.db:
​
  # 假如数据库为空,那么跳过这个数据库
  if db.is_empty(): continue
​
  # 写入 SELECT 指令,用于切换数据库
  f.write_command("SELECT " + db.number)
​
  # 遍历一切键
  for key in db:
​
   # 假如键带有过期时间,而且现已过期,那么跳过这个键
   if key.have_expire_time() and key.is_expired(): continue
​
   if key.type == String:
​
    # 用 SET key value 指令来保存字符串键value = get_value_from_string(key)
​
    f.write_command("SET " + key + value)
​
   elif key.type == List:
​
    # 用 RPUSH key item1 item2 ... itemN 指令来保存列表键
​
    item1, item2, ..., itemN = get_item_from_list(key)
​
    f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)
​
   elif key.type == Set:
​
    # 用 SADD key member1 member2 ... memberN 指令来保存调集键
​
    member1, member2, ..., memberN = get_member_from_set(key)
​
    f.write_command("SADD " + key + member1 + member2 + ... + memberN)
​
   elif key.type == Hash:
​
    # 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 指令来保存哈希键
​
    field1, value1, field2, value2, ..., fieldN, valueN =
    get_field_and_value_from_hash(key)
​
    f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +
             ... + fieldN + valueN)
​
   elif key.type == SortedSet:
​
    # 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
    # 指令来保存有序集键
​
    score1, member1, score2, member2, ..., scoreN, memberN = 
    get_score_and_member_from_sorted_set(key)
​
    f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +
             ... + scoreN + memberN)
​
   else:
​
    raise_type_error()
​
   # 假如键带有过期时间,那么用 EXPIREAT key time 指令来保存键的过期时间
   if key.have_expire_time():
    f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())
​
  # 关闭文件
  f.close()

实践为了防止履行指令时形成客户端输入缓冲区溢出,重写程序在处理list hash set zset等类型的数据时,会检查键所包括的元素的个数,假如元素的数量超越了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,该常量默认值是64。

那么重写程序会运用多条指令来记载键的值,而不是单运用一条指令。 即每条指令设置的元素的个数 是最多64个,运用多条指令重写完结调集键中元素数量超越64个的键;

AOF后台重写

aof_rewrite函数能够创建新的AOF文件,可是这个函数会进行很多的写入操作,所以调用这个函数的线程将被长时间的堵塞,由于Redis服务器运用单线程来处理指令恳求。

所以假如直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的指令恳求。

Redis不期望AOF重写会形成服务器无法处理恳求,所以Redis决定将AOF重写程序放到子进程(后台)里履行。这样处理的最大优点是: 子进程进行AOF重写期间,主进程能够持续处理指令恳求。

子进程带有主进程的数据副本,运用子进程而不是线程,能够防止在锁的状况下,确保数据的安全性。

运用子进程进行AOF重写的问题

子进程在进行AOF重写期间,服务器进程还要持续处理指令恳求,而新的指令或许对现有的数据进行批改,这会让当时数据库的数据和重写后的AOF文件中的数据不一致。

怎么批改

为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开端启用,Redis服务器主进程在履行完写指令之后,会一起将这个写指令追加到现有AOF文件和AOF重写缓冲区。 即子进程在履行AOF重写时,主进程需求履行以下三个工作: 履行client发来的指令恳求; 将写指令追加到现有的AOF文件中; 将写指令追加到AOF重写缓存中。

13.AOF日志

作用 能够确保:

AOF缓冲区的内容会定期被写入和同步到AOF文件中,对现有的AOF文件的处理工作会正常进行 从创建子进程开端,服务器履行的一切写操作都会被记载到AOF重写缓冲区中; 完结AOF重写之后 当子进程完结对AOF文件重写之后,它会向父进程发送一个完结信号,父进程接到该完结信号之后,会调用一个信号处理函数,该函数完结以下工作: 1,将AOF重写缓存中的内容悉数写入到新的AOF文件中;这个时分新的AOF文件所保存的数据库状况和服务器当时的数据库状况一致; 2,对新的AOF文件进行改名,新的AOF掩盖原有的AOF文件;完结新旧两个AOF文件的替换。 当这个信号处理函数履行完毕之后,主进程就能够持续像平常相同接纳指令恳求了。在整个AOF后台重写进程中,只有最后的“主进程写入指令到AOF缓存”和“对新的AOF文件进行改名,掩盖原有的AOF文件。”这两个步骤(信号处理函数履行期间)会形成主进程堵塞,在其他时分,AOF后台重写都不会对主进程形成堵塞,这将AOF重写对功用形成的影响降到最低。 以上,即AOF后台重写,也便是BGREWRITEAOF指令的工作原理。

原文链接:blog.csdn.net/m0_45861545…

小结

Redis经过AOF 办法逐个记载操作指令,用于防止数据丢掉的,在康复时再逐个履行指令的办法,确保了数据的可靠性。

这个办法看似“简单”,但也是充分考虑了对 Redis 功用的影响。总结来说,它供给了 AOF 日志的三种写回战略,别离是 Always、Everysec 和 No,这三种战略在可靠性上是从高到低,而在功用上则是从低到高。

此外,为了防止日志文件过大,Redis 还供给了 AOF 重写机制,直接依据数据库里数据的最新状况,生成这些数据的刺进指令,作为新日志。这个进程经过后台线程完结,防止了对主线程的堵塞。

其间,三种写回战略体现了体系设计中的一个重要原则 ,即 trade-off,或者称为“取舍”,指的便是在功用和可靠性确保之间做取舍。我认为,这是做体系设计和开发的一个关键哲学,我也十分期望,你能充分地了解这个原则,并在日常开发中加以使用。

不过,你或许也注意到了,落盘机遇和重写机制都是在“记日志”这一进程中发挥作用的。例如,落盘机遇的不挑选同步写回能够防止记日志时堵塞主线程,重写能够防止日志文件过大。

可是,在“用日志”的进程中,也便是运用 AOF 进行毛病康复时,咱们仍然需求把一切的操作记载都运转一遍。再加上 Redis 的单线程设计,这些指令操作只能一条一条按顺序履行,这个“重放”的进程就会很慢了。那么,有没有既能防止数据丢掉,又能更快地康复的办法呢?当然有,那便是 RDB 快照了。