Redis设计与实现-笔记(四)

文章目录

第四部分 独立功能的实现

第18章 发布与订阅

Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。
通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):没定有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会受到这条信息。
除了订阅频道之外,客户端还可以通过执行PSUBCRIBE命令订阅一个或多个模式,从而成为这些模式的订阅者:每当有其他客户端向某个频道发送消息时,消息不仅会被发送刚给这个频道的所有订阅者,它还会被发送给所有与这个频道相匹配的模式的订阅者。

18.1 频道的订阅与退订

当一个客户端执行SUBSCRIBE命令订阅某个或者某些频道的时候,这个客户端与被订阅频道之间就建立起了一种订阅关系。
Redis将所有频道订阅关系够保存在服务器状态的pubsub_channels字典里面,这个字典的键时某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端:

struct redisServer {
	// ...
	// 保存所有频道的订阅关系
	dict *pubsub_channels;
	// ...
};
18.1.1 订阅频道

每当客户端执行SUBCRIBE命令订阅某个或某些频道的时候,服务器都会将客户端与被订阅的频道在pubsub_channels字典中进行关联。
根据频道是否已经有其他订阅者,关联操作分为两种情况执行:

  • 如果频道已经有其他订阅者,那么它在pubsub_channels字典中必然有相应的订阅者链表,程序唯一要做的就是将客户端添加到订阅者链表的末尾。
  • 如果频道没有任何订阅者,那么它必然不存在于pubsub_channels字典,程序首先要在pubsub_channels字典中为频道创建一个键,并将这个键的值设置为空链表,然后再将客户端添加到链表,成为链表的第一个元素。
18.1.2 退订频道

UNSUBSCRIBE命令的行为和SUBSCRIBE命令的行为正好相反,当一个客户端退订某一个或某些频道的时候,服务器将从pubsub_channels中解除客户端与被退订频道之间的关联:

  • 程序会根据被退订频道的名字,在pubsub_channels字典中找到频道对应的订阅者链表,然后,从订阅表链表中删除退订客户端的信息。
  • 如果删除退订客户端之后,频道的订阅者链表变成了空链表,那么说明这个频道已经没有任何订阅者了,程序将从pubsub_channels字典中删除频道对应的键。
18.2 模式的订阅与退订

服务器将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面:

struct redisServer {
	// 保存所有模式订阅关系
	list *pubsub_patterns;
}

pubsub_patterns属性是一个链表,链表中的每个节点包含着一个pubsubPattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性在记录了订阅模式的客户端:

typedef struct pubsubPattern {
	// 订阅模式的客户端
	RedisClient *client;
	// 被订阅的模式
	robj *pattern;
} pubsubPattern;
18.2.1 订阅模式

每当客户端执行PSUBCRIBE命令订阅某个或某些模式的时候,服务器会对每个被订阅的模式执行以下的两个操作:

  1. 新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端
  2. 将pubsubPattern结构添加到pubsub_patterns链表的表尾。
18.2.2 退订模式

模式的退订命令PUNSUBSCRIBE是PSUBSCRIBE命令的反操作:当一个客户端退订某个或某些模式的时候,服务器将在pubsub_patterns链表中查找并删除那些pattern属性为被退订模式,并且client属性为执行退订命令的客户端的pubsubPattern结构。

18.3 发送消息

当一个Redis客户端执行PUBLISH 命令将消息message发送给频道channel的时候,服务器需要执行以下两个动作:

  1. 将消息message发送给channel频道的所有订阅者
  2. 如果有一个或多个模式pattern与频道channel相匹配,那么将消息message发送给pattern模式的订阅。
18.3.1 将信息发送给频道订阅者

根据pubsub_channels字典中的键(channel)对应的值(client链表)。

18.3.2 将消息发送模式订阅者

遍历pubsub_patterns链表,查找那些与channel频道相匹配的模式,并将消息发送给订阅了这些模式的客户端。

18.4 查看订阅消息

PUBSUB命令Redis2.8新增加的命令,来查看频道或者模式的相关信息,如,某个频道目前哟多少订阅者,有或者某个模式目前有多少订阅者。

18.4.1 PUBSUB CHANNELS
PUNSUB CHANNELS [pattern]

用于返回服务器当前被订阅的频道,pattern可选,1)不给定,返回服务器当前被订阅的所有频道;2)给定,返回服务器房钱被订阅的频道中那些与pattern模式相匹配的频道。
通过遍历pubsub_channels字典的所有键,然后记录并返回所有符号条件的频道。

18.4.2 PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 channel-2...channel-n]

接受任意多个频道作为输入参数,并返回这些频道的订阅者数量
通过在pubsub_channels字典中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现的。

18.4.3 PUBSUB NUMPAT
PUBSUB NUMPAT

用于返回服务器当前被订阅模式的数量。
通过返回pubsub_patterns链表的长度来实现。

重点回顾
  • 服务器状态在pubsub_channels字典保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和订阅的频道关联到这个字典里面,而UNSUBSCRIBE命令则负责解除客户端和被退订频道之间的关联。
  • 服务器状态在pubsub_patterns链表保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到这个链表中,PUNSUBSCRIBE命令则负责溢出客户端和被退订模式在链表中的记录。
  • PUBLISH命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub_patterns链表来向所有匹配频道的模式的订阅者发送消息。
  • PUBSUB命令的三个子命令都是通过读取pubsub_channels字典和pubsub_patterns链表中的信息来实现的。

第19章 事务

Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。
事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

19.1 事务的实现

一个事务从开始到结束通常会经历一下三个阶段:
1)事务开始
2)命令入队
3)事务执行

19.1.1 事务开始

MULTI命令执行标志着事务的开始。
MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的。

19.1.2 命令入队

当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行;
当一个客户端处于事务状态时,服务器会根据这个客户端发来的不同命令执行不同的操作:

  • 如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,那么服务器立即执行这个命令。
  • 与此相反,如果客户端发送的命令时EXEC、DISCARD、WATCH、MULTI四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUE回复。
