源码、相关资源和勘误

最后更新于:2022-04-01 21:38:39

## 注释源码 为了帮助有需要的读者进一步了解 Redis 的实现细节, 本书附带了一份包含详细中文注释的 Redis 3.0 版本源码可供参考:[https://github.com/huangz1990/redis-3.0-annotated](https://github.com/huangz1990/redis-3.0-annotated) 。 ## 相关资源 [《如何阅读 Redis 源码》](http://blog.huangz.me/diary/2014/how-to-read-redis-source-code.html) —— 文章给出了一个推荐的 Redis 源码阅读顺序以供参考, 读者可以在阅读完本书之后, 根据文章描述的顺序来尝试阅读源码, 从而进一步提高对 Redis 的了解。 [《Redis 设计与实现》图片集](http://1e-gallery.redisbook.com/) —— 展示了本书包含的绝大多数图片以及图片的源码, 方便读者在写博客、记笔记或者做演讲稿时引用本书的图片, 或者通过阅读图片的源码来学习 dot 语言和 Graphviz 图片生成工具。 [《Redis 多机特性工作原理简介》](http://www.chinahadoop.cn/course/31) —— 这个课程对 Redis 的复制、Sentinel 和集群三个特性的工作原理进行了基本的介绍。 因为课程的内容都提取自本书的《复制》、《Sentinel》和《集群》三个章节, 所以可以把这个课程看作是这三个章节的简介版本。 [旧版《Redis 设计与实现》](http://origin.redisbook.com/) —— 本书的上一版, 介绍了 Redis 2.6 的内部运作机制和单机功能。 要了解本书和旧版之间的区别, 请阅读 [*《Redis 设计与实现》新旧版本详细对比*](http://redisbook.com/different.html) 页面。 ## 勘误 [*勘误信息*](http://redisbook.com/errata/index.html) 页面列出了本书已确认的勘误信息, 请读者在阅读本书之前, 根据这些信息对书本进行校正, 由此带来的不便作者深感抱歉。 如果读者发现了勘误页面目前尚未记录的新错误, 可以在本页面的 disqus 论坛进行反馈, 又或者通过 [huangz.me](http://huangz.me/) 页面展示的任意一种联系方式来联系作者。
';

重点回顾

最后更新于:2022-04-01 21:38:37

* 客户端可以通过执行 MONITOR 命令, 将客户端转换成监视器, 接收并打印服务器处理的每个命令请求的相关信息。 * 当一个客户端从普通客户端变为监视器时, 该客户端的 `REDIS_MONITOR` 标识会被打开。 * 服务器将所有监视器都记录在 `monitors` 链表中。 * 每次处理命令请求时, 服务器都会遍历 `monitors` 链表, 将相关信息发送给监视器。
';

向监视器发送命令信息

最后更新于:2022-04-01 21:38:34

服务器在每次处理命令请求之前, 都会调用 `replicationFeedMonitors` 函数, 由这个函数将被处理命令请求的相关信息发送给各个监视器。 以下是 `replicationFeedMonitors` 函数的伪代码定义, 函数首先根据传入的参数创建信息, 然后将信息发送给所有监视器: ~~~ def replicationFeedMonitors(client, monitors, dbid, argv, argc): # 根据执行命令的客户端、当前数据库的号码、命令参数、命令参数个数等参数 # 创建要发送给各个监视器的信息 msg = create_message(client, dbid, argv, argc) # 遍历所有监视器 for monitor in monitors: # 将信息发送给监视器 send_message(monitor, msg) ~~~ 举个例子, 假设服务器在时间 `1378822257.329412` , 根据 IP 为 `127.0.0.1` 、端口号为 `56604` 的客户端发送的命令请求, 对 `0` 号数据库执行命令 `KEYS *` , 那么服务器将创建以下信息: ~~~ 1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" ~~~ 如果服务器 `monitors` 链表的当前状态如图 24-3 所示, 那么服务器会分别将信息发送给 `c128` 、 `c256` 、 `c512` 和 `c10086` 四个监视器, 如图 24-4 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52db1d1d4f.png)
';

成为监视器

最后更新于:2022-04-01 21:38:32

