前面的几章介绍了各式各样的Redis命令以及使用这些命令来操作数据结构的方法,还列举了几个使用Redis来解决实际问题的例子。 为了让读者做好使用Redis构建真实软件的准备, 本章将展示维护数据安全以及应对系统故障的方法。另外,本章还会介绍一些能够在保证数据完整性的前提下提升Redis性能的方法。
本章首先会介绍Redis的各个持久化选项, 这些选项可以让用户将自己的数据存储到硬盘上面。接着本章将介绍如何通过Redis的复制特性,把不断更新的数据副本存储到附加的机器上面, 从而提升系统的性能和数据的可靠性。 之后本章将会说明同时使用复制和持久化的好处和坏处, 并通过一些例子来告诉读者应该如何去选择适合自己的持久化选项和复制选项。 最后本章将对Redis的事务特性和流水线特性进行介绍, 并讨论如何诊断某些性能问题。
阅读这一章的重点是要弄懂更多的Redis运作原理,从而学会如何在首先保证数据正确的前提下,加快数据操作的执行速度。
现在, 让我们来看看Redis是如何将数据存储到硬盘里面, 使得数据在Re山s重启之后仍然存在的。
4.1 持久化选项
Redis提供了两种不同的持久化方法来将数据存储到硬盘里面。 一种方法叫快照(snapshotting)它可以将存在于某一时刻的所有数据都写入硬盘里面。另一种方法叫只追加文件(append-onlyfile, AOF), 它会在执行写命令时,将被执行的写命令复制到硬盘里面。这两种持久化方法既可以同时使用,又可以单独使用,在某些情况下甚至可以两种方法都不使用,具体选择哪种持久化方法需要根据用户的数据以及应用来决定。
将内存中的数据存储到硬盘的一个主要原因是为了在之后重用数据,或者是为了防止系统故障而将数据备份到一个远程位置。另外 , 存储在Redis里面的数据有可能是经过长时间计算得出的,或者有程序正在使用Redis存储的数据进行计算 , 所以用户会希望自己可以将这些数据存储起来以便之后使用,这样就不必再重新计算了。对于一些Redis应用来说 , ”计算 ” 可能 只是简单地将另一个数据库的数据复制到Redis里面(2.4节中就介绍过这样的例子),但对 于另外一些Redis应用来说,Redis存储的数据可能是根据数十亿行日志进行聚合分析得出的结果。
两组不同的配置选项控制着Redis将数据写入硬盘里面的方式,代码清单4-1展示了这些配置选项以及它们的示例配置值。因为之后的 4.1. l 节和 4.1.2 节会更详细地介绍这些选项,所以目前我们只要稍微了解一下这些选项就可以了。
代码清单 4-1最开头的几个选项和快照持久化有关,比如:如何命名硬盘上的快照文件、多久执行一次自动快照操作、是否对快照文件进行压缩,以及在创建快照失败后是否仍然继续执行写命令。代码清单的第二组选项用于配置AOF子系统(subsystem): 这些选项告诉Redis 是否使用AOF持久化、多久才将写入的内容同步到硬盘、在对AOF进行压缩(compaction)的时候能否执行同步操作,以及多久执行一次AOF压缩。接下来的一节将介绍如何使用快照来保持数据安全。
4.1.1 快照持久化
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。在创建快照之后,用户可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本, 还可以将快照留在原地以便重启服务器时使用。
根据配置, 快照将被写入dbfilename选项指定的文件里面, 并储存在dir选项指定的路径上面。 如果在新的快照文件创建完毕之前, Redis、 系统或者硬件这三者之中的任意一个崩溃了, 那么Reclis将丢失最近一次创建快照之后写入的所有数据。
举个例子,假设Redis目前在内存里面存储了10GB的数据,上一个快照是在下午2:35开始创建的, 并且已经创建成功。 下午3:06时, Redis又开始创建新的快照, 并且在下午3:08快照文件创建完毕之前,有35个键进行了更新。 如果在下午3:06至下午3:08期间, 系统发生崩溃, 导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面, 如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将只丢失35个键的更新数据。
创建快照的办法有以下几种:
• 客户端可以通过向Redis发送BGSAVE命令来创建一个快照。 对千支持BGSAVE命令的平台来说(基本上所有平台都支持, 除了Windows平台),Redis会调用fork1D来创建一 个子进程, 然后子进程负责将快照写入硬盘, 而父进程则继续处理命令请求。
• 客户端还可以通过向Redis发送SAVE命令来创建一个快照, 接到SAVE命令的Redis服务器在快照创建完毕之前将不再响应任何其他命令。 SAVE命令并不常用,我们通常只会在没有足够内存去执行BGSAVE命令的清况下, 又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。
• 如果用户设置了save配置选项, 比如save 60 10000, 那么从Redis最近一次创建快照之后开始算起,当"60秒之内有10 000次写入“ 这个条件被满足时,Redis就会自动触发BGSAVE命令。 如果用户设置了多个save配置选项, 那么当任意一个save配置选项所设置的条件被满足时,Re山s就会触发一次BGSAVE命令。
• 当Redis通过SHUTDOWN命令接收到关闭服务器的请求时, 或者接收到标准TERM信号时, 会执行一个SAVE命令, 阻塞所有客户端,不再执行客户端发送的任何命令, 并在SAVE命令执行完毕之后关闭服务器。
• 当一个Redis服务器连接另一个Redis服务器, 并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有在执行BGSAVE操作, 或者主服务器并非刚刚执行完BGSAVE操作, 那么主服务器就会执行BGSAVE命令。更多有关复制的信息请参考4.2节。
在只使用快照持久化来保存数据时, 一定要记住:如果系统真的发生崩溃, 用户将丢失最近一次生成快照之后更改的所有数据。 因此,快照持久化只适用于那些即使丢失一部分数据也不会造成问题的应用程序, 而不能接受这种数据损失的应用程序则可以考虑使用4.1.2节中介绍的AOF持久化。
4.1.2 AOF持久化
简单来说, AOF持久化会将被执行的写命令写到AOF文件的末尾, 以此来记录数据 发生的变化。 因此, Redis只要从头到尾重新执行一次AOF文件包含的所有写命令, 就可以恢复AOF文件所记录的数 据集。 AOF持久 化可以通 过设置代码清单4-1 所示的appendonly yes配置选项来打开。 表4-1展示了appendfsync配置选项对AOF文件的同步频率的影响。
如果用户使用appendfsyncalways选项的话, 那么每个Redis 写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少。不过遗憾的是, 因为这种同步策略需要对硬盘进行大量写入, 所以Redis处理命令的速度会受到硬盘性能的限制:转盘式硬盘(spinning disk)在这种同步频率下每秒只能处理大约200 个写命令, 而固态硬盘(solid-state drive, SSD) 每秒大概也只能处理几万个写命令。
为了兼顾数据安全和写入性能, 用户可以考虑使用appendfsync everysec选项,让Redis以每秒一次的频率对AOF 文件进行同步。Re山s每秒同步一次AOF 文件时的性能和不使用任何持久化特性时的性能相差无几, 而通过每秒同步一次AOF 文件,Re山s可以保证, 即使出现系统崩溃, 用户也最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候, Redis还会优雅地放慢自己的速度以便适应硬盘的最大写入速度。
最后, 如果用户使用appendfsync no选项,那么Redis 将不对AOF 文件执行任何显式的同步操作, 而是由操作系统来决定应该在何时对AOF 文件进行同步。这个选项在一般情况下不会对Redis的性能带来影响, 但系统崩溃将导致使用这种选项的Redis服务器丢失不定数量的数据。另外, 如果用户的硬盘处理写入操作的速度不够快的话,那么当缓冲区被等待写入硬盘的数据填满时, Redis的写入操作将被阻塞, 并导致Redis处理命令请求的速度变慢。因为这个因,一般来说并不推荐使用appendfsyncno选项, 在这里介绍它只是为了完整列举appendfsync选项可用的3个值。
虽然AOF 持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求, 但AOF 持久化也有缺陷随就是AOF 文件的体积大小。
4.1.3 重写/压缩AOF文件
在阅读了上一节对AOF 持久化的介绍之后, 读者可能会感到疑惑: AOF 持久化既可以将丢失数据的时间窗口降低至1秒(甚至不丢失任何数据), 又可以在极短的时间内完成定期的待久化操作, 那么我们有什么理由不使用AOF持久化呢?但是这个问题实际上并没有那么简单, 因为Redis会不断地将被执行的写命令记录到AOF文件里面, 所以随着Redis不断运行,AOF文件的体积也会不断增长, 在极端情况下, 体积不断增大的AOF文件甚至可能会用完硬盘的所有 可用空间 。还有另一个问题就是, 因为Redis在重启之后需要 通过重新执行AOF文件记录的所有写命令来还原数据集, 所以如果AOF文件的体积非常大, 那么还原操作执行的时间就可能会非常长。
为了解决AOF文件体积不断增大的问题, 用户可以向Redis发送BGREWRITEAOF命令, 这个命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件, 使AOF文件的体积变得尽可能地小。 BGREWRITEAOF的工作原理和 BGSAVE创建快照的工作原理非常相似: Redis会创 建一个子进程, 然后由子进程负责对AOF文件进行重写。 因为AOF文件重写也需要 用到子进程, 所以快照待久化因为创建子进程而导致的性能问题和内存占用问题, 在AOF持久化中也同样存 在。 更糟糕的是, 如果不加以控制的话,AOF 文件的体积可能会比快照文件的体积大好几倍,在进行AOF重写并删除旧AOF文件的时候, 删除一个体积达到数十GB大的旧AOF文件可能会导致操作系统挂起(hang)数秒。
跟快照持久化可以通过设置 save选项来自动执行BGSAVE一样,AOF持久化也可以通过设置 auto-aof-rewrite-percentage选项和auto-aof-rewrite-rnin-size选项来自动执行BGREWRITEAOF。举个例子, 假设用户对Redis设置了配置选项auto-aof-rewrite-percentage 100和 auto-aof-rewrite-rnin-size 64rnb, 并且启用了AOF持久化, 那么当AOF文件的体积大于64MB, 并且AOF文件的体积比上 次重写之后的体积大了至少 倍(100%)的时候,Redis将执行BGREWRITEAOF命令。 如果AOF重写执行得过于频繁的话, 用户可以考虑将 auto-aof-rewrite-percentage选项的值设置为100以上, 这种做法可以让Redis在AOF 文件的体积变得更大之后才执行重写操作, 不过也会让Redis在启动时还原数据集所需的时间变得更长。
无论是使用AOF待久化还是快照持久化, 将数据持久化到硬盘上都是非常有必要的, 但除 了进行持久化之外, 用户还必须对待久化所得的文件进行备份(最好是备份到多个不同的地方),这样才能尽量避免数据丢失事故发生。 如果条件允许的话, 最好能将快照文件和最新重写的AOF文件备份到不同的服务器上面。
通过使用AOF持久化或者快照持久化, 用户可以在系统重启或者崩溃的情况下仍然保留数据。 随着负载量的上升, 或者数据的完整性变得越来越重要时, 用户可能需要使用复制特性。
4.2 复制
对于有扩展平台以适应更高负载经验的工程师和管理员来说, 复制(replication)是不可或缺的 。复制可以让其他服务器拥有 个不断地更新的数据副本,从而使得拥有数据副本的服务器可以用于处理客户端发送的读请求。关系数据库通常会使用一个主服务器(master)向多个从服务器(slave)发送更新, 并使用从服务器来处理所有读请求。 Redis也采用了同样的方法来实现自己的复制特性,并将其用作扩展性能的一种手段。 本节将对Redis的复制配置选项进行讨论,并说明Redis在进行复制时的各个步骤。
尽管Redis的性能非常优秀,但它也会遇上没办法快速地处理请求的情况, 特别是在对集合和有序集合进行操作的时候, 涉及的元素可能会有上万个甚至上百万个, 在这种情况下,执行操 作所花费的时间可能需要以秒来进行计算, 而不是毫秒或者微秒。但即使一个命令只需要花费10毫秒就能完成,单个Redis实例(ins画ce) 1秒也只能处理100个命令。
在需要扩展读请求的时候,或者在需要写入临时数据的时候(第7章对此有详细的介绍),用户可以通过设置额外的Redis从服务器来保存数据集的副本。在接收到主服务器发送的数据初始副本(initialcopy of the data)之后,客户端每次向主服务器进行写入时,从服务器都会实时地得到更新。在部署好主从服务器之后, 客户端就可以向任意一个从服务器发送读请求了, 而不必再像之前一样, 总是把每个读请求都发送给主服务器(客户端通常会随机地选择使用哪个从服 务器,从而将负载平均分配到各个从服务器上)。
4.2.1 对Redis 的复制相关选项进行配置
4.1.1节中曾经介绍过, 当从服务器连接主服务器的时候, 主服务器会执行BGSAVE操作。因此为了正确地使用复制特性, 用户需要保证主服务器已经正确地设置了代码清单4-1里面列出的dir选项和dbfilename选项, 并且这两个选项所指示的路径和文件对于Redis进程来说都是可写的(writable)。
尽管有多个不同的选项可以控制从服务器自身的行为,但开启从服务器所必须的选项只有slaveof一个。如果用户在启动Redis服务器的时候,指定了一个包含slaveof host port选项的配置文件,那么Redis服务器将根据该选项给定的IP地址和端口号来连接主服务器。对于一个正在运行的Redis服务器, 用户可以通过发送SLAVEOF no one命令来让服务器终止复制操作,不再接受主服务器的数据更新;也可以通过发送SLAVEOF host port命令来让服务器开始复制一个新的主服务器。
开启Redis的主从复制特性并不需要进行太多的配置, 但了解Redis服务器是如何变成主服务器或者从服务器的, 对于我们来说将是非常有用的和有趣的过程。
4.2.2 Redis 复制的启动过程
本章前面曾经说过, 从服务器在连接一个主服务器的时候,主服务器会创建一个快照文件并将其发送至从服务器,但这只是主从复制执行过程的其中一步。 表4-2完整地列出了当从服务器连接主服务器时, 主从服务器执行的所有操作。
通过使用表4-2所示的办法,Redis在复制进行期间也会尽可能地处理接收到的命令请求,但是, 如果主从服务器之间的网络带宽不足, 或者主服务器没有足够的内存来创建子进程和创建记录写命令的缓冲区, 那么Redis处理命令请求的效率就会受到影响。因此, 尽管这并不是必须的,但在实际中最好还是让主服务器只使用50%65%的内存, 留下30%45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。
设置从服务器的步骤非常简单, 用户既可以通过配置选项SLAVEOF host port来将一个Redis服务器设置为从服务器, 又可以通过向运行中的Redis服务器发送SLAVEOF命令来将其设置为从服务器。如果用户使用的是SLAVEOF配置选项, 那么Redis在启动时首先会载入当前可用的任何快照文件或者AOF文件, 然后连接主服务器并执行表4-2所示的复制过程。如果用户使用的是SLAVEOF命令, 那么Redis会立即尝试连接主服务器, 并在连接成功之后, 开始表4-2所示的复制过程。
当多个从服务器尝试连接同一个主服务器的时候, 就会出现表 4-3 所示的两种情况中的其中 一种。
在大部分情况下, Redis 都会尽可能地减少复制所需的工作, 然而,如果从服务器连接主服务器的时间并不凑巧, 那么主服务器就需要多做一些额外的工作。另一方面, 当多个从服务器同时连接主服务器的时候,同步多个从服务器所占用的带宽可能会使得其他命令请求难以传递给主服务器, 与主服务器位于同一网络中的其他硬件的网速可能也会因此而降低。
4.2.3 主从链
有些用户发现 , 创建多个从服务器可能会造成网络不可用一当复制需要通过互联网进行或 者需要在不同数据中心之间进行时, 尤为如此。 因为 Redis 的主服务器和从服务器并没有特别不 同的地方, 所以从服务器也可以拥有自己的从服务器,并由此形成主从链 (master/slave chaining)。
从服务器对从服务器进行复制在操作上和从服务器对主服务器进行复制的唯一区别在于 , 如果从服务器X拥有从服务器 Y, 那么当从服务器X在执行表 4-2 中的步骤 4 时 , 它将断开与从 服务器 Y的连接 , 导致从服务器 Y需要重新连接并重新同步 (resync)。
当读请求的重要性明显高于写请求的重要性,并且读请求的数拭远远超出一台 Redis 服务器 可以处理的范围时,用户就需要添加新的从服务裸来处理读请求。 随着负载不断上升,主服务器 可能会无法快速地更新所有从服务器,或者因为重新连接和重新同步从服务器而导致系统超载。 为了缓解这个问题, 用户可以创建一个由 Redis 主从节点 (master/slave node) 组成的中间层来分担主服务器的复制工作,如图4-1所示。
尽管主从服务器之间并不一定要像图4-1那样组成一个树状结构,但记住并理解这种树状结构对于Redis复制来说是可行的(possible)并且是合理的(reasonable)将有助于读者理解之后的内容。本书在前面的4.1.2节中曾经介绍过, AOF持久化的同步选项可以控制数据丢失的时间长度:通过将每个写命令同步到硬盘里面, 用户几乎可以不损失任何数据(除非系统崩溃或者硬盘驱动器损坏), 但这种做法会对服务器的性能造成影响;另一方面, 如果用户将同步的频率设置为每秒一次, 那么服务器的性能将回到正常水平, 但故障可能会造成1秒的数据丢失。通过同时使用复制和AOF持久化, 我们可以将数据持久化到多台机器上面。
为了将数据保存到多台机器上面, 用户首先需要为主服务器设置多个从服务器, 然后对每个从服务器设置appendonly yes选项和appendfsync everysec选项(如果有需要的话,也可以对主服务器进行相同的设置), 这样的话, 用户就可以让多台服务器以每秒一次的频率将数据同步到硬盘上了。但这还只是第一步: 因为用户还必须等待主服务器发送的写命令到达从服务器, 并且在执行后续操作之前, 检查数据是否已经被同步到了硬盘里面。
4.4 Redis事务
为了保证数据的正确性, 我们必须认识到这一点:在多个客户端同时处理相同的数据时, 不谨慎的操作很容易会导致数据出错。本节将介绍使用Redis事务来防止数据出错的方法, 以及在 某些情况下, 使用事务来提升性能的方法。
Redis的事务和传统关系数据库的事务并不相同。在关系数据库中, 用户首先向数据库服务器发送BEGIN, 然后执行各个相互一致(consistent)的写操作和 读操作, 最后, 用户可以选择发送COMMIT 来确认之前所做的修改, 或者发送ROLLBACK来放弃那些修改。
在Redis里面也有简单的方法可以处理一连串相互一致的读操作和写操作。正如本书在3.7.2节中介绍的那样, Redis的事务以特殊命令MULTI为开始, 之后跟着用户传入的多个命令, 最后以EXEC为结束。但是由于这种简单的事务在EXEC命令被调用之前不会执行任何实际操作, 所以用户将没办法根据 读取到的数据来做决定。这个问题看上去似乎无足轻重, 但实际上无法以 一致的形式读取数据将导致某一类型的问题变得难以解决, 除此之外, 因为 在多个事务同时处理同一个对象时通常需要用到二阶提交(two-phase commit), 所以如果事务不能以 一致的形式读取数据, 那么二阶提交将无法实现, 从而导致一些原本可以成功执行的事务沦落至执行失败的地步。
为什么Redis没有实现典型的加锁功能? 在访问以写入为目的数据的时候(SQL中的 SELECT FOR UPDATE), 关系数据库会对被访问的数据行进行加锁, 直到事务被提交(COMMIT) 或者被回滚(ROLLBACK)为止 . 如果有其他客户端试图对被加锁的数据行进行写入, 那么该客户端将被阻塞, 直到笫一个事务执行完毕为止. 加锁在实际使用中非常有效, 基本上所有关系数据库都实现了这种加锁功能, 它的缺点在于, 持有锁的客户端运行越慢, 等待解锁的客户端被阻塞的时间就越长.
因为加锁有可能会造成长时间的等待, 所以Redis为了尽可能地减少客户端的等待时问, 并不会在执行WATCH命令时对数据进行加锁. 相反地,Redis只会在数据已经被其他客户端抢先修改了的情况下 , 通知执行了 WATCH命令的客户端, 这种做法被称为乐观锁(optimitic locking), 而关系数据库实际执行的加锁操作则被称为悲观锁(pessimistic locking)。 乐观锁在实 际使用中同样非常有效, 因为客户端永远不必花时间去等待第一个取得锁的客户端——它们只 需要在自己的事务执行失败时进行重试就可以了.
当有多个客户端同时对相同的数据进行操作时,正确地使用事务可以有效地防止数据错误发生。 而接下来的一节将向我们展示,在无需担心数据被其他客户端修改了的情况下,如何以 更快的速度执行操作。
4.5 非事务型流水线
第3章在首次介绍 MULTI和EXEC的时候讨论过它们的 ”事务“ 性质——被MULTI和 EXEC包裹的命令在执行时不会被其他客户端打扰。 而使用事务的其中一个好处就是底层的客户 端会通过使用流水线来提高事务执行时的性能。本节将介绍如何在不使用事务的情况下, 通过 使用流水线来进一步提升命令的执行性能。
第2章曾经介绍过一些可以接受多个参数的添加命令和更新命令, 如MGET、MSET、HMGET、 HMSET、 RPUSH和LPUSH、 SADD、 ZADD等。 这些命令简化了那些需要重复执行相同命令的操作,并且极大地提升了性能。尽管效果可能没有以上提到的命令那么显著 , 但使用非事务型流水线(non-transactional pipeline)同样可以获得相似的性能提升,并且可以让用户同时执行多个不同的命令。
4.6 关于性能方面的注意事项
要对Redis的性能进行优化, 用户首先需要弄清楚各种类型的Redis命令到底能跑多快, 而 这一点可以通过调用Redis附带的性能测试程序redis-benchmark来得知,.