redis实战笔记(4)-第4章 数据安全与性能保障
本章主要内容
4.1 将数据持久化至硬盘
4.2 将数据复制至其他机器
4.3 处理系统故障
4.4 Redis事务
4.5 非事务型流水线( non-transactional pipeline)
4.6 诊断性能问题
1.本章首先会介绍Redis的各个持久化选项, 这些选项可以让用户将自己的数据存储到硬盘上面。
2.接着本章将介绍如何通过 Redis 的复制特性, 把不断更新的数据副本存储到附加的机器上面, 从而提升系统的性能和数据的可靠性。
3. 之后本章将会说明同时使用复制和持久化的好处和坏处, 并通过一些例子来告诉读者应该如何去选择适合自 己的持久化选项和复制选项。
4.最后本章将对Redis的事务特性和流水线特性进行介绍, 并讨论如何诊断某些性能问题。
4.1 持久化选项
Redis提供了两种不同的持久化方法来将数据存储到硬盘里面。
一种方法叫快照( snapshotting) , 它可以将存在于某一时刻的所有数据都写入硬盘里面。
另一种方法叫只追加文件( append-only file, AOF) , 它会在执行写命令时, 将被执行的写命令复制到硬盘里面。
这两种持久化方法既可以同时使用, 又可以单独使用, 在某些情况下甚至可以两种方法都不使用
快照
如何命名硬盘上的快照文件、 多久执行一次自 动快照操作、 是否对快照文件进行压缩, 以及在创建快照失败后是否仍然继续执行写令
配置AOF子系统( subsystem)
这些选项告诉Redis是否使用AOF持久化、 多久才将写入的内容同步到硬盘、 在对AOF进行压缩( compaction) 的时候能否执行同步操作, 以及多久执行一次AOF压缩
4.1.1 快照持久化
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。 在创建快照之后, 用户可以对快照进行备份, 可以将快照复制到其他服务器从而创建具有相同数据的服务器副本, 还可以将快照留在原地以便重启服务器时使用。
根据配置, 快照将被写入dbfilename选项指定的文件里面, 并储存在dir选项指定的路径上面。 如果在新的快照文件创建完毕之前,
Redis、 系统或者硬件这三者之中的任意一个崩溃了, 那么 Redis将丢失最近一次创建快照之后写入的所有数据。
举个例子, 假设Redis目 前在内存里面存储了 10GB的数据, 上一个快照是在下午2:35开始创建的, 并且已经创建成功。 下午3:06时, Redis又开始创建新的快照, 并且在下午3:08快照文件创建完毕之前, 有35个键进行了更新。 如果在下午3:06至下午3:08期间, 系统发生崩溃, 导致Redis无法完成新快照的创建工作, 那么 Redis将丢失下午2:35之后写入的所有数据。 另一方面, 如果系统恰好在新的快照文件创建完毕之后崩溃, 那么 Redis将只丢失35个键的更新数据。
创建快照的办法有以下几种。
1.客户端可以通过向Redis发送BGSAVE命令来创建一个快照。 对于支持BGSAVE命令的平台来说(基本上所有平台都支持, 除了 Windows平台) , Redis会调用fork①来创建一个子进程, 然后子进程负责将快照写入硬盘, 而父进程则继续处理命令请求。
2.客户端还可以通过向Redis发送SAVE命令来创建一个快照, 接到SAVE命令的Redis服务器在快照创建完毕之前将不再响应任何其
他命令。 SAVE命令并不常用, 我们通常只会在没有足够内存去执行BGSAVE命令的情况下, 又或者即使等待持久化操作执行完毕也
无所谓的情况下, 才会使用这个命令。
3.如果用户设置了save配置选项, 比如save 60 10000, 那么从Redis最近一次创建快照之后开始算起, 当“60秒之内有10 000次写入”这个条件被满足时, Redis就会自 动触发BGSAVE命令。 如果用户设置了多个save配置选项, 那么当任意一个save配置选项所设置的条件被满足时, Redis就会触发一次BGSAVE命令。
4.当Redis通过SHUTDOWN命令接收到关闭服务器的请求时, 或者接收到标准TERM信号时, 会执行一个SAVE命令, 阻塞所有客户端, 不再执行客户端发送的任何命令, 并在SAVE命令执行完毕之后关闭服务器。
5.当一个Redis服务器连接另一个Redis服务器, 并向对方发送SYNC命令来开始一次复制操作的时候, 如果主服务器目 前没有在执行BGSAVE操作, 或者主服务器并非刚刚执行完BGSAVE操作, 那么主服务器就会执行BGSAVE命令。 更多有关复制的信息请参考4.2节。
1. 个人开发
在个人开发服务器上面, 我主要考虑的是尽可能地降低快照持久化带来的资源消耗。 基于这个原因以及对自 己硬件的信任, 我只设置了save 900 1这一条规则。 其中save选项告知Redis, 它应该根据这个选项提供的两个值来执行BGSAVE操作。 在这个规则设置下, 如果服务器距离上次成功生成快照已经超过了 900秒(也就是15分钟) , 并且在此期间执行了至少一次写入操作, 那么 Redis就会自 动开始一次新的BGSAVE操作。
如果你打算在生产服务器中使用快照持久化并存储大量数据, 那么你的开发服务器最好能够运行在与生产服务器相同或者相似的硬件上
面, 并在这两个服务器上使用相同的save选项、 存储相似的数据集并处理相近的负载量。 把开发环境设置得尽量贴近生产环境, 有助于判断快照是否生成得过于频繁或者过于稀少(过于频繁会浪费资源, 而过于稀少则带有丢失大量数据的隐患) 。
2. 对日 志进行聚合计算
在对日 志文件进行聚合计算或者对页面浏览量进行分析的时候, 我们唯一需要考虑的就是: 如果Redis因为崩溃而未能成功创建新的快照, 那么我们能够承受丢失多长时间以内产生的新数据。 如果丢失一个小时之内产生的数据是可以被接受的, 那么可以使用配置值save 36001( 3600为一小时的秒数) 。
在决定好了持久化配置值之后, 另一个需要解决的问题就是如何恢复因为故障而被中断的日 志处理操作。
在进行数据恢复时, 首先要做的就是弄清楚我们丢失了哪些数据。为了弄明白这一点, 我们需要在处理日 志的同时记录被处理日 志的相关信息。 代码清单4-2展示了一个用于处理新日 志的函数, 该函数有3个参数, 它们分别是: 一个Redis连接; 一个存储日 志文件的路径; 待处理日 志文件中各个行( line) 的回调函数( callback) 。 这个函数可以在处理日 志文件的同时, 记录被处理日 志文件的名字以及偏移量。代码清单4-2 process_logs()函数会将被处理日 志的信息存储到Redis里面
通过将日 志的处理进度记录到Redis里面, 程序可以在系统崩溃之后, 根据进度记录继续执行之前未完成的处理工作。 而通过使用第3章介绍的事务流水线, 程序将保证日 志的处理结果和处理进度总是会同时被记录到快照文件里面。
3. 大数据
当Redis存储的数据量只有几个GB的时候, 使用快照来保存数据是没有问题的。 Redis会创建子进程并将数据保存到硬盘里面, 生成快照所需的时间比你读这句话所需的时间还要短。
但随着Redis占用的内存越来越多, BGSAVE在创建子进程时耗费的时间也会越来越多。 如果Redis的内存占用量达到数十个GB, 且剩余的空闲内存并不多,那么执行BGSAVE可能会导致系统长时间地停顿, 也可能引 发系统大量地使用虚拟内存( virtualmemory) , 从而导致Redis的性能降低至无法使用的程度。
为了防止Redis因为创建子进程而出现停顿, 我们可以考虑关闭自 动保存, 转而通过手动发送BGSAVE或者SAVE来进行持久化。 手动发送BGSAVE一样会引 起停顿, 唯一不同的是用户可以通过手动发送BGSAVE命令来控制停顿出现的时间。 另一方面, 虽然SAVE会一直阻塞Redis直到快照生成完毕, 但是因为它不需要创建子进程, 所以就不会像BGSAVE一样因为创建子进程而导致Redis停顿; 并且因为没有子进程在争抢资源, 所以SAVE创建快照的速度会比BGSAVE创建快照的速度要来得更快一些。
根据我的个人经验, 在一台拥有68 GB内存的Xen虚拟机上面, 对一个占用50 GB内存的Redis服务器执行BGSAVE命令的话, 光是创建子进程就需要花费15秒以上, 而生成快照则需要花费15~20分钟;但使用SAVE只需要3~5分钟就可以完成快照的生成工作。 因为我的应用程序只需要每天生成一次快照, 所以我写了一个脚本, 让它在每天凌晨3点停止所有客户端对Redis的访问, 调用SAVE命令并等待该命令执行完毕, 之后备份刚刚生成的快照文件, 并通知客户端继续执行操作。
如果用户能够妥善地处理快照持久化可能会带来的大量数据丢失,那么快照持久化对用户来说将是一个不错的选择, 但对于很多应用程序来说, 丢失15分钟、 1小时甚至更长时间的数据都是不可接受的, 在这种情况下, 我们可以使用AOF持久化来将存储在内存里面的数据尽快地保存到硬盘里面。
4.1.2 AOF持久化
AOF持久化会将被执行的写命令写到AOF文件的末尾,以此来记录数据发生的变化。 因此, Redis只要从头到尾重新执行一次AOF文件包含的所有写命令, 就可以恢复AOF文件所记录的数据集。
AOF持久化可以通过设置代码清单4-1所示的appendonly yes配置选项来打开。appendfsync配置选项对AOF文件的同步频率的
影响。
文件同步 在向硬盘写入文件时, 至少会发生3件事。 当调用file. write()方法(或者其他编程语言里面的类似操作) 对文件进 行写入时, 写入的内容首先会被存储到缓冲区, 然后操作系统会在将来的某个时候将缓冲区存储的内容写入硬盘, 而数据只有在被写入硬盘之 后, 才算是真正地保存到了硬盘里面。 用户可以通过调用file. flush()方法来请求操作系统尽快地将缓冲区存储的数据写入硬盘里, 但具体何时执行写入操作仍然由操作系统决定。 除此之外, 用 户还可以命令操作系统将文件同步( sync) 到硬盘, 同步操作会一直阻塞直到指定的文件被写入硬盘为止。 当同步操作执行完毕之后, 即使系 统出现故障也不会对被同步的文件造成任何影响。 |
1.
如果用户使用appendfsync always选项的话, 那么每个Redis写命令都会被写入硬盘, 从而将发生系统崩溃时出现的数据丢失减到最少。不过遗憾的是, 因为这种同步策略需要对硬盘进行大量写入, 所以Redis处理命令的速度会受到硬盘性能的限制: 转盘式硬盘( spinningdisk) 在这种同步频率下每秒只能处理大约200个写命令, 而固态硬盘
( solid-state drive, SSD) 每秒大概也只能处理几万个写命令。
2.
为了兼顾数据安全和写入性能, 用户可以考虑使用appendfsynceverysec选项, 让Redis以每秒一次的频率对AOF文件进行同步。 Redis每秒同步一次AOF文件时的性能和不使用任何持久化特性时的性能相差无几, 而通过每秒同步一次AOF文件, Redis可以保证, 即使出现系统崩溃, 用户也最多只会丢失一秒之内产生的数据。 当硬盘忙于执行写入操作的时候, Redis还会优雅地放慢自 己的速度以便适应硬盘的最大写入速度。
3.
如果用户使用appendfsync no选项, 那么 Redis将不对AOF文件执行任何显式的同步操作, 而是由操作系统来决定应该在何时对
AOF文件进行同步。 这个选项在一般情况下不会对Redis的性能带来影响, 但系统崩溃将导致使用这种选项的Redis服务器丢失不定数量的数据。
AOF持久化也有缺陷——那就是AOF文件的体积大小。
AOF持久化既可以将丢失数据的时间窗口降低至1秒(甚至不丢失任何数据) , 又可以在极短的时间内完成定期的持久化操作, 那么我们有什么理由不使用AOF持久化呢?
但是这个问题实际上并没有那么简单, 因为Redis会不断地将被执行的写命令记录到AOF文件里面, 所以随着Redis不断运行, AOF文件的体积也会不断增长, 在极端情况下, 体积不断增大的AOF文件甚至可能会用完硬盘的所有可用空间。 还有另一个问题就是, 因为Redis在重启之后需要通过重新执行AOF文件记录的所有写命令来还原数据集, 所以如果AOF文件的体积非常大, 那么还原操作执行的时间就可能会非常长。
4.1.3 重写/压缩AOF文件
为了解决AOF文件体积不断增大的问题, 用户可以向Redis发送BGREWRITEAOF命令, 这个命令会通过移除AOF文件中的冗余命令来重写( rewrite) AOF文件, 使AOF文件的体积变得尽可能地小。
GREWRITEAOF的工作原理和BGSAVE创建快照的工作原理非常相似: Redis会创建一个子进程, 然后由子进程负责对AOF文件进行重写。 因为AOF文件重写也需要用到子进程, 所以快照持久化因为创建子进程而导致的性能问题和内存占用问题, 在AOF持久化中也同样存在。更糟糕的是, 如果不加以控制的话, AOF文件的体积可能会比快照文件的体积大好几倍, 在进行AOF重写并删除旧AOF文件的时候, 删除一个体积达到数十GB大的旧AOF文件可能会导致操作系统挂起( hang) 数秒。
跟快照持久化可以通过设置save选项来自 动执行BGSAVE一样, AOF持久化也可以通过设置auto-aof-rewrite-percentage选项和autoaof-rewrite-min-size选项来自 动执行BGREWRITEAOF。 举个例子,假设用户对Redis设置了配置选项auto-aof-rewrite-percentage100和auto-aof-rewrite-min-size 64mb, 并且启用了 AOF持久化,那么当AOF文件的体积大于64 MB, 并且AOF文件的体积比上一次重写之后的体积大了至少一倍( 100%) 的时候, Redis将执行BGREWRITEAOF命令。 如果AOF重写执行得过于频繁的话, 用户可以考虑将auto-aofrewrite-percentage选项的值设置为100以上, 这种做法可以让Redis在AOF文件的体积变得更大之后才执行重写操作, 不过也会让Redis在启动时还原数据集所需的时间变得更长。
4.2 复制
关系数据库通常会使用一个主服务器( master) 向多个从服务器( slave) 发送更新, 并使用从服务器来处理所有读请求。
Redis也采用了同样的方法来实现自 己的复制特性, 并将其用作扩展性能的一种手段
尽管Redis的性能非常优秀, 但它也会遇上没办法快速地处理请求的情况, 特别是在对集合和有序集合进行操作的时候, 涉及的元素可能会有上万个甚至上百万个, 在这种情况下, 执行操作所花费的时间可能需要以秒来进行计算, 而不是毫秒或者微秒。 但即使一个命令只需要花费10毫秒就能完成, 单个Redis实例( instance) 1秒也只能处理100个命x令。
用户可以通过设置额外的Redis从服务器来保存数据集的副本。 在接收到主服务器发送的数据初始副本( initial copy ofthe data) 之后, 客户端每次向主服务器进行写入时, 从服务器都会实时地得到更新。 在部署好主从服务器之后, 客户端就可以向任意一个从服
务器发送读请求了, 而不必再像之前一样, 总是把每个读请求都发送给主服务器(客户端通常会随机地选择使用哪个从服务器, 从而将负载平均分配到各个从服务器上) 。
4.2.1 对Redis的复制相关选项进行配置
4.1.1节中曾经介绍过, 当从服务器连接主服务器的时候, 主服务器会执行BGSAVE操作。 因此为了正确地使用复制特性, 用户需要保证主服务器已经正确地设置了代码清单4-1里面列出的dir选项和dbfilename选项, 并且这两个选项所指示的路径和文件对于Redis进程来说都是可写的( writable) 。
1.开启从服务器所必须的选项只有slaveof一个。 如果用户在启动Redis服务器的时候, 指定了一个包含slaveof host port选项的配置文件, 那么 Redis服务器将根据该选项给定的IP地址和端口号来连接主服务器。
2.对于一个正在运行的Redis服务器, 用户可以通过发送SLAVEOF no one命令来让服务器终止复制操作, 不再接受主服务器的数据更新; 也可以通过发送SLAVEOF host port命令来让服务器开始复制一个新的主服务器。
4.2.2 Redis复制的启动过程
Redis在复制进行期间也会尽可能地处理接收到的命令请求, 但是, 如果主从服务器之间的网络带宽不足, 或者主服务器没有足够的内存来创建子进程和创建记录写命令的缓冲区, 那么 Redis处理命令请求的效率就会受到影响。 因此, 尽管这并不是必须的, 但在实际中最好还是让主服务器只使用50%~65%的内存, 留下30%~45%的内存用于执行BGSAVE命令和创建记录写命令的缓冲区。
从服务器在进行同步时, 会清空自 己的所有数据 因为有些用户在第一次使用从服务器时会忘记这件事, 所以这里要特别提醒一下: 从服务器在与主服务器进行初始连接时, 数据库中原有的所有数据都将丢失, 并被替换成主服务器发来的数据。
当多个从服务器尝试连接同一个主服务器的时候, 就会出现表4-3所示的两种情况中的其中一种。
4.2.3 主从链
创建多个从服务器可能会造成网络不可用——当复制需要通过互联网进行或者需要在不同数据中心之间进行时, 尤为如
此。 因为Redis的主服务器和从服务器并没有特别不同的地方, 所以从服务器也可以拥有自 己的从服务器, 并由此形成主从链( master/slavechaining) 。
从服务器对从服务器进行复制在操作上和从服务器对主服务器进行复制的唯一区别在于, 如果从服务器X拥有从服务器Y, 那么当从服务器X在执行表4-2中的步骤4时, 它将断开与从服务器Y的连接, 导致从服务器Y需要重新连接并重新同步( resync) 。
当读请求的重要性明显高于写请求的重要性, 并且读请求的数量远远超出一台Redis服务器可以处理的范围时, 用户就需要添加新的从服务器来处理读请求。
随着负载不断上升, 主服务器可能会无法快速地更新所有从服务器, 或者因为重新连接和重新同步从服务器而导致系统超
载。 为了缓解这个问题, 用户可以创建一个由Redis主从节点( master/slave node) 组成的中间层来分担主服务器的复制工作
尽管主从服务器之间并不一定要像图4-1那样组成一个树状结构, 但记住并理解这种树状结构对于Redis复制来说是可行的( possible) 并且是合理的( reasonable) 将有助于读者理解之后的内容。
本书在前面的4.1.2节中曾经介绍过, AOF持久化的同步选项可以控制数据丢失的时间长度: 通过将每个写命令同步到硬盘里面, 用户几乎可以不损失任何数据(除非系统崩溃或者硬盘驱动器损坏) , 但这种做法会对服务器的性能造成影响;
另一方面, 如果用户将同步的频率设置为每秒一次, 那么服务器的性能将回到正常水平, 但故障可能会造成1秒的数据丢失。
通过同时使用复制和AOF持久化, 我们可以将数据持久化到多台机器上面。
4.2.4 检验硬盘写入
为了验证主服务器是否已经将写数据发送至从服务器, 用户需要在向主服务器写入真正的数据之后, 再向主服务器写入一个唯一的虚构值( unique dummy value) , 然后通过检查虚构值是否存在于从服务器来判断写数据是否已经到达从服务器, 这个操作很容易就可以实现。
另一方面, 判断数据是否已经被保存到硬盘里面则要困难得多。 对于每秒同步一次AOF文件的Redis服务器来说, 用户总是可以通过等待1秒来确保数据已经被保存到硬盘里面; 但更节约时间的做法是, 检查INFO命令的输出结果中aof_pending_bio_fsync属性的值是否为0, 如果是的话,那么就表示服务器已经将已知的所有数据都保存到硬盘里面了。 在向主服务器写入数据之后, 用户可以将主服务器和从服务器的连接作为参数, 调用代码清单4-3所示的函数来自 动进行上述的检查操作。
INFO命令中的其他信息 INFO命令提供了大量的与Redis服务器当前状态有关的信息, 比如内存占用量、 客户端连接数、 每个数据库包含的键的数量、 上一次创建快照文件之后执行的命令数量, 等等。 总的来说, INFO命令对于了解Redis服务器的综合状态非常有帮助
4.3 处理系统故障
跟提供了 ACID②保证的传统关系数据库不同, 在使用Redis为后端构建应用程序的时候, 用户需要多做一些工作才能保证数据的一致性。
4.3.1 验证快照文件和AOF文件
无论是快照持久化还是AOF持久化, 都提供了在遇到系统故障时进行数据恢复的工具。
Redis提供了两个命令行程序redis-check-aof和redis-check-dump,
它们可以在系统故障发生之后, 检查AOF文件和快照文件的状态, 并在有需要的情况下对文件进行修复。 在不给定任何参数的情况下运行这两个程序, 就可以看见它们的基本使用方法
如果用户在运行redis-check-aof程序时给定了--fix参数, 那么程序将对AOF文件进行修复。 程序修复AOF文件的方法非常简单: 它会
扫描给定的AOF文件, 寻找不正确或者不完整的命令, 当发现第一个出错命令的时候, 程序会删除出错的命令以及位于出错命令之后的所有命令, 只保留那些位于出错命令之前的正确命令。 在大多数情况下, 被删除的都是AOF文件末尾的不完整的写命令
遗憾的是, 目 前并没有办法可以修复出错的快照文件。 尽管发现快照文件首个出现错误的地方是有可能的, 但因为快照文件本身经过了压缩, 而出现在快照文件中间的错误有可能会导致快照文件的剩余部分无法被读取。 因此, 用户最好为重要的快照文件保留多个备份, 并在进行数据恢复时, 通过计算快照文件的SHA1散列值和SHA256散列值来对内容进行验证。 (当今的Linux平台和Unix平台都包含类似sha1sum和sha256sum这样的用于生成和验证散列值的命令行程序。 )
校验和( checksum) 与散列值( hash) 从2.6版本开始, Redis会在快照文件中包含快照文件自 身的CRC64校验和。
4.3.2 更换故障主服务器
现在让我们来看看, 在拥有一个主服务器和一个从服务器的情况下, 更换主服务器的具体步骤。
假设A、 B两台机器都运行着Redis, 其中机器A的Redis为主服务器,而机器B的Redis为从服务器。 不巧的是, 机器A刚刚因为某个暂时无法修复的故障而断开了网络连接, 因此用户决定将同样安装了 Redis的机器C用作新的主服务器。更换服务器的计划非常简单: 首先向机器B发送一个SAVE命令, 让它创建一个新的快照文件, 接着将这个快照文件发送给机器C, 并在机器C上面启动Redis。 最后, 让机器B成为机器C的从服务器③。
另一种创建新的主服务器的方法, 就是将从服务器升级( turn) 为主服务器, 并为升级后的主服务器创建从服务器。
Redis Sentinel
Redis Sentinel可以监视指定的Redis主服务器及其属下的从服务器, 并在主服务器下线时自 动进行故障转移( failover) 。 本书将在第10章介绍Redis Sentinel。
4.4 Redis事务
在Redis里面也有简单的方法可以处理一连串相互一致的读操作和写操作。 正如本书在3.7.2节中介绍的那样, Redis的事务以特殊命令MULTI为开始, 之后跟着用户传入的多个命令, 最后以EXEC为结束。
但是由于这种简单的事务在EXEC命令被调用之前不会执行任何实际操作, 所以用户将没办法根据读取到的数据来做决定。
这个问题看上去似乎无足轻重, 但实际上无法以一致的形式读取数据将导致某一类型的问题变得难以解决, 除此之外, 因为在多个事务同时处理同一个对象时通常需要用到二阶提交( two-phase commit) , 所以如果事务不能以一致的形式读取数据, 那么二阶提交将无法实现, 从而导致一些原本可以成功执行的事务沦落至执行失败的地步。
延迟执行事务有助于提升性能 因为Redis在执行事务的过程中, 会延迟执行已入队的命令直到客户端发送EXEC命令为止。 因此, 包括本书使用的Python客户端在内的很多Redis客户端都会等到事务包含的所有命令都出现了之后, 才一次性地将MULTI命令、 要在事务中执行的一系列命令, 以及EXEC命令全部发送给Redis, 然后等待直到接收到所有命令的回复为止。 这种“一次性发送多个命令, 然后等待所有回复出现”的做法通常被称为流水线( pipelining) , 它可以通过减少客户端与Redis服务器之间的网络通信次数来提升Redis在执行多个命令时的性能。
例子:商品买卖市场
略
为了将商品放到市场上进行销售, 程序除了要使用MULTI命令和EXEC命令之外, 还需要配合使用WATCH命令, 有时候甚至还会用
到UNWATCH或DISCARD命令。 在用户使用WATCH命令对键进行监视之后, 直到用户执行EXEC命令的这段时间里面, 如果有其他客户端抢先对任何被监视的键进行了替换、 更新或删除等操作, 那么当用户尝试执行EXEC命令的时候, 事务将失败并返回一个错误(之后用户可以选择重试事务或者放弃事务) 。 通过使用WATCH、 MULTI/EXEC、UNWATCH/DISCARD等命令, 程序可以在执行某些重要操作的时候, 通过确保自 己正在使用的数据没有发生变化来避免数据出错。
什么是DISCARD? UNWATCH命令可以在WATCH命令执行之后、 MULTI命令执行之前对连接进行重置( reset) ; 同样地, DISCARD 命令也可以在MULTI命令执行之后、 EXEC命令执行之前对连接进行重置。 这也就是说, 用户在使用WATCH监视一个或多个键, 接着使 用MULTI开始一个新的事务, 并将多个命令入队到事务队列之后, 仍然可以通过发送DISCARD命令来取消WATCH命令并清空所有已入队命令。 本章展示的例子都没有用到DISCARD, 主要原因在于我们已经清楚地知道自 己是否想要执行MULTI/EXEC或者UNWATCH, 所以没有必要在这些 例子里面使用DISCARD。 |
为什么 Redis没有实现典型的加锁功能? 在访问以写入为目 的数据的时候( SQL中的SELECT FOR UPDATE) , 关系数据库会对被访问的数据行进行加锁, 直到事务被提交( COMMIT) 或者被回滚( ROLLBACK) 为止。 如果有其他客户端试图对被加锁的数据行进行写入, 那么该客户端将被阻塞, 直到第一个事务执行完毕为止。 加锁在实际使用中非常有效, 基本上所有关系数据库都实现了这种加锁功能, 它的缺点在于, 持有锁的客户端运行越慢, 等待解锁的客户端被阻塞的时间就越长。
因为加锁有可能会造成长时间的等待, 所以Redis为了尽可能地减少客户端的等待时间, 并不会在执行WATCH命令时对数据进行加锁。 相反地, Redis只会在数据已经被其他客户端抢先修改了的情况下, 通知执行了WATCH命令的客户端,
这种做法被称为乐观锁( optimisticlocking) , 而关系数据库实际执行的加锁操作则被称为悲观锁( pessimistic locking) 。 乐观锁在实际使用中同样非常有效, 因为客户端永远不必花时间去等待第一个取得锁的客户端——它们只需要在自 己的事务执行失败时进行重试就可以了。
4.5 非事务型流水线
本节将介绍如何在不使用事务的情况下, 通过使用流水线来进一步提升命令的执行性能。
第2章曾经介绍过一些可以接受多个参数的添加命令和更新命令, 如MGET、 MSET、 HMGET、 HMSET、 RPUSH和LPUSH、 SADD、 ZADD等。 这些命令简化了那些需要重复执行相同命令的操作, 并且极大地提升了性能。 尽管效果可能没有以上提到的命令那么显著, 但使用非事务型流水线( non-transactional pipeline) 同样可以获得相似的性能提升, 并且可以让用户同时执行多个不同的命令。
在需要执行大量命令的情况下, 即使命令实际上并不需要放在事务里面执行, 但是为了通过一次发送所有命令来减少通信次数并降低延迟值, 用户也可能会将命令包裹在MULTI和EXEC里面执行。 遗憾的是, MULTI和EXEC并不是免费的——它们也会消耗资源, 并且可能会导致其他重要的命令被延迟执行。 不过好消息是, 我们实际上可以在不使用MULTI和EXEC的情况下, 获得流水线带来的所有好处。
第3章和4.4节中都使用了以下语句来在Python中执行MULTI和EXEC命令:
6 关于性能方面的注意事项
要对Redis的性能进行优化, 用户首先需要弄清楚各种类型的Redis命令到底能跑多块, 而这一点可以通过调用Redis附带的性能测试程序redis-benchmark来得知, 代码清单4-10展示了一个相应的例子。 如果有兴趣的话, 读者也可以试着用redis-benchmark来了解Redis在自 己服务器上的各种性能特征。
代码清单4-10 在装有英特尔酷睿2双核2.4 GHz处理器的台式电脑上运行redis-benchmark
redis-benchmark的运行结果展示了一些常用Redis命令在1秒内可以执行的次数。 如果用户在不给定任何参数的情况下运行redisbenchmark, 那么redis-benchmark将使用50个客户端来进行性能测试, 但是为了在redis-benchmark和我们自 己的客户端之间进行性能对比, 让redis-benchmark只使用一个客户端要比使用多个客户端更方便一些。
在考察redis-benchmark的输出结果时, 切记不要将输出结果看作是应用程序的实际性能, 这是因为redis-benchmark不会处理执行命
令所获得的命令回复, 所以它节约了大量用于对命令回复进行语法分析的时间。 在一般情况下, 对于只使用单个客户端的redis-benchmark来说, 根据被调用命令的复杂度, 一个不使用流水线的Python客户端的性能大概只有redis-benchmark所示性能的50%~60%。
另一方面, 如果你发现自 己客户端的性能只有redis-benchmark所示性能的25%至30%, 或者客户端向你返回了 “Cannot assign requested address”(无法分配指定的地址) 错误, 那么你可能是不小心在每次发送命令时都创建了新的连接。
另一个引 起性能问题的原因是以不正确的方式使用Redis的数据结构)
大部分Redis客户端库都提供了某种级别的内置连接池( connection pool) 。 以Python的Redis客户端为例, 对于每个Redis服务器, 用户只需要创建一个redis. Redis()对象, 该对象就会按需创建连接、 重用已有的连接并关闭超时的连接(在使用多个数据库的情况下, 即使客户端只连接了一个Redis服务器, 它也需要为每一个被使用的数据库创建一个连接) , 并且Python客户端的连接池还可以安全地应用于多线程环境和多进程环境。
4.7 小结