发送 MONITOR 命令可以让一个普通客户端变为一个监视器, 该命令的实现原理可以用以下伪代码来实现: ~~~ def MONITOR(): # 打开客户端的监视器标志 client.flags |= REDIS_MONITOR # 将客户端添加到服务器状态的 monitors 链表的末尾 server.monitors.append(client) # 向客户端返回 OK send_reply("OK") ~~~ 举个例子, 如果客户端 `c10086` 向服务器发送 MONITOR 命令, 那么这个客户端的 `REDIS_MONITOR` 标志会被打开, 并且这个客户端本身会被添加到 `monitors` 链表的表尾。 假设客户端 `c10086` 发送 MONITOR 命令之前, `monitors` 链表的状态如图 24-2 所示, 那么在服务器执行客户端 `c10086` 发送的 MONITOR 命令之后, `monitors` 链表将被更新为图 24-3 所示的状态。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d874bd3f.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d932e37f.png)
';

监视器

最后更新于:2022-04-01 21:38:30

# 监视器 通过执行 MONITOR 命令, 客户端可以将自己变为一个监视器, 实时地接收并打印出服务器当前处理的命令请求的相关信息: ~~~ redis> MONITOR OK 1378822099.421623 [0 127.0.0.1:56604] "PING" 1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world" 1378822109.036925 [0 127.0.0.1:56604] "SET" "number" "123" 1378822140.649496 [0 127.0.0.1:56604] "SADD" "fruits" "Apple" "Banana" "Cherry" 1378822154.117160 [0 127.0.0.1:56604] "EXPIRE" "msg" "10086" 1378822257.329412 [0 127.0.0.1:56604] "KEYS" "*" 1378822258.690131 [0 127.0.0.1:56604] "DBSIZE" ~~~ 每当一个客户端向服务器发送一条命令请求时, 服务器除了会处理这条命令请求之外, 还会将关于这条命令请求的信息发送给所有监视器, 如图 24-1 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d5a670d7.png)
';

重点回顾

最后更新于:2022-04-01 21:38:27

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

添加新日志

最后更新于:2022-04-01 21:38:25

## 添加新日志 在每次执行命令的之前和之后, 程序都会记录微秒格式的当前 UNIX 时间戳, 这两个时间戳之间的差就是服务器执行命令所耗费的时长, 服务器会将这个时长作为参数之一传给 `slowlogPushEntryIfNeeded` 函数, 而 `slowlogPushEntryIfNeeded` 函数则负责检查是否需要为这次执行的命令创建慢查询日志, 以下伪代码展示了这一过程: ~~~ # 记录执行命令前的时间 before = unixtime_now_in_us() # 执行命令 execute_command(argv, argc, client) # 记录执行命令后的时间 after = unixtime_now_in_us() # 检查是否需要创建新的慢查询日志 slowlogPushEntryIfNeeded(argv, argc, before-after) ~~~ `slowlogPushEntryIfNeeded` 函数的作用有两个: 1. 检查命令的执行时长是否超过 `slowlog-log-slower-than` 选项所设置的时间, 如果是的话, 就为命令创建一个新的日志, 并将新日志添加到 `slowlog` 链表的表头。 2. 检查慢查询日志的长度是否超过 `slowlog-max-len` 选项所设置的长度, 如果是的话, 那么将多出来的日志从 `slowlog` 链表中删除掉。 以下是 `slowlogPushEntryIfNeeded` 函数的实现代码: ~~~ void slowlogPushEntryIfNeeded(robj **argv, int argc, long long duration) { // 慢查询功能未开启,直接返回 if (server.slowlog_log_slower_than < 0) return; // 如果执行时间超过服务器设置的上限,那么将命令添加到慢查询日志 if (duration >= server.slowlog_log_slower_than) // 新日志添加到链表表头 listAddNodeHead(server.slowlog,slowlogCreateEntry(argv,argc,duration)); // 如果日志数量过多,那么进行删除 while (listLength(server.slowlog) > server.slowlog_max_len) listDelNode(server.slowlog,listLast(server.slowlog)); } ~~~ 函数中的大部分代码我们已经介绍过了, 唯一需要说明的是 `slowlogCreateEntry` 函数: 该函数根据传入的参数, 创建一个新的慢查询日志, 并将 `redisServer.slowlog_entry_id` 的值增一。 举个例子, 假设服务器当前保存的慢查询日志如图 23-2 所示, 如果我们执行以下命令: ~~~ redis> EXPIRE msg 10086 (integer) 1 ~~~ 服务器在执行完这个 EXPIRE 命令之后, 就会调用 `slowlogPushEntryIfNeeded` 函数, 函数将为 EXPIRE 命令创建一条 `id` 为 `6` 的慢查询日志, 并将这条新日志添加到 `slowlog` 链表的表头, 如图 23-3 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d3dd6b30.png) 注意, 除了 `slowlog` 链表发生了变化之外, `slowlog_entry_id` 的值也从 `6` 变为 `7` 了。 之后, `slowlogPushEntryIfNeeded` 函数发现, 服务器设定的最大慢查询日志数目为 `5` 条, 而服务器目前保存的慢查询日志数目为 `6` 条, 于是服务器将 `id` 为 `1` 的慢查询日志删除, 让服务器的慢查询日志数量回到设定好的 `5` 条。 删除操作执行之后的服务器状态如图 23-4 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d3fa0c16.png)
';