19.1.3 事务队列

每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性

typedef struct redsiClient {
	// 事务状态
	multiState mstate;
} redisClient;

事务状态包含一个事务队列,以及一个已入队命令的计数器(也可以说是事务队列的长度):

typedef struct multState {
	// 事务队列,FIFO顺序
	multCmd *commands;
	// 已入队命令计数
	int count;
} multiState;

事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的函数,以及参数的数量。

typedef struct multiCmd {
	// 参数
	robj **argv;
	// 参数数量
	int argc;
	// 命令指针
	struct redisCommand *cmd;
} multiCmd;

事务队列先进先出(FIFO)的方式保存入队的命令。较先入队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面。

19.1.4 执行事务

当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历整个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部分会给客户端。

19.2 WATCH命令的实现

WATCH命令时一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

19.2.1 使用WATCH 命令监视数据库键

每个Redis数据库都保存着一个watched_keys字典,这个字典的键时某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端:

typedef struct redisDb {
	// 正在被WATCH命令监视的键
	dict *watched_keys;
} redisDb;

通过watched_keys字典,服务器可以清楚地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键

19.2.2 监视机制的触发

所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等待,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,标识该客户端的事务安全性已经被破坏。

19.2.3 判断事务是否安全

当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否发开了REDIS_DIRTY_CAS标识来决定是否执行事务:

  • 如果客户端的REDIS_DIRTY_CAS标识已经被发开,那么说明客户端所监视的键当中,至少有一个键已经被修改过了,在这种情况下,客户端提交的事务已经不再安全,所以服务器会拒绝执行客户端提交的事务。
  • 如果客户端的REDIS_DIRTY_CAS表示没有被打开,那么说明客户端监视的所有键都没有被修改过(或者客户端没有监视任何键),事务仍然是安全的,服务器将执行客户端提交的这个事务。
19.2.4 一个完整的WATCH事务执行过程
19.3 事务的ACID性质

在Redis中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性(Durability)。

19.3.1 原子性

对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,那么就一个都不执行。Redis的事务是具有原子性的。
Redis的事务和传统的关系型数据库事务的最大区别在于,
Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,知道将事务队列中的所有命令都执行完毕为止。
Redis的作者在事务功能的文档中解释说,不支持事务回滚是因为这种复杂的功能和Redis追求简单高效的设计主旨不相符,并且他认为,Redis事务的执行时错误通常都是编程错误产生的,这种错误通常只会出现在开发环境中,而很少会在实际的生产环境中出现,所以他认为没有要为Redis开发事务回滚功能。

19.3.2 一致性

事务具有一致性指的是,如果数据库执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。
“一致”指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。
1)入队错误,
如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝这个事务。
2) 执行错误,
执行过程中发生错误,1)执行过程中发生的错误都是一些不能再入队时被服务器发现的错误,这些错误只会在命令实际执行时被触发。2)即时在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响。对数据库键执行了错误类型的操作是事务执行期间最常见的错误之一。
3) 服务器停机,
根据服务器持久化模式,可能有一下情况:

  • 如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的
  • 如果服务器运行RDB模式下,那么在事务中途停机不会导致不一致,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用RDB文件,那么重启之后的数据库将是空白的,而空白的数据库总是一致的。
  • 如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用AOF文件,那么重启之后的数据库将是空白的,而空白的数据库总是一致的。
19.3.3 隔离性

事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
因为Redis使用单线程的方式(6.0以后可以多线程)来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行终端,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。

19.3.4 耐久性

事务的耐久性指的事,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后,停机,执行事务所得的结果,也不会丢失。
因为Redis的事务不过是简单地用队列包裹起了一组Redis命令,Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定:

  • 当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的偶有服务器数据都将丢失。
  • 当服务器在RDB持久化模式下运作时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE命令,对数据库进行保存操作,并且异步执行的BGSAVE不能保证事务数据被第一时间保存到硬盘里面,因此RDB持久化模式下的事务也不具有耐久性。
  • 当服务器运行在AOF持久化模式下,并且appendsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正的保存到硬盘里面,因此这种配置下的事务是具有耐久性的。
  • 当服务器运行AOF持久化模式下,并且appendfsync选项的值为everysec时,程序会每秒同步一次命令数据到硬盘。因为停机可能会恰好发生在等待同步的那一秒钟之内,这可能会造成事务数据丢失,所以这种配置下的事务不具有耐久性。
  • 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为no时,程序会交由操作系统来决定何时将命令数据同步到硬盘。因为事务可能在等待同步的过程中丢失,所以这种配置下的事务不具有耐久性。

配置选项no-appendfsync-on-rewrite可以配合appendfsync选项为always或者everysec的AOF持久化模式使用。
当no-appendfsync-on-rewrite选项处于打开状态时,在执行BGSAVE命令或者BGREWRITEAOF命令期间,服务器会暂时停止对AOF文件进行同步,从而尽可能地减少I/O阻塞。但是这样一来,关于“always模式的AOF持久化可以保证事务的耐久性”这一结论不再成立,因为在服务器停止对AOF文件进行同步期间,事务结果可能会因为停机而丢失。
因此,如果服务器打开了no-appendfsync-on-rewrite选项,那么即使服务器运行在always模式的AOF持久化下,事务也不具有耐久性。默认no-appendfsync-on-rewrite关闭。

不论Redis在什么模式下运作,在一个事务的最后加上SAVE命令总可以保证事务的耐久性,但,效率太低,并不具有实用性。

19.4 重点回顾
  • 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制。
  • 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行。
  • 事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。
  • 带有WATCH命令的事务会将客户端和被监视的键在数据库的watched_keys字典进行关联,当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS标识打开。
  • 只有客户端的REDIS_DIRTY_CAS标志未被打开时,服务器才会执行客户端提交的事务,否则的话,服务器将拒绝执行客户端提交的事务。
  • Redis的事务总是具有ACID中的原子性、一致性和隔离性,当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

