原文链接: https://www.changxuan.top/?p=1386
Redis 是一个非关系型的内存数据库,使用内存存储数据是它能够进行快速存取数据的原因之一。
在实际应用中,常有人提倡把 Redis 只作为一种能够提高用户体验的组件来使用, 也就是说即使 Redis 服务挂掉之后也要保证系统正常使用。不过,在很多系统中还是希望既能发挥 Redis 基于内存快速存取的特性,又希望机器断电或 Redis服务停止后数据不丢失。所以,才引出了 Redis 的持久化功能。
在许多技术文章中,提到 Redis 的持久化时往往都会直接抛出两个名词 RDB 和 AOF。然后接下来就是分别介绍这两个名词。当然,如果要谈 Redis 的持久化肯定避免不了讲 RDB 和 AOF,但这是介绍持久化最恰当的方式吗?这样的文章是不是显得有些生硬呢?所以,在尝试弄明白一个事物的原理时一定要从头到尾的思考它存在的意义?为了解决什么问题?采用了什么方式?达到了什么目的?自己有没有其它的方案?这样从问题的源头切入,才能对这个事物理解的更加深刻,从而能够更好的帮助自己进行举一反三。而不是人云亦云,对于一些知识仅仅是背诵下来,这种死记硬背下来的知识在脑海里的保质期也是短的可怜。
在前面,我们已经提到为什么需要引入持久化?简单的来说持久化就是把内存中的数据存储到外存上,这样服务停止后,当再启动的时候就可以把外存的数据读取到内存中从而达到了不丢失数据的目的。
如果让你设计一个持久化的方案,你会怎么做呢?(假装绞尽脑汁… …)首先,我们可以使用一种简单的策略,将 Redis 中所有的数据按照一定格式全部写到磁盘上,即创建数据的快照文件。然后,你为了尽量保证不丢数据需要考虑使用实时写还是定时写,又或者用其它策略。其实,现在的你已经在尝试着去实现 RDB (Redis Database)持久化的机制了。所以,你看它其实并不难。万丈高楼从地起,先从一个简单的 idea 开始,逐渐去完善它,丰富它的过程便是解决问题的过程。例如用这种思路去学习计算机网络也是同样适用的,你可以给自己出一个问题“如何让两台电脑进行通信?”,自己想办法解决这个问题的过程肯定会比在计算机网络课堂上收获的知识更多,也更牢固。
尽管不需要我们写代码来实现 RDB 持久化,但是并不妨碍我们来思考一下假如让我们来实现的话大概会遇到哪些问题?例如:什么时候生成数据快照?文件数据格式的定义?如果在主进程中进行持久化,阻塞客户端的请求后会不会有影响?接下来,我们就看一下 RDB 是如何做的吧。
在 Redis 中,提供了两个 RDB 持久化的命令: SAVE
和 BGSAVE
。执行 SAVE
时,Redis 服务会停止处理任何客户端的命令请求;执行 BGSAVE
时,Redis 服务则会创建一个子进程,由子进程来负责数据的持久化,而此时 Redis 服务就可以正常处理客户端的请求。
BGSAVE
解决了我们对于持久化时是否会影响 Redis 服务处理客户端的请求的担心。
自动间隔性保存,则解决了“什么时候生成数据快照?”的问题。在 Redis 的配置文件中我们可以写入以下配置:
save 600 1 save 300 10 save 60 100 save 30 1000
上面的配置表示,如果在 600 秒内对数据库进行了 1 次修改,就执行执行一次 BGSAVE 命令;如果在 300 秒内对数据库进行了 10 次修改,就执行一次 BGSAVE 命令;以此类推。你可以根据你的业务场景,配置 save 的参数,也不仅仅局限于 4 条配置。
在 Redis 启动时,会把上述配置存储到 Redis 服务器的状态中,具体的结构体则是 redisServer,存储 save 参数的结构体为 saveparam。
1 // Redis 服务器状态信息结构体 2 struct redisServer { 3 // ... ... 4 5 // 记录多个 save 配置参数 6 struct saveparam *saveparams; 7 // 修改次数计数器 8 long long dirty; 9 // 上次执行保存的时间 10 time_t lastsave; 11 12 // ... ... 13 } 14 // Save 参数结构体 saveparam 15 struct saveparam { 16 // 秒数 17 time_t seconds; 18 // 修改数 19 int changes; 20 }
看到上面 redisServer 结构体的属性信息,你心里应该有答案了吧?dirty 表示的是自从上次执行 SAVE 或者 BGSAVE 命令完成之后对数据库进行修改的次数;lastsave 表示的是上次成功执行SAVE 或者 BGSAVE 命令的时间。这个时候,如果再有个机制能够定时检查是否有满足条件的配置参数就可以了。
Redis 提供了一个周期性操作函数 serverCron,每 100 ms 会执行一次。它其中的一项工作就是来检查是否有符合条件的 save 参数,如果存在符合条件的参数则执行 BGSAVE 命令,执行完毕之后将 dirty 和 lastsave 的值重置。相信只要有基础的编程知识,根据这些变量就能实现这个检查的过程吧。
在上图中,大写字母的单词表示的常量,小写字母单词则是变量和数据。RDB 文件开头的“REDIS”是我们习惯称为的魔数,类似于 class 文件的 COFFEE,用来识别文件类型;紧接着长度为四个字节的 db_version 记录的是 RDB 文件的版本号;database 表示的是所存储的数据;EOF 则表明数据内容结束了;check_sum 的值是整个文件的校验和,用来检查文件是否损坏。
其实持久化数据除了 RDB 这种方式,肯定会有同学能想到另一种方式,就是把服务端执行的所有客户端请求增加、修改和删除等会改变数据的命令全都存储起来。通过存储这些命令数据,在遇到机器宕机和服务进程异常中断的情况下重启服务时只要执行一遍这些持久化的命令即可恢复之前的数据了。(也是一个相当好的办法呀!)
原理就是如此,那么问题来了,假如同样让你来实现这个过程,你会考虑到哪些问题呢?
一是性能问题,执行完命令之后是否直接将此命令持久化到磁盘上还是由操作系统控制文件同步?在这个问题上如何做取舍?二是文件大小问题,随着 Redis 服务运行越来越久,数据文件势必会越来越大?应该使用什么办法解决?… …
我们来看下 Redis 的 AOF 的过程吧!
首先,通过在配置文件中增加一行配置 appendonly yes
来开启 AOF 持久化。
像 RDB 机制所依赖 redisServer
结构体中的 saveparams、dirty、lastsave
参数一样,AOF 的实现依赖 redisServer
结构体中的 aof_buf
参数。
1 struct redisServer{ 2 // ... ... 3 4 // AOF 缓冲区 5 sds aof_buf; 6 7 // ... ... 8 }
aof_buf 参数用来以协议格式缓存会对数据进行变更的命令。
在 Redis 服务器执行完命令,并将命令以协议的格式追加到 aof_buf
缓冲区之后,在当前这个事件循环结束之前,Redis 还会调用一个函数 flushAppendOnlyFile
,这个函数会根据配置文件中 appendfsync
的值来决定接下来的持久化行为。appendfsync
有三个可选值,分别是 always、everysec、no
。
以上就是 AOF 持久化的基本过程。
由于命令数据是以协议格式存储至文件中的,所以在启动 Redis 服务时检测到 AOF 文件的存在后会启动载入程序。(如果 RDB 和 AOF 持久化的文件同时存在则会优先载入 AOF 文件数据)
启动载入程序后,其载入过程如下图所示:
在前面,我们提到 AOF 的这种机制会造成 AOF 数据文件越来越大,并且可能会存在许多无意义的命令。例如,先执行了一个命令 set chang xuan
,随后又执行了命令 del chang
。其实这两条语句都会被持久化到 AOF 文件中,但实际上除了能证明曾经执行过这两条命令之外对于我们要持久化数据的目的而言并没有什么作用。
对此,Redis 提供了 AOF 重写的机制。
Redis 的 AOF 重写其实是根据当前存储的数据,生成命令的过程。并且会采用一些策略尽量减小 AOF 文件的大小,例如对于 List 中的数据会尽量使用较少的命令操作较多的数据。当然,如果在当前进程中进行重写处理并且数据量特别大的情况下肯定会阻塞客户端的请求,所以和 RDB 一样,Redis 提供了 AOF 后台重写的机制。
AOF 通过 fork 子进程的方式进行后台重写有两个优点:
天下没有免费的午餐,这种方式还带来一个问题。就是在使用子进程重写期间,如果父进程还在处理着客户端请求,如何保证重写后 AOF 文件数据的一致性呢?
对于这个问题,Redis 设置了一个 AOF 重写缓冲区。在子进程被创建后,Redis 服务器就会启用这个重写缓冲区。在将命令以协议格式追加到 AOF 缓冲区之后,同时也会追加到 AOF 重写缓冲区。
当子进程完成重写工作后会向父进程发送一个信号。父进程接收到信后之后会进行调用相关函数,进行以下工作:
这时,就完成了一次 AOF 后台重写。
通过前文内容,我们可以大致清楚 Redis 所提供的 RDB 和 AOF 两种持久化机制的过程以及基本原理。它们各有特点,也各有适合使用的场景所以并不能说谁一定比谁好。通过搭配使用,能够确保线上环境数据的安全性就是最好的。