慢查询日志的阅览和删除

最后更新于:2022-04-01 21:38:23

## 慢查询日志的阅览和删除 弄清楚了服务器状态的 `slowlog` 链表的作用之后, 我们可以用以下伪代码来定义查看日志的 SLOWLOG GET 命令: ~~~ def SLOWLOG_GET(number=None): # 用户没有给定 number 参数 # 那么打印服务器包含的全部慢查询日志 if number is None: number = SLOWLOG_LEN() # 遍历服务器中的慢查询日志 for log in redisServer.slowlog: if number <= 0: # 打印的日志数量已经足够,跳出循环 break else: # 继续打印,将计数器的值减一 number -= 1 # 打印日志 printLog(log) ~~~ 查看日志数量的 SLOWLOG LEN 命令可以用以下伪代码来定义: ~~~ def SLOWLOG_LEN(): # slowlog 链表的长度就是慢查询日志的条目数量 return len(redisServer.slowlog) ~~~ 另外, 用于清除所有慢查询日志的 SLOWLOG RESET 命令可以用以下伪代码来定义: ~~~ def SLOWLOG_RESET(): # 遍历服务器中的所有慢查询日志 for log in redisServer.slowlog: # 删除日志 deleteLog(log) ~~~
';

慢查询记录的保存

最后更新于:2022-04-01 21:38:21

# 慢查询记录的保存 服务器状态中包含了几个和慢查询日志功能有关的属性: ~~~ 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` 的值 `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; ~~~ 举个例子, 对于以下慢查询日志来说: ~~~ 1) (integer) 3 2) (integer) 1378781439 3) (integer) 10 4) 1) "SET" 2) "number" 3) "10086" ~~~ 图 23-1 展示的就是该日志所对应的 `slowlogEntry` 结构。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d3ac0b55.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52d3c2ff21.png) 图 23-2 展示了服务器状态中, 和慢查询功能有关的属性: * `slowlog_entry_id` 的值为 `6` , 表示服务器下条慢查询日志的 `id` 值将为 `6` 。 * `slowlog` 链表包含了 `id` 为 `5` 至 `1` 的慢查询日志, 最新的 `5` 号日志排在链表的表头, 而最旧的 `1` 号日志排在链表的表尾, 这表明`slowlog` 链表是使用插入到表头的方式来添加新日志的。 * `slowlog_log_slower_than` 记录了服务器配置 `slowlog-log-slower-than` 选项的值 `0` , 表示任何执行时间超过 `0` 微秒的命令都会被慢查询日志记录。 * `slowlog-max-len` 属性记录了服务器配置 `slowlog-max-len` 选项的值 `5` , 表示服务器最多储存五条慢查询日志。 > 注意 > 因为版面空间不足的缘故, 所以图 23-2 展示的各个 `slowlogEntry` 结构都省略了 `argv` 数组。
';

慢查询日志

最后更新于:2022-04-01 21:38:18