第20章 Lua脚本

通过在服务器嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。

20.1 创建并修改Lua环境

为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境(environment),并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足Redis服务器的需要。
Redis服务器创建并修改Lua环境的整个过程由以下步骤组成:

  1. 创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的。
  2. 载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数据操作。
  3. 创建全局表格redis,这个表格包含了对Redis进行操作的函数,比如用于在Lua脚本中执行Redis命令的redis.call函数。
  4. 使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用。
  5. 创建排序辅助函数,Lua环境使用这个辅佐函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性。
  6. 创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息。
  7. 对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程总,将额外的全局变量添加到Lua环境中。
  8. 将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的Lua脚本。
20.1.1 创建Lua环境

在最开始的这一步,服务器首先调用Lua的C API函数lua_open,创建一个新的Lua环境。
因为lua_open函数创建的只是一个基本的lua环境,为了让这个Lua环境可以满足Redis的操作要求。

20.1.2 载入函数库

Redis修改Lua环境的第一步,就是将以下函数库载入到Lua环境里面:

  • 基础库(base library):这个库包含Lua的核心(core)函数
  • 表格库(table library):
  • 字符串库(string library):
  • 数学库(math library):
  • 调试库(debug library):
  • Lua CJSON库(/lua-cjson.php):
  • Struct 库:
  • Lua cmsgpack库:
20.1.3 创建redis全局表格

服务器将在Lua环境中创建一个redis表格(table),并将它设为全局变量。这个redis表格包含以下函数:

  • 用于执行Redis命令的redis.call和redis.pcall函数
  • 用于记录Redis日志(log)的redis.log函数,以及相应的日志级别(level)常量:redis.LOG_DEBUG,redis.LOG_VERBOSE,redis.LOG_NOTICE,以及redis.LOG_WARNING。
  • 用于计算SHA1校验和的redis.shalhex函数
  • 用于返回错误信息的redis.error_reply函数和redis.status_reply函数。
20.1.4 使用Redis自制的随机函数来替换Lua原有的随机函数

为了保证相同的脚本可以再不同的机器上产生相同的结果,Redis要求所有传入服务器的Lua脚本,以及Lua环境中的所有函数,都必须是无副作用(side effect)的纯函数(pure function)。
但是,在之前载入Lua环境math函数库中,用于生成随机数的math.random函数和math.randomseed函数都是带有副作用的,他们不符合Redis对Lua环境的无副作用要求。
所以,Redis使用自制的函数替换了math库中原有的math.random函数和math.randomseed函数,替换之后的两个函数有以下特征:

  • 对于相同的seed来说,meth.random总产生相同的随机数字列,这个函数是一个纯函数。
  • 除非在脚本中使用math.randomseed显示地修改seed,否则每次运行脚本时,Lua环境都使用固定的math.randomseed(0)语句来初始化seed。
20.1.5 创建排序辅助函数

对于Lua脚本来说,另一个可能产生不一致数据的地方是那些带有不确定性质的命令。
对于一个集合键来说,因为集合元素的排列是无序的,所以即使两个集合的元素完全相同,他们的输出结果也可能并不相同。
Redis将这种在相同数据集上可能会产生不同输出的命令称为“带有不确定性的命令”,这些命令包括:

  • SINTER
  • SUNION
  • SDIFF
  • SMEMBERS
  • HKEYS
  • HVALS
  • KEYS

为了消除这些命令带来的不确定性,服务器会为Lua环境创建一个排序辅助函数__redis__compare_helper,当Lua脚本执行完一个带有不确定性的命令之后,程序会使用__redis__compare_helper作为对比函数,自动调用table.sort函数对命令的返回值做一次排序,一次来保证相同的函数集总是产生相同的输出。

20.1.6 创建redis.pcall函数的错误报告辅助函数

服务器将为Lua环境创建一个名为__redis__err__handler的错误处理函数,当脚本调用redis.pcall函数执行Redis命令,并且被执行的命令出现错误时,__redis__err_handler就会打印出错代码的来源和发生错误的行数,为程序的调试提供方便。

20.1.7 保护Lua的全局环境

服务器对Lua环境中的全局环境进行保护,确保传入服务器的脚本不会因为忘记使用local关键字而将额外的全局变量添加到Lua环境里面。
Redis并未禁止用户修改已存在的全局变量,所以在执行Lua脚本的时候,必须非常小心,以免错误地修改了已存在的全局变量。

20.1.8 将Lua环境保存到服务器状态的lua属性里面

最后一步,服务器会将Lua环境和服务器的lua属性关联起来。
因为Redis使用串行化的方式来执行Redis命令,所以在任何特定时间里,最多都只会有一个脚本能够被放进Lua环境里面运行,因此,整个Redis服务器只需要创建一个Lua环境即可。

20.2 Lua环境协作组件

除了创建并修改Lua环境之外,Redis服务器还创建了两个用于与Lua环境进行协作的组件,他们分别是负责执行Lua脚本中的Redis命令的伪客户端,以及用于保存Lua脚本的lua_scripts字典。

20.2.1 伪客户端

因为执行Redis命令必须有相应的客户端状态,所以为了执行Lua脚本中包含的Redis命令,Redis服务器专门为Lua环境创建了一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有Redis命令。
Lua脚本使用redis.call函数或者redis.pcall函数执行一个Redis命令,需要的步骤:

  1. Lua环境将redis.call函数或者redis.pcall函数想要执行的命令传给伪客户端。
  2. 伪客户端将脚本想要执行的命令传给命令执行器。
  3. 命令执行器执行伪客户端传给它的命令,并将命令的执行结果返回给伪客户端。
  4. 伪客户端接收命令执行器返回的命令结果,并将这个命令结果返回给Lua环境。
  5. Lua环境在接收到命令结果之后,将该结果返回给redis.call函数或者redis.pcall函数
  6. 接收到结果的redis.call函数或者redis.pcall函数会将命令结果作为函数返回值返回给脚本中的调用者。
