源码、相关资源和勘误
最后更新于: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` 时, 事务也具有耐久性
';