# 慢查询日志 Redis 的慢查询日志功能用于记录执行时间超过给定时长的命令请求, 用户可以通过这个功能产生的日志来监视和优化查询速度。 服务器配置有两个和慢查询日志相关的选项: * `slowlog-log-slower-than` 选项指定执行时间超过多少微秒(`1` 秒等于 `1,000,000` 微秒)的命令请求会被记录到日志上。 举个例子, 如果这个选项的值为 `100` , 那么执行时间超过 `100` 微秒的命令就会被记录到慢查询日志; 如果这个选项的值为 `500` , 那么执行时间超过 `500` 微秒的命令就会被记录到慢查询日志; 诸如此类。 * `slowlog-max-len` 选项指定服务器最多保存多少条慢查询日志。 服务器使用先进先出的方式保存多条慢查询日志: 当服务器储存的慢查询日志数量等于 `slowlog-max-len` 选项的值时, 服务器在添加一条新的慢查询日志之前, 会先将最旧的一条慢查询日志删除。 举个例子, 如果服务器 `slowlog-max-len` 的值为 `100` , 并且假设服务器已经储存了 `100` 条慢查询日志, 那么如果服务器打算添加一条新日志的话, 它就必须先删除目前保存的最旧的那条日志, 然后再添加新日志。 让我们来看一个慢查询日志功能的例子, 首先用 CONFIG_SET 命令将 `slowlog-log-slower-than` 选项的值设为 `0` 微秒, 这样 Redis 服务器执行的任何命令都会被记录到慢查询日志中, 接着将 `slowlog-max-len` 选项的值设为 `5` , 让服务器最多只保存 `5` 条慢查询日志: ~~~ redis> CONFIG SET slowlog-log-slower-than 0 OK redis> CONFIG SET slowlog-max-len 5 OK ~~~ 接着, 我们用客户端发送几条命令请求: ~~~ redis> SET msg "hello world" OK redis> SET number 10086 OK redis> SET database "Redis" OK ~~~ 然后使用 SLOWLOG GET 命令查看服务器所保存的慢查询日志: ~~~ redis> SLOWLOG GET 1) 1) (integer) 4 # 日志的唯一标识符(uid) 2) (integer) 1378781447 # 命令执行时的 UNIX 时间戳 3) (integer) 13 # 命令执行的时长,以微秒计算 4) 1) "SET" # 命令以及命令参数 2) "database" 3) "Redis" 2) 1) (integer) 3 2) (integer) 1378781439 3) (integer) 10 4) 1) "SET" 2) "number" 3) "10086" 3) 1) (integer) 2 2) (integer) 1378781436 3) (integer) 18 4) 1) "SET" 2) "msg" 3) "hello world" 4) 1) (integer) 1 2) (integer) 1378781425 3) (integer) 11 4) 1) "CONFIG" 2) "SET" 3) "slowlog-max-len" 4) "5" 5) 1) (integer) 0 2) (integer) 1378781415 3) (integer) 53 4) 1) "CONFIG" 2) "SET" 3) "slowlog-log-slower-than" 4) "0" ~~~ 如果这时再执行一条 SLOWLOG GET 命令, 那么我们将看到, 上一次执行的 SLOWLOG GET 命令已经被记录到了慢查询日志中, 而最旧的、编号为 `0` 的慢查询日志已经被删除, 服务器的慢查询日志数量仍然为 `5` 条: ~~~ redis> SLOWLOG GET 1) 1) (integer) 5 2) (integer) 1378781521 3) (integer) 61 4) 1) "SLOWLOG" 2) "GET" 2) 1) (integer) 4 2) (integer) 1378781447 3) (integer) 13 4) 1) "SET" 2) "database" 3) "Redis" 3) 1) (integer) 3 2) (integer) 1378781439 3) (integer) 10 4) 1) "SET" 2) "number" 3) "10086" 4) 1) (integer) 2 2) (integer) 1378781436 3) (integer) 18 4) 1) "SET" 2) "msg" 3) "hello world" 5) 1) (integer) 1 2) (integer) 1378781425 3) (integer) 11 4) 1) "CONFIG" 2) "SET" 3) "slowlog-max-len" 4) "5" ~~~
';

重点回顾

最后更新于:2022-04-01 21:38:16