20.2.2 lua_scripts字典

这个字典的键为某个Lua脚本的SHA1校验和(checksum),而字典的值则是SHA1校验和对应的Lua脚本。
Redis服务器会将所有被EVAL命令执行过的Lua脚本,以及所有被SCRIPT LOAD命令载入过的Lua脚本都保存到lua_scripts字典里面。
lua_scripts字典由两个作用,一个是实现SCRIPT EXISTS命令,两个是实现脚本复制功能。

20.3 EVAL命令的实现

EVAL命令的执行过程可以分为三个步骤:

  1. 根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数
  2. 将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用
  3. 执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。
20.3.1 定义脚本函数

当客户端向服务器发送EVAL命令,要求执行某个Lua脚本的时候,服务器首先要做的就在Lua环境中,为传入的脚本定义一个与这个脚本相对应的Lua函数,其中,Lua函数的名字由f_前缀加上脚本的SHA1校验和(四十个字符长)组成,而函数的体(body)则是脚本本身。
使用函数来保存客户端传入的脚本有以下好处:

  • 执行脚本的步骤非常简单,只要调用与脚本相对应的函数即可。
  • 通过函数的局部性来让Lua环境保持清洁,减少了垃圾回收的工作量,并且避免了使用全局变量
  • 如果某个脚本所对应的函数在Lua环境中被定义过至少一次,那么只要记得这个脚本的SHA1校验和,服务器就可以再不知道脚本本身的情况下,直接通过调用Lua函数来执行脚本,这是EVALSHA命令的实现原理。
20.3.2 将脚本函数保存到lua_scripts字典

服务器将在lua_scripts字典中新添加一个键值对,其中键为Lua脚本的SHA1校验和,值为Lua脚本本身。

20.3.3 执行脚本函数

在为脚本定义函数,并且将脚本保存到lua_scripts字典之后,服务器还需要进行一些设置钩子、传入参数之类的准备动作,才能正式开始执行脚本。
整个准备和执行脚本的过程:

  1. 将EVAL命令中传入的键名(key name)参数和脚本参数分别保存到KEYS数组和ARGV数组,然后将这两个数组作为全局变量传入到Lua环境里面。
  2. 为Lua环境装载超市处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过SCRIPT KILL命令停止脚本,或者通过SHUTDOWN命令直接关闭服务器。
  3. 执行脚本函数。
  4. 移除之前装载的超时钩子。
  5. 将执行脚本函数所得的结果保存到客户端状态的输出缓冲区里面,等待服务器将结果返回给客户端。
  6. 对Lua环境执行垃圾回收操作。

执行算是结束,之后服务器只要将保存在输出缓冲区里面的执行结果返回给执行EVAL命令的客户端就可以了。

20.4 EVALSHA命令的实现

每个被EVAL命令成功执行过的Lua脚本,在Lua环境里面都有一个与这个脚本相对应的Lua函数,函数的名字由f_前缀加上40个字符长的SHA1校验和组成。
只要脚本对应的函数曾经在Lua环境里面定义过,那么即使不知道脚本的内容本身,客户端也可以根据脚本的SHA1校验和来调用脚本对应的函数,从而达到执行脚本的目的,这就是EVALSHA命令实现原理。

20.5 脚本管理命令的实现

除了EVAL命令和EVALSHA命令之外,Redis中与Lua脚本有关的命令还有四个,他们分别是SCRIPT FLUSH命令、SCRIPT EXISTS命令、SCRIPT LOAD命令、以及SCRIPT KILL命令。

20.5.1 SCRIPT FLUSH

用于清除服务器中所有和Lua脚本有关的信息,这个命令会释放并重建lua_scripts字典,关闭现有的Lua环境并重新创建一个新的Lua环境。

20.5.2 SCRIPT EXISTS

SCRIPT EXISTS命令根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器中。
SCRIPT EXISTS命令时通过检查给定的校验和是否存在于lua_scripts字典来实现的。

SCRIPT EXISTS命令允许一次传入多个SHA1校验和。

实现SCRIPT EXISTS实际上并不需要lua_scripts字典的值。lua_scripts字典既保存脚本的SHA1校验和,又保存脚本本身的原因是为了实现脚本复制功能。

20.5.3 SCRIPT LOAD

SCRIPT LOAD命令所做的事情和EVAL命令执行脚本时所做的前两步完全一样:命令首先在Lua环境中为脚本创建相对应的函数,然后再将脚本保存到lua_scripts字典里面

20.5.4 SCRIPT KILL

如果服务器设置了lua-time-limit配置选项,那么在每次执行Lua脚本走之前,服务器都会在Lua环境里面设置一个超市处理钩子。
超时梳理钩子在脚本运行期间,会定期检查脚本已经运行了多长时间,一旦钩子发现脚本的运行时间已经超过了lua-time-limit选项设置的时长,钩子将定期在脚本运行的间隙中,查看是否有SCRIPT KILL命令或者SHUTDOWN命令到达服务器。
如果脚本已经执行过写入操作,那么客户端只能用SHUTDOWN nosave命令来停止服务器,从而防止不合法的数据被写入数据库中。

20.6 脚本复制

与其他普通Redis命令一样,当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包含EVAL命令、EVALSHA命令、SCRIPT FLUSH命令,以及SCRIPT LOAD命令。

20.6.1 复制EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令

与复制其他普通Redis命令的方法一样,当服务器执行完以上三个命令的其中一个时,主服务器会直接将被执行的命令传播给所有从服务器。

20.6.2 复制EVALSHA命令

