一个可靠安全的系统,肯定要考虑数据的可靠性,尤其对于内存为主的redis,就要考虑一旦服务器挂掉,启动之后,如何恢复数据的问题,也就是说数据如何持久化的问题。redis保证数据的可靠性主要有两种策略:RDB,AOF.
1.RDB
redis以数据结构的形式将数据存放在内存中,为了让数据在redis服务器挂掉重启之后可以继续服务,那么就必须对数据进行持久化处理。
1.1 RDB文件格式
RDB文件格式如下所示:
- REDIS——开始REDIS 五个字符,标识着一个 RDB 文件的开始
- RDB-VERSION——一个四字节长的以字符表示的整数,记录了该文件所使用的 RDB 版本号。 不同版本的RDB文件是不兼容的,因此在载入RDB文件的时候需要选择RDB文件的版本号。
- DB-DATA ——保存内存快照部分。
- EOF——结束标志位
- CHECK-SUM——redis文件所有内容的校验和。REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾,当读取时,根据它的值对内容
进行校验。 如果为0,则表示已经关闭了检查功能。
1.2 RDB方式介绍
RDB方式:redis将内存中的数据库快照保存到磁盘中。redis服务器在挂掉重启之后,可以通过加载RDB文件进行数据的恢复。可以继续提供服务。redis提供了rdbSave ,redbLoad,其中rdbSave 是将内存中的快照以RDB格式保存到磁盘中。redbLoad是在redis重启后将磁盘中的RDB文件加载到内存中恢复数据。
- 保存
rdbSave 将内存中的快照以RDB格式保存到磁盘中,如果RDB文件存在,则覆盖为最新的RDB文件。在保存RDB文件期间,主线程需要被阻塞,知道保存完成为止。其中SAVE和BGSAVE 都会调用 rdbSave 函数,但是处理方式有一些区别:
SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求 。
- 加载
redbLoad函数用于在redis重启后将磁盘中的RDB文件加载到内存中恢复数据。在载入期间,服务器每载入 1000 个键就处理一次所有已到达的请求,不过只有 PUBLISH 、SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 五个命令的请求会被正确地处理,
其他命令一律返回错误。等到载入完成之后,服务器才会开始正常处理所有命令。
Note: 发布与订阅功能和其他数据库功能是完全隔离的,前者不写入也不读取数据库,所以在服务器载入期间,订阅与发布功能仍然可以正常使用,而不必担心对载入数据的完整性产生影响。
另外,因为 AOF 文件的保存频率通常要高于 RDB 文件保存的频率,所以一般来说,AOF 文件中的数据会比 RDB 文件中的数据要新。
因此,如果服务器在启动时,打开了 AOF 功能,那么程序优先使用 AOF 文件来还原数据。只有在 AOF 功能未打开的情况下,Redis 才会使用 RDB 文件来还原数据。
2.AOF
2.1 AOF同步redis写操作
为了保证redis数据的可靠性,采用了另一种方式AOF同步,将redis写操作命令及参数保存到AOF文件中。除了SELECT命令是是AOF自己加上去的,其他命令都是之前客户端调用发来的写操作命令,同步命令到AOF文件包括三个流程:
- 命令传播:写操作命令包括参数的个数,命令的参数发送到AOF程序中。
- 缓存追加:AOF程序将接收到的写操作信息,转换为网络通讯协议的格式,然后将其追加到AOF缓存中。
- 文件写入和保存:AOF缓存中的文件被写入到AOF文件末尾,如果设定的AOF保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存
到磁盘中 。
下面来详细介绍下这三个步骤。
2.2 命令传播
当一个 Redis 客户端需要执行命令时 ,他通过网络协议将命令操作传给redis服务器,比 如 说, 要 执 行 命 令 SET KEY VALUE , 客 户 端 将 向 服 务 器 发 送 文 本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。
服务器接收到文本之后将其转化为redis字符串对象。比如 SET KEY VALUE,redis服务器向指向实现 SET 命令的setCommand 函数 ,创建三个字符串SET,KEY,VALUE,分别保存 SET 、 KEY 和 VALUE 三个参数(命
令也算作参数)。
2.3缓存追加
现在命令传播过程已经准备好了AOF程序中可用的redis字符串,现在需要将将命令还原成 Redis 网络通讯协议 ,然后将协议文本追加到aof_buf字符创对象中。
redisServer 结构维持着 Redis 服务器的状态,aof_buf 域则保存着所有等待写入到 AOF 文
件的协议文本:
struct redisServer {
// 其他域...
sds aof_buf;
// 其他域...
};
2.4文件写入和保存
每当服务器常规任务函数被执行、或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行以下两个工作:
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中 。
2.5引发的问题
到此为止,我们就看到了AOF保证redis数据可靠性的方案。但是问题是,随着我们业务量不断增大,我们的AOF文件也会越来越庞大,越来越臃肿,这对于以内存为基础的redis数据库而言
是致命的伤害。那么有什么办法可以解决掉这个问题呢?
答案就是AOF文件重写,当然这个重写不是说重写原来的AOF文件,而是新建一个AOF文件,写入当前数据库状态来实现的。
比如:RPUSH list 1 2 3 4 // [1, 2, 3, 4]
RPOP list // [1, 2, 3]
LPOP list // [2, 3]
LPUSH list 1 // [1, 2, 3]
如果执行了着一些系列redis命令之后,那原来的AOF文件中必然追加了四次这样的协议文本。但是如果我们通过重写当前数据框状态的方式就只需要写入数据库中当前此键的状态RPUSH list 1 2 3 // [1, 2, 3 ] 即可。
根据键的类型,使用适当的写入命令来重现键的当前值,这就是 AOF 重写的实现原理。整个重写过程可以用伪代码表示如下:
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()
根据伪代码可以看出,redis重写不仅有利于释放原来庞大的AOF文件占用的内存,而且还能清除过期键所占用的内存。
现在我们知道了AOF文件可以创建一个新的AOF文件,并保存数据库当前状态,会存在大量的写操作命令,所以代用这个函数的调用者会被阻塞很长一段时间,这是因为redis使用单线程来处理命令请求的。这个时候,redis服务器无法处理客户端发来的任何请求,这肯定不是我们希望的。所以
Redis 决定将 AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是:
- 子进程进行AOF重写的时候,主线程可以处理客户端发来的命令请求。
- 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数
据的安全性。
2.6 子进程引发的另一个问题
redis 处理请求时单线程的。通过开启子线程来进行AOF文件重写,能够解决主线程被阻塞的问题。但是,由于主线程在子线程进行AOF文件重写的时候,继续处理客户端的请求,有可能对已经写入的AOF文件的命令做了修改,这个时候AOF文件写入的命令还是原来的,状态就不一致了。
比如说:我在1s的时候AOF重写命令RPUSH list 1 2 3 // [1, 2, 3 ] ,1s后主线程又接收了一个客户端命令请求:LPUSH list 1 // [1, 2, 3,4]。此时我们可以看到redis数据库与重写的AOF文件状态不一致。
为了解决这个问题,Redis 增加了一个 AOF 重写缓存。这个缓存在fork出子线程之后开启应用,redis在接收到写操作命令式,一方面将写操作命令传入到AOF重写缓存中,另一方面将写操作命令追加到当前重写的AOF文件中。就是说,主进程在开启了子进程进行AOF重写的时候做了
一下三件事:
- 处理命令请求
- 写操作命令追加到重写的AOF文件中
- 写操作命令追加到AOF重写缓存中
这样一来就可以保证,在现有AOF程序继续执行,一旦在AOF执行期间宕机,也不会发生数据丢失。一方面又保证了所有对数据库的写操作命令到在AOF缓存中,有利于redis服务器中数据与AOF数据不一直的问题。
那么接下来要讨论等写操作命令写到了AOF缓存中,接下来如何处理呢?
当子进程完成AOF重写命令,就会向主进程发送一个完成信号,父进程在接到完成信号之后,会调用一个信号处理函数,并完成以下工作:
- 将 AOF 重写缓存中的内容全部写入到新 AOF 文件中
- 新的AOF文件覆盖掉原来的AOF文件。
在整个 AOF后台重写过程中,只有最后的写入缓存和改名操作会造成主进程阻塞,在其他时候,AOF 后台重写都不会对主进程造成阻塞,这将 AOF 重写对性能造成的影响降到了最低。
2.7 什么时候出发AOF后台程序?
那么问题来了,什么时候fork一个子程序进行AOF重写呢?就是说在什么样的情况下,需要AOF重写呢?有两种方案
- AOF 重写可以由用户通过调用 BGREWRITEAOF 手动触发。
- AOF重写可以通过配置设置成为自动出发。服务器在 AOF 功能开启的情况下,会维持 三个变量:录当前 AOF 文件大小的变量 aof_current_size,记录最后一次 AOF 重写之后,AOF 文件大小的变量 aof_rewirte_base_size,增长百分比变量 aof_rewirte_perc 。
每次当 serverCron 函数执行时,它都会检查以下条件是否全部满足,如果是的话,就会触发自动的 AOF 重写 :
1. 没有 BGSAVE 命令在进行。
2. 没有 BGREWRITEAOF 在进行。
3. 当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。
4. 当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。
默认情况下,增长百分比为 100% ,也即是说,如果前面三个条件都已经满足,并且当前 AOF文件大小比最后一次 AOF 重写时的大小要大一倍的话,那么触发自动 AOF 重写
2.8总结
来总结一下,这里主要讲了几个问题:
- redis数据库为了保证其数据可靠性,采用RDB和AOF方式来同步数据库状态。
- RDB方式将redis数据库快照保存到磁盘中,一旦重启redis服务器,只需要到磁盘中加载数据库快照即可。AOF方式将redis执行的写操作命令追加到AOF文件中,一旦重启redis服务器,只需要执行AOF文件中的命令,即可还原服务器中数据库的状态。
- AOF为了避免随着业务壮大导致的AOF文件越来越庞大,占内存空间的问题,采用AOF重写机制,即只同步数据库的当前状态到新的AOF文件中,然后以新的AOF文件覆盖旧的AOF文件。
- redis数据库是单线程的,这一点要务必牢记。所以在主线程处理客户端写操作命令请求,一方面又需要AOF重写,这样会导致主线程阻塞。为了解决这个问题,设计了AOF缓存,将主线程新的写操作命令追加到AOF缓存中,一旦AOF重写完成,发送一个完成命令给
主线程,则主线程继续讲AOF缓存中的写操作命令同步到当前新的AOF文件中,并用新的AOF文件替代掉旧的AOF文件。
- AOF重写既 可以由用户手动启动,也可以由服务器自动开启。