* Redis 使用 SDS 来保存位数组。 * SDS 使用逆序来保存位数组, 这种保存顺序简化了 SETBIT 命令的实现, 使得 SETBIT 命令可以在不移动现有二进制位的情况下, 对位数组进行空间扩展。 * BITCOUNT 命令使用了查表算法和 variable-precision SWAR 算法来优化命令的执行效率。 * BITOP 命令的所有操作都使用 C 语言内置的位操作来实现。
';

GETBIT 命令的实现

最后更新于:2022-04-01 21:38:14

GETBIT 命令用于返回位数组 `bitarray` 在 `offset` 偏移量上的二进制位的值: ~~~ GETBIT ~~~ GETBIT 命令的执行过程如下: 1. 计算 ![byte = \lfloor offset \div 8 \rfloor](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cbfae0a1.png) , `byte` 值记录了 `offset` 偏移量指定的二进制位保存在位数组的哪个字节。 2. 计算 ![bit = (offset \bmod 8) + 1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cc84080d.png) , `bit` 值记录了 `offset` 偏移量指定的二进制位是 `byte` 字节的第几个二进制位。 3. 根据 `byte` 值和 `bit` 值, 在位数组 `bitarray` 中定位 `offset` 偏移量指定的二进制位, 并返回这个位的值。 举个例子, 对于图 IMAGE_BIT_EXAMPLE 所示的位数组来说, 命令: ~~~ GETBIT 3 ~~~ 将执行以下操作: 1. ![\lfloor 3 \div 8 \rfloor](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cceae34a.png) 的值为 `0` 。 2. ![(3 \bmod 8) + 1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52ccf865f5.png) 的值为 `4` 。 3. 定位到 `buf[0]` 字节上面, 然后取出该字节上的第 `4` 个二进制位(从左向右数)的值。 4. 向客户端返回二进制位的值 `1` 。 命令的执行过程如图 IMAGE_SEARCH_EXAMPLE 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cd08c209.png) 再举一个例子, 对于图 IMAGE_ANOTHER_BIT_EXAMPLE 所示的位数组来说, 命令: ~~~ GETBIT 10 ~~~ 将执行以下操作: 1. ![\lfloor 10 \div 8 \rfloor](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cd26b00a.png) 的值为 `1` 。 2. ![(10 \bmod 8) + 1](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cd32bb39.png) 的值为 `3` 。 3. 定位到 `buf[1]` 字节上面, 然后取出该字节上的第 `3` 个二进制位的值。 4. 向客户端返回二进制位的值 `0` 。 命令的执行过程如图 IMAGE_ANOTHER_SEARCH_EXAMPLE 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cd430f13.png) 因为 GETBIT 命令执行的所有操作都可以在常数时间内完成, 所以该命令的算法复杂度为 ![O(1)](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52cd565550.png) 。
';

二进制位数组

最后更新于:2022-04-01 21:38:11

';

重点回顾

最后更新于:2022-04-01 21:38:09

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

SORT <key> 命令的实现

最后更新于:2022-04-01 21:38:07