EVALSHA命令是所有与Lua脚本有关的命令中,复制操作最复杂的一个,因为主服务器与从服务器载入Lua脚本的情况可能有所不同,所以主服务器不能像复制EVAL命令、、SCRIPT FLUSH命令和SCRIPT LOAD命令那样,直接将EVALSHA命令传播给从服务器。对于一个在主服务器被成功执行的EVALSHA命令来说,相同的EVALSHA命令在从服务器执行时却可能会出现脚本未找到错误。
原因:
1、现在,假设一个从服务器slave1开始复制主服务master,如果master不想办法将脚本传送给slave1载入的话,那么当客户端向主服务器发送命令时,master将成功执行这个EVALSHA命令,而当master将这个命令传播个slave1执行时,slave却会出现脚本未找到错误
2、因为多个从服务器之间载入Lua脚本的情况也可能各有不同,所以即使一个EVALSHA命令可以再某个从服务器执行成功,也不代表这个EVALSHA命令就一定可以在另一个从服务器成功执行。

所以,为了防止以上假设的情况出现,Redis要求主服务在传播EVALSHA命令的时候,必须确保EVALSHA命令要执行的脚本已经被所有从服务器再如果,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成一个等价的EVAL命令,然后通过传播EVAL命令来代替EVALSHA命令。
传播EVAL命令或将EVALSHA命令转换成EVAL命令,都需要用到服务器转台的lua_scripts字典和repl_scriptcache_dict字典。

  1. 判断传播EVALSHA命令是否安全的方法:repl_scriptcache_dict字典记录自己已经将哪些脚本传播给所有从服务器;如果一个脚本的SHA1校验和存在于lua_scripts字典,却不存在repl_scriptcache_dict字典,那么说明校验和对应的Lua脚本已经被主服务器载入,但是并没有传播给所有从服务器。
  2. 清空repl_scriptcache_dict字典:每当主服务器添加一个新的从服务器,主服务器都会清空自己的repl_scriptcache_dict字典,这时因为随着新从服务器的出现,repl_scriptcache_dict字典里面记录的脚本已经不再被所有从服务器载入过,所以主服务器会清空repl_scriptcache_dict字典,强制自己重新向所有从服务器传播脚本,从而确保新的从服务器不会出现脚本未找到错误。
  3. EVALSHA命令转换成EVAL命令的方法:通过使用EVALSHA命令指定的SHA1校验和,以及lua_scripts字典保存的Lua脚本,服务器总可以将一个EVALSHA命令转换成一个等价的EVAL命令。1)根据SHA1校验和sha1,在lua_scripts字典中查找sha1对应的Lua脚本script。2)将原来的EVALSHA命令改写为EVAL命令,并将校验和sha1改成脚本script,至于numkeys、key、arg等参数保持不变。
  4. 传播ECALSHA命令的方法:当主服务器成功在本机执行完衣蛾EVALSHA命令之后,它将EVALSHA命令指定的SHA1校验和是否存在repl_scriptcache_dict字典来决定是向从服务器传播EVALSHA命令还是EVAL命令。
20.7 重点回顾
  • Redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足Redis在功能性、安全性等方面的需要。
  • Redis服务器专门使用一个伪客户端来执行Lua脚本中包含的Redis命令。
  • Redis使用脚本字典来保存所有被EVAL命令执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
  • EVAL命令为客户端输入的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
  • EVALSHA命令通过直接调用Lua环境中医定义的函数来执行脚本。
  • SCRIPT FLUSH命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
  • SCRIPT EXISTS命令接收一个或多个SHA1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
  • SCRIPT LOAD命令接受一个Lua脚本未参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
  • 服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送SCRIPT KILL命令让钩子停止正在执行的脚本,或者发送SHUTDOWN nosave命令来让钩子关闭整个服务器。
  • 主服务器复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制普通Redis命令一样,只要将相同的命令传播给从服务器就可以了。
  • 主服务器在复制EVALSHA命令时,必须确保所有从服务器都一斤个载入了EVALSHA命令指定的SHA1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令获得相同的脚本执行结果。

第21章 排序

Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。

21.1 SORT 命令的实现

服务器执行SORT numbers命令的详细步骤:

  1. 创建一个和numbers列表长度相同的数组,该数组的每个项都是一个redis.h/redisSortObject结构。
  2. 遍历数组,将各个数组项的obj指针分别指向numbers列表的各个项,构成obj指针和列表项之间的一对一关系。
  3. 遍历数组,将 各个obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的u.score属性里面。
  4. 根据数组项u.score属性的值,对数组进行数字值排序,排序后的数组项按u.score属性的值从小到大排列。
  5. 遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端,程序首先访问数组的索引0,返回u.score值为1.0的列表项“1”;然后访问数组的索引1,返回u.score值为2.0的列表项“2”;最后访问数组的索引2,返回u.score值为3.0的列表项“3”。

以下是redisSortObject结构的完整的定义:

typedef struct _redisSortObject {
	// 被排序键的值
	robj *obj;
	// 权重
	union {
		// 排序数字值时使用
		double score;
		// 排序带有BY选项的字符串值时使用
		robj *cmpobj;
	} u;
} redisSortObject;

SORT命令为每个被排序的键都创建一个与键长度相同的数组,数组的每个项都是一个RedisSortObject结构,根据SORT命令使用的选项不同,程序使用RedisSortObject结构的方式也不同。

21.2 ALPHA选项的实现

通过使用ALPHA选项,SORT命令可以对包含字符串值的键进行排序:

SORT <key> ALPHA

服务器执行SORT fruits ALPHA命令的详细步骤:

  1. 创建一个RedisSortObject结构数组,数组的长度等于fruits集合的大小。
  2. 遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素。
  3. 根据obj指针所指向的集合元素,对数组进行字符串排序,排序后的数组项按集合元素的字符串值从小到大排序。
  4. 遍历数组,依次将数组项的obj指针所指向的元素返回给客户端。
21.3 ASC选项和DESC选项的实现

在默认情况下,SORT命令执行升级排序,排序后的结果按值的大小从小到大排序,
升序排序和降序排序都由相同的快速排序算法执行,不同之处在于:

  • 在执行升序排序时,排序算法使用的对比函数产生升序对比结果
  • 而在执行降序排序时,排序算法所使用的对比函数产生降序对比结果。
21.4 BY选项的实现

在默认情况下,SORT命令使用被排序键包含的元素作为排序的权重,元素本身决定了元素的排序之后所处的位置。
另一方面,通过使用BY选项,SORT命令可以指定某些字符串键,或者某个哈希键所包含的某些域(field)来作为元素的权重,对一个键进行排序。
服务器执行SORT fruits BY *-price命令的详细步骤:

  1. 创建一个RedisSortObject结构数组,数组的长度等于fruits集合的大小。
  2. 遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素。
  3. 遍历数组,根据各个数组项的obj指针所指向的集合元素,以及BY选项所给定的模式*-price,查找相应的权重键。
  4. 将各个权重键的值转成一个double类型的浮点数,然后保存在相应数组项的u.score属性里面。
  5. 以数组项u.score属性的值为权重,对数组进行排序,得到一个按u.score属性的值从小到大排序的数组。
21.5 带有ALPHA选项的BY选项的实现

BY选项默认假设权重键保存的值为数字值,如果权重键保存的事字符串值的话,那么就需要在使用BY选项的同时,配合使用ALPHA选项。
服务器执行SORT fruits BY *-id ALPHA命令的详细步骤:

  1. 创建一个RedisSortObject结构数组,数组的长度等于fruits集合的大小
  2. 遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素
  3. 遍历数组,根据各个数组项的obj指针所指向的集合元素,以及BY选项所给定的模式*-id,查询找相应的权重键
  4. 将各个数组项的u.cmpobj指针分别指向相应的权重键(一个字符串对象)
  5. 以各个数组项的权重键,对数组执行字符串排序
  6. 遍历数组,一次将数组项obj指针所指向的集合元素返回客户端。
21.6 LIMIT选项的实现

在默认情况下,SORT命令总会将排序后的所有元素都返回给客户端
通过LIMIT选项,我们可以让SORT命令只返回其中一部分已排序的元素。

LIMIT <offset> <count>

offset 参数表示要跳过的已排序元素数量
count 参数表示跳过给定数量的已排序元素之后,要返回的已排序元素数量。
服务器执行SORT alphabet ALPHA LIMIT 0 4详细步骤:

  1. 创建一个RedisSortObject结构数组,数组的长度等于alphabet集合的大小。
  2. 变量数组,将各个数组项的obj指针分别指向alphabet集合的各个元素。
  3. 根据obj指针所指向的集合元素,对数组进行字符串排序,排序后的数组。
  4. 根据选项LIMIT 0 4,将指针移动到数组的索引0上面,然后依次访问array[0]、array[1]、array[2]、array[3]这4个数组项,并将数组的obj指针所指向的元素返回给客户端。
21.7 GET选项的实现

默认情况下,SORT命令在对键进行排序之后,总是返回被排序键本身所包含的元素。
通过使用GET选项,我们可以让SORT命令对键进行排序之后,根据被排序的元素,以及GET选项所执行的模式,查找并返回某些键的值。
服务器执行SORT students ALPHA GET *-name命令的详细步骤:

  1. 创建一个RedisSortObject结构数组,数组的长度等于students集合的大小。
  2. 遍历数组,将各个数组项的obj指针分别指向students集合的各个元素
  3. 根据obj指针所指向的集合元素,对数组进行字符串排序。
  4. 遍历数组,根据数组项obj指针所指向的集合元素,以及GET选项所给定的*-name模式,查找相应的键
  5. 遍历查找程序返回的三个键,并向客户端返回他们的值。

一个SORT命令可以带多个GET选项,所以随着GET选项的曾都,命令要执行的查找操作也会增多。

21.8 STORE选项的实现

在默认情况下,SORT命令只向客户端返回排序结果,而不保存排序结果
通过使用STORE选项,我们可以将排序结果保存在指定的键里面,并在有需要时重用这个排序结果。
服务器执行SORT students ALPHA STORE sorted_students命令的详细步骤:

  1. 创建一个RedisSortObject结果数组,数组的长度等于students集合的大小
  2. 遍历数组,将各个数组项的obj指针分别指向students集合的各个元素。
  3. 根据obj指针所指向的集合元素,对数组进行字符串排序
  4. 检查sorted_students键是否存在,如果存在的话,那么删除该键。
  5. 设置sorted_students为空白的列表键。
  6. 遍历数组,将排序后的三个元素依次推入sorted_students列表的末尾。
  7. 遍历数组,向客户端返回三个元素。

SORT命令在执行其他带有STORE选项的排序操作时,执行的步骤也和这里给出的步骤类似。

21.9 多个选项的执行顺序
21.9.1 选项的执行顺序

如果按照选项来划分的话,一个SORT命令的执行过程可以分为以下四步:

  1. 排序: 在这一步,命令会使用ALPHA、ASC或DESC、BY这几个选项,对输入键进行排序,并得到一个排序结果集。
  2. 限制排序结果集的长度: 在这一步,命令会使用LIMIT选项,对排序结果集的长度进行限制,只有LIMIT选项指定的那部分元素会被保留在排序结果中。
  3. 获取外部键: 在这一步,命令会使GET选项,根据排序结果集中的元素,以及GET选项指定的模式,查找并获取指定键的值,并用这些值来作为新的排序结果集。
  4. 保存排序结果集: 在这一步,命令会使用STORE选项,将排序结果集保存到指定的键上面去。
  5. 向客户端返回排序结果集: 在最后一步,命令遍历排序结果集,并依次向客户端返回排序结果集中的元素。
SORT <key> ALPHA DESC BY <by-pattern> LIMIT <offset> <count> GET <get-pattern> STORE <store_key>
首先执行:
SORT <key> ALPHA DESC BY <by-pattern>
接着执行:
LIMIT <offset> <count>
然后执行:
GET <get-pattern>
之后执行:
STORE <store_key>
最后,命令遍历排序结果集,将结果中的元素一次返回给客户端。
21.9.2 选项的摆放顺序