SORT 命令的最简单执行形式为: ~~~ SORT ~~~ 这个命令可以对一个包含数字值的键 `key` 进行排序。 以下示例展示了如何使用 SORT 命令对一个包含三个数字值的列表键进行排序: ~~~ redis> RPUSH numbers 3 1 2 (integer) 3 redis> SORT numbers 1) "1" 2) "2" 3) "3" ~~~ 服务器执行 `SORT numbers` 命令的详细步骤如下: 1. 创建一个和 `numbers` 列表长度相同的数组, 该数组的每个项都是一个 `redis.h/redisSortObject` 结构, 如图 IMAGE_CREATE_ARRAY 所示。 2. 遍历数组, 将各个数组项的 `obj` 指针分别指向 `numbers` 列表的各个项, 构成 `obj` 指针和列表项之间的一对一关系, 如图 IMAGE_POINT_OBJ 所示。 3. 遍历数组, 将各个 `obj` 指针所指向的列表项转换成一个 `double` 类型的浮点数, 并将这个浮点数保存在相应数组项的 `u.score` 属性里面, 如图 IMAGE_SET_SCORE 所示。 4. 根据数组项 `u.score` 属性的值, 对数组进行数字值排序, 排序后的数组项按 `u.score` 属性的值从小到大排列, 如图 IMAGE_SORTED 所示。 5. 遍历数组, 将各个数组项的 `obj` 指针所指向的列表项作为排序结果返回给客户端: 程序首先访问数组的索引 `0` , 返回 `u.score` 值为`1.0` 的列表项 `"1"` ; 然后访问数组的索引 `1` , 返回 `u.score` 值为 `2.0` 的列表项 `"2"` ; 最后访问数组的索引 `2` , 返回 `u.score` 值为`3.0` 的列表项 `"3"` 。 其他 `SORT ` 命令的执行步骤也和这里给出的 `SORT numbers` 命令的执行步骤类似。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52c5ee7750.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52c612e4a6.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52c6296d2b.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52c63dfd57.png) 以下是 `redisSortObject` 结构的完整定义: ~~~ typedef struct _redisSortObject { // 被排序键的值 robj *obj; // 权重 union { // 排序数字值时使用 double score; // 排序带有 BY 选项的字符串值时使用 robj *cmpobj; } u; } redisSortObject; ~~~ SORT 命令为每个被排序的键都创建一个与键长度相同的数组, 数组的每个项都是一个 `redisSortObject` 结构, 根据 SORT 命令使用的选项不同, 程序使用 `redisSortObject` 结构的方式也不同, 稍后介绍 SORT 命令的各种选项时我们会看到这一点。
';

排序

最后更新于:2022-04-01 21:38:05

';

重点回顾

最后更新于:2022-04-01 21:38:02

* 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 命令来获得相同的脚本执行效果
';

创建并修改 Lua 环境

最后更新于:2022-04-01 21:38:00

为了在 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 脚本。 接下来的各个小节将分别介绍这些步骤。 ## 创建 Lua 环境 在最开始的这一步, 服务器首先调用 Lua 的 C API 函数 `lua_open` , 创建一个新的 Lua 环境。 因为 lua_open 函数创建的只是一个基本的 Lua 环境, 为了让这个 Lua 环境可以满足 Redis 的操作要求, 接下来服务器将对这个 Lua 环境进行一系列修改。 ## 载入函数库 Redis 修改 Lua 环境的第一步, 就是将以下函数库载入到 Lua 环境里面: * 基础库(base library): 这个库包含 Lua 的核心(core)函数, 比如 `assert` 、 `error` 、 `pairs` 、 `tostring` 、 `pcall` , 等等。 另外, 为了防止用户从外部文件中引入不安全的代码, 库中的 `loadfile` 函数会被删除。 * 表格库(table library): 这个库包含用于处理表格的通用函数, 比如 `table.concat` 、 `table.insert` 、 `table.remove` 、 `table.sort`, 等等。 * 字符串库(string library): 这个库包含用于处理字符串的通用函数, 比如用于对字符串进行查找的 `string.find` 函数, 对字符串进行格式化的 `string.format` 函数, 查看字符串长度的 `string.len` 函数, 对字符串进行翻转的 `string.reverse` 函数, 等等。 * 数学库(math library): 这个库是标准 C 语言数学库的接口, 它包括计算绝对值的 `math.abs` 函数, 返回多个数中的最大值和最小值的 `math.max` 函数和 `math.min` 函数, 计算二次方根的 `math.sqrt` 函数, 计算对数的 `math.log` 函数, 等等。 * 调试库(debug library): 这个库提供了对程序进行调试所需的函数, 比如对程序设置钩子和取得钩子的 `debug.sethook` 函数和`debug.gethook` 函数, 返回给定函数相关信息的 `debug.getinfo` 函数, 为对象设置元数据的 `debug.setmetatable` 函数, 获取对象元数据的`debug.getmetatable` 函数, 等等。 * Lua CJSON 库([http://www.kyne.com.au/~mark/software/lua-cjson.php](http://www.kyne.com.au/~mark/software/lua-cjson.php)): 这个库用于处理 UTF-8 编码的 JSON 格式, 其中`cjson.decode` 函数将一个 JSON 格式的字符串转换为一个 Lua 值, 而 `cjson.encode` 函数将一个 Lua 值序列化为 JSON 格式的字符串。 * Struct 库([http://www.inf.puc-rio.br/~roberto/struct/](http://www.inf.puc-rio.br/~roberto/struct/)): 这个库用于在 Lua 值和 C 结构(struct)之间进行转换, 函数`struct.pack` 将多个 Lua 值打包成一个类结构(struct-like)字符串, 而函数 `struct.unpack` 则从一个类结构字符串中解包出多个 Lua 值。 * Lua cmsgpack 库([https://github.com/antirez/lua-cmsgpack](https://github.com/antirez/lua-cmsgpack)): 这个库用于处理 MessagePack 格式的数据, 其中 `cmsgpack.pack` 函数将 Lua 值转换为 MessagePack 数据, 而 `cmsgpack.unpack` 函数则将 MessagePack 数据转换为 Lua 值。 通过使用这些功能强大的函数库, Lua 脚本可以直接对执行 Redis 命令获得的数据进行复杂的操作。 ## 创建 `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.sha1hex` 函数。 * 用于返回错误信息的 `redis.error_reply` 函数和 `redis.status_reply` 函数。 在这些函数里面, 最常用也最重要的要数 `redis.call` 函数和 `redis.pcall` 函数 —— 通过这两个函数, 用户可以直接在 Lua 脚本中执行 Redis 命令: ~~~ redis> EVAL "return redis.call('PING')" 0 PONG ~~~ ## 使用 Redis 自制的随机函数来替换 Lua 原有的随机函数 为了保证相同的脚本可以在不同的机器上产生相同的结果, Redis 要求所有传入服务器的 Lua 脚本, 以及 Lua 环境中的所有函数, 都必须是无副作用([side effect](http://en.wikipedia.org/wiki/Side_effect_(computer_science)))的纯函数([pure function](http://en.wikipedia.org/wiki/Pure_function))。 但是, 在之前载入到 Lua 环境的 `math` 函数库中, 用于生成随机数的 `math.random` 函数和 `math.randomseed` 函数都是带有副作用的, 它们不符合 Redis 对 Lua 环境的无副作用要求。 因为这个原因, Redis 使用自制的函数替换了 `math` 库中原有的 `math.random` 函数和 `math.randomseed` 函数, 替换之后的两个函数有以下特征: * 对于相同的 seed 来说, `math.random` 总产生相同的随机数序列, 这个函数是一个纯函数。 * 除非在脚本中使用 `math.randomseed` 显式地修改 seed , 否则每次运行脚本时, Lua 环境都使用固定的 `math.randomseed(0)` 语句来初始化 seed 。 比如说, 使用以下脚本, 我们可以打印 seed 值为 `0` 时, `math.random` 对于输入 `10` 至 `1` 所产生的随机序列: 无论执行这个脚本多少次, 产生的值都是相同的: ~~~ $ redis-cli --eval random-with-default-seed.lua 1) (integer) 1 2) (integer) 2 3) (integer) 2 4) (integer) 3 5) (integer) 4 6) (integer) 4 7) (integer) 7 8) (integer) 1 9) (integer) 7 10) (integer) 2 ~~~ 但是, 如果我们在另一个脚本里面, 调用 `math.randomseed` 将 seed 修改为 `10086` : 那么这个脚本生成的随机数序列将和使用默认 seed 值 `0` 时生成的随机序列不同: ~~~ $ redis-cli --eval random-with-new-seed.lua 1) (integer) 1 2) (integer) 1 3) (integer) 2 4) (integer) 1 5) (integer) 1 6) (integer) 3 7) (integer) 1 8) (integer) 1 9) (integer) 3 10) (integer) 1 ~~~ ## 创建排序辅助函数 上一个小节说到, 为了防止带有副作用的函数令脚本产生不一致的数据, Redis 对 `math` 库的 `math.random` 函数和 `math.randomseed` 函数进行了替换。 对于 Lua 脚本来说, 另一个可能产生不一致数据的地方是那些带有不确定性质的命令。 比如对于一个集合键来说, 因为集合元素的排列是无序的, 所以即使两个集合的元素完全相同, 它们的输出结果也可能并不相同。 考虑下面这个集合例子: ~~~ redis> SADD fruit apple banana cherry (integer) 3 redis> SMEMBERS fruit 1) "cherry" 2) "banana" 3) "apple" redis> SADD another-fruit cherry banana apple (integer) 3 redis> SMEMBERS another-fruit 1) "apple" 2) "banana" 3) "cherry" ~~~ 这个例子中的 `fruit` 集合和 `another-fruit` 集合包含的元素是完全相同的, 只是因为集合添加元素的顺序不同, SMEMBERS 命令的输出就产生了不同的结果。 Redis 将 SMEMBERS 这种在相同数据集上可能会产生不同输出的命令称为“带有不确定性的命令”, 这些命令包括: * SINTER * SUNION * SDIFF * SMEMBERS * HKEYS * HVALS * KEYS 为了消除这些命令带来的不确定性, 服务器会为 Lua 环境创建一个排序辅助函数 `__redis__compare_helper` , 当 Lua 脚本执行完一个带有不确定性的命令之后, 程序会使用 `__redis__compare_helper` 作为对比函数, 自动调用 `table.sort` 函数对命令的返回值做一次排序, 以此来保证相同的数据集总是产生相同的输出。 举个例子, 如果我们在 Lua 脚本中对 `fruit` 集合和 `another-fruit` 集合执行 SMEMBERS 命令, 那么两个脚本将得出相同的结果 —— 因为脚本已经对 SMEMBERS 命令的输出进行过排序了: ~~~ redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 fruit 1) "apple" 2) "banana" 3) "cherry" redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 another-fruit 1) "apple" 2) "banana" 3) "cherry" ~~~ ## 创建 `redis.pcall` 函数的错误报告辅助函数 在这一步, 服务器将为 Lua 环境创建一个名为 `__redis__err__handler` 的错误处理函数, 当脚本调用 `redis.pcall` 函数执行 Redis 命令, 并且被执行的命令出现错误时, `__redis__err__handler` 就会打印出错代码的来源和发生错误的行数, 为程序的调试提供方便。 举个例子, 如果客户端要求服务器执行以下 Lua 脚本: 那么服务器将向客户端返回一个错误: ~~~ $ redis-cli --eval wrong-command.lua (error) @user_script: 4: Unknown Redis command called from Lua script ~~~ 其中 `@user_script` 说明这是一个用户定义的函数, 而之后的 `4` 则说明出错的代码位于 Lua 脚本的第四行。 ## 保护 Lua 的全局环境 在这一步, 服务器将对 Lua 环境中的全局环境进行保护, 确保传入服务器的脚本不会因为忘记使用 `local` 关键字而将额外的全局变量添加到了 Lua 环境里面。 因为全局变量保护的原因, 当一个脚本试图创建一个全局变量时, 服务器将报告一个错误: ~~~ redis> EVAL "x = 10" 0 (error) ERR Error running script (call to f_df1ad3745c2d2f078f0f41377a92bb6f8ac79af0): @enable_strict_lua:7: user_script:1: Script attempted to create global variable 'x' ~~~ 除此之外, 试图获取一个不存在的全局变量也会引发一个错误: ~~~ redis> EVAL "return x" 0 (error) ERR Error running script (call to f_03c387736bb5cc009ff35151572cee04677aa374): @enable_strict_lua:14: user_script:1: Script attempted to access unexisting global variable 'x' ~~~ 不过 Redis 并未禁止用户修改已存在的全局变量, 所以在执行 Lua 脚本的时候, 必须非常小心, 以免错误地修改了已存在的全局变量: ~~~ redis> EVAL "redis = 10086; return redis" 0 (integer) 10086 ~~~ ## 将 Lua 环境保存到服务器状态的 `lua` 属性里面 经过以上的一系列修改, Redis 服务器对 Lua 环境的修改工作到此就结束了, 在最后的这一步, 服务器会将 Lua 环境和服务器状态的 `lua`属性关联起来, 如图 IMAGE_REDIS_SERVER_LUA 所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-09-13_55f52c077aabd.png) 因为 Redis 使用串行化的方式来执行 Redis 命令, 所以在任何特定时间里, 最多都只会有一个脚本能够被放进 Lua 环境里面运行, 因此, 整个 Redis 服务器只需要创建一个 Lua 环境即可。
';

Lua 脚本

最后更新于:2022-04-01 21:37:58

';

重点回顾

最后更新于:2022-04-01 21:37:55

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