调用SORT命令时,除了GET选项之外,改变选项的摆放顺序并不会影响SORT命令执行选项的顺序。
不过,如果命令包含了多个GET选项,那么在调整选项的位置时,我们必须保证多个GET选项的摆放顺序不变,这才可以让排序结果集保持不变。

21.10 重点回顾
  • SORT命令通过将被排序键包含的元素载入到数组里面,然后对数组进行排序来完成对键进行排序的工作。
  • 在默认情况下,SORT命令假设被排序键包含的都是数字值,并且以数字值的方式来进行排序。
  • 如果SORT命令使用了ALPHA选项,那么SORT命令假设被排序键包含的都是字符串值,并且以字符串的方式来进行排序。
  • SORT命令的排序操作由快速排序算法实现。
  • SORT命令会根据用户是否使用了DESC选项来决定是使用升序对比还是降序对比来比较被排序的元素,升序对比会产生升序排序结果,被排序的元素按值的大小从小到大排序,降序对比会产生降序结果,被排序的元素按值的大小从大到小排序。
  • 当SORT命令使用了BY选项时,命令使用其他键的值作为权重来进行排序操作。
  • 当SORT命令使用了LIMIT选项时,命令值保留排序结果集中LIMIT选项指定的元素。
  • 当SORT命令使用了GET选项时,命令会根据排序结果集中的元素,以及GET选项给定的模式,查找并返回其他键的值,而不是返回被排序的元素。
  • 当SORT命令使用了STROE选项时,命令会将排序结果集保存在指定的键里面。
  • 当SORT命令同时使用多个选项时,命令先执行排序操作(可用的选项为ALPHA、ASC或DESC、BY),然后执行LIMIT选项,之后执行GET选项,再之后执行STORE选项,最后才将排序结果集返回给客户端。
  • 处理GET选项之外,调整选项的摆放位置不会影响SORT命令的排序结果。

第22章 二进制位数组

Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个命令用于粗粒二进制位数组(bit array,又称“位数组”)。
SETBIT命令用于为位数组指定偏移量上的二进制位设置值,位数组的偏移量从0开始计数,而二进制位的值则可以是0或者1。
GETBIT命令则用于获取位数组指定偏移量上的二进制位的值
BITCOUNT命令用于统计位数组里面,值为1的二进制位的数量。
BITOP命令既可以对多个数组进行按为与(and)、按位或(or)、按位异或(xor)运算。

22.1 位数组的表示

Redis使用字符串对象来表示位数组,因为字符串对象使用的SDS数据结构是二进制安全的,所以程序可以直接使用SDS结构来保存位数组,并使用SDS结果的操作函数来处理位数组。

22.2 GETBIT命令的实现

GETBIT命令用于返回为数组bitarray在offset偏移量上的二进制位的值:

GETBIT <bitarray> <offset>

GETBIT命令的执行过程

  1. 计算byte=offset/8,byte值记录了offset偏移量指定的二进制位保存在位数组的哪个字节
  2. 计算bit=(offset mod 8)+1,bit值记录了offset偏移量指定的二进制位的byte字节的第几个二进制位。
  3. 根据byte值和bit值,在位数组bitarray中定位offset偏移量指定的二进制位,并返回这个位的值。

因为GETBIT命令执行的所有操作都可以再常数时间内完成,所以该命令的算法复杂度为O(1)。

22.3 SETBIT命令的实现

SETBIT用于将位数组bitarray在offset偏移量上的二进制位的值设置为value,并向客户端返回二进制位被设置之前的旧值:

SETBIT <bitarray> <offset> <value>
  1. 计算len=offset/8 +1,len值记录了保存offset偏移量指定的二进制位至少需要多少字节。
  2. 检查bitarray键保存的位数组(也即是SDS)的长度是否小于len,如果是的话,将SDS的长度扩展为len字节,并将所有新扩展空间的二进制位的值设置为0。
  3. 计算byte=offset/8,byte值记录了offset偏移量指定的二进制位保存在为数组的哪个字节。
  4. 计算bit=(offset mod 8)+1,bit值记录了offset偏移量指定的二进制位的byte字节的第几个二进制位。
  5. 根据byte值和bit值,在bitarray键保存的位数组中定位offset偏移量指定的二进制位,首先将指定二进制位现在值保存在oldvalue变量,然后将新值value设置为这个二进制的值。
  6. 想客户端返回oldvalue变量的值

因为SETBIT命令执行的所有操作都可以再常数时间内完成,所以该命令的市价复杂度为O(1)。

22.3.1 SETBIT命令的执行示例
22.3.2 带扩展操作的SETBIT命令示例
22.4 BITCOUNT命令的实现

BITCOUNT命令用于统计给定位数组中,值为1的二进制的数量。

22.4.1 二进制位统计算法(1):遍历算法

是最简单的方法。但是效率非常低,因为这个算法在每次循环中只能检查一个二进制位的值是否为1,所以检查操作执行的次数将与位数组包含的二进制位的数量成正比。

22.4.2 二进制位统计算法(2):查表算法

根据以下原理:
1)对于一个有限集合来说,集合元素的排列方式是有限的。
2)而对于一个有限成都的位数组来说,它能表示的二进制位排列也是有限的。
我们可以创建一个表,表的键为某种排列的位数组,而表的值则是相应位数组中,值为1的二进制位的数量。
与之前的遍历算法,效率提升了8倍。
查表法的实际效果会受到内存和缓存两方面的因素的限制。

22.4.3 二进制位统计算法(3):variable-percision SWAR 算法

BITCOUNT命令要解决的问题——统计一个位数组中非0二进制位的数量,在数学上被称为“计算汉明重量”。
该算法通过一系列位移和位运算操作,可以再常数时间内计算多个字节的汉明重量,并且不需要使用任何额外的内存。

22.4.4 二进制位统计算法(4):Redis的实现

BITCOUNT命令的实现用到了查表和variable-precisionSWAR两种算法:

  • 查表算法使用键长为8位的表,表中记录了从0000 0000到1111 1111在内的所以二进制位的汉明重量。
  • 至于variable-precision SWAR算法方面,BITCOUNT命令在每次循环中载入128个二进制位,然后调用四次32位variable-precision SWAR算法来计算着128个二进制位的汉明重量。

在执行BITCOUNT命令时,程序会根据未处理的二进制位数量来决定使用哪种算法:

  • 如果未处理的二进制位的数量大于等于128位,那么程序使用variable-precision SWAR算法来计算二进制位的汉明重量。
  • 如果未处理的二进制位的数量小于128位,那么程序使用查表算法来计算二进制位的汉明重量。
  • 这个BITCOUNT实现的算法复杂度为O(n),其中n为输入二进制位的数量。
22.5 BITOP命令的实现

因为C语言直接支持对字节执行逻辑与、或、异或和非操作,所以BITOP命令的AND、OR、XOR和NOT四个操作都是直接基于这些逻辑操作实现的的。

22.6 重点回顾
  • Redis使用SDS来保存为数组
  • SDS使用逆序来保存位数组,这种保存顺序简化了SETBIT命令的实现,是的SETBIT命令可以砸不移动现有二进制的情况下,对于数组进行空间扩展。
  • BITCOUNT命令使用了查表算法和variable-precison SWAR算法来优化命令的执行效率。
  • BITOP 命令的所有操作都使用C语言内置的位操作来实现。

第23章 慢查询日志

Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度。
服务器配置有两个和慢查询日志相关的选项:

  • slowlog-log-slower-than选项指定时间超过多少微秒的命令请求会被记录到日志上。
  • slowlog-max-len选项指定服务器最多保存多少条慢查询日志。

先进先出的方式保存。
使用

// 设置
CONFIG SET slowlog-log-slower-than 0
CONFIG SET slowlog-max-len 5
// 查看
SLOWLOG GET
23.1 慢查询记录的保存
struct redisServer {
	// 下一条慢查询日志的ID
	long long slowlog_entry_id;
	// 保存了所有慢查询日志的链表
	list *slowlog;
	// 服务器配置slowlog-log-slower-than选项的值
	long long slowlog_log_slower_than;
	// 服务器配置slowlog-max_len选项的值
	unsigned long slowlog_max_len;
}

slowlog_entry_id,值为0会成为第一条慢查询日志的ID,而之后服务器会对这个属性的值增一;当服务器再创建新的慢查询日志的时候,slowlog_entry_id的值1就会成为第二条慢查询日志的ID,然后服务器再次对这个属性的值增一。
slowlog,链表保存了服务器的所有慢查询日志,链表中的每个节点都保存了一个slowlogEntry结果,每个slowlogEntry结构代表一条慢查询日志。

typedef struct slowlogEntry {
	// 唯一标识符
	long long id;
	// 命令执行时的时间,格式为UNIX时间戳
	time_t time;
	// 执行命令消耗的时间,以微妙为单位
	long long duration;
	// 命令与命令参数
	robj **argv;
	// 命令与命令参数的数量
	int argc;
} slowlogEntry;
23.2 慢查询日志的阅览和删除
SLOWLOG GET
SLOWLOG LEN
SLOWLOG RESET
23.3 添加新日志

在每次执行命令的之前和之后,程序都会记录微妙格式的当前UNIX时间戳,这两个时间戳之间的差就是服务器执行命令所耗费的时长,服务器会将这个时长作为参数之一传给slowlogPushEntryIfNeeded函数,而slowlogPushEntryIfNeeded函数则负责检查是否需要为这次执行的命令创建慢查询日志。
slowlogPushEntryIfNeeded函数,作用:

  • 检查命令的执行时长是否超过slowlog-log-slower-than选项所设置的时间,如果是的,就为命令创建一个新的日志,并将新日志添加到slowlog链表的表头。
  • 检查慢查询日志的长度是否超过slowlog-max-len选项所设置的长度,如果是的话,那么将多出来的日志从slowlog链表中删除掉。
23.4 重点回顾
  • Redis的慢查询日志功能用于记录执行时间超过指定时长的命令
  • Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,每个slowlogEntry结果代表一条慢查询
  • 打印和删除慢查询日志可以通过遍历slowlog链表来完成。
  • slowlog链表的长度就是服务器所保存慢查询日志的数量。
  • 新的慢查询日志会被添加到slowlog链表的表头,如果日志的数量超过slowlog-max-len选项的值,那么多出来的日志会被删除

第24章 监视器

通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前的处理的命令请求的相关信息。
每当一个客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将关于这条命令请求的信息发送给所有监视器。

24.1 成为监视器

发送MONITOR命令可以让一个普通客户端变为一个监视器,该命令实现原理是向服务器发送MONITOR命令,那么这个客户端的REDIS_MONITOR标志会被打开,并且这个客户端本身会被添加到monitor链表的表尾。

伪代码
def MONITOR():
	# 打开客户端的监视器标志
	client.flags != REDIS_MONITOR
	# 将客户端添加到服务器状态的monitors链表的末尾
	server.monitors.append(client)
	# 向客户端返回OK
	send_reply("OK")
24.2 向监视器发送命令信息

服务器在每次处理命令请求之前,都会调用replicationFeedMonitors函数,由这个函数将被处理的命令请求的相关信息发送给各个监视器。

24.3 重点回顾
  • 客户端可以通过执行MONITOR命令,将客户端转换成监视器,接收并打印服务器处理的每个命令请求的相关信息。
  • 当一个客户端从普通客户端变为监视器时,该客户端的REDIS_MONITOR标识会被打开。
  • 每次处理命令请求时,服务器都会遍历monitors链表,将相关信息发送给监视器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值