1、发布与订阅
Redis的发布订阅功能由PUBLISH、SUBCRIBE、PSUBCRIBE等命令组成。通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息。
1.1 频道的订阅与退订
Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端。
struct redisServer {
// 保存所有频道的订阅关系
dict *pubsub_channels;
}
当客户端订阅(SUBSCRIBE)一个频道,服务器会在pubsub_channels字典里找到对应的频道键(没有则新创建一个键),将客户端添加到列表尾部。当客户端退订(UNSUBSCRIBE)一个频道,服务器会在pubsub_channels字典里找到对应的频道键,将客户端从列表中删除。如果删除退订客户端后列表为空,说明这个频道没有订阅者了,那么Redis会删除对应的键。
1.2 模式的订阅与退订
服务器也将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面。pubsub_patterns属性是一个链表,链表中的每个节点都包含着一个pubsubPattern结构,这个结构的pattern属性记录了被订阅的模式,而client属性则记录了订阅模式的客户端。
struct redisServer {
//保存所有模式订阅关系
list *pubsub_patterns;
}
typedef struct pubsubPattern {
//订阅模式的客户端
redisClient *client;
//被订阅的模式
robj *pattern;
} pubsubPattern;
当客户端订阅(PSUBSCRIBE)某个模式时,服务器会新建一个pubsubPattern结构,将结构的pattern属性设置为被订阅的模式,client属性设置为订阅模式的客户端。并把这个结构添加到pubsub_patterns链表的表尾。当客户端退订(PUNSUBSCRIBE)某个模式时,Redis将会在pubsub_patterns链表中删除掉对应的pubsubPattern结构。
1.3 发送消息
当一个Redis客户端执行PUBLISH <channel> <message>命令时,服务器需要执行如下操作:
- 因为服务器状态中的pubsub_channels字典记录了所有频道的订阅关系,所以为了将消息发送给channel频道的所有订阅者,PUBLISH命令要做的就是在pubsub_channels字典里找到频道channel的订阅者名单(一个链表),然后将消息发送给名单上的所有客户端。
- 因为服务器状态中的pubsub_patterns链表记录了所有模式的订阅关系,所以为了将消息发送给所有与channel频道相匹配的模式的订阅者,PUBLISH命令要做的就是遍历整个pubsub_patterns链表,查找那些与channel频道相匹配的模式,并将消息发送给订阅了这些模式的客户端。
1.4 查看订阅消息
PUBSUB命令是Redis2.8新增加的命令之一,客户端可以通过这个命令来查看频道或者模式的相关信息。
- PUBSUB CHANNELS [pattern]用于返回服务器当前被订阅的频道,其中pattern参数是可选的。是通过遍历服务器pubsub_channels字典的所有键(每个键都是一个被订阅的频道),然后记录并返回所有符合条件的频道来实现的。
- PUBSUB NUMSUB [channel-1 channel-2...channel-n]命令接收任意多个频道作为输入参数,并返回这些频道的订阅者数量。是通过在pubsub_channels字典中找到频道对应的订阅者链表,然后返回订阅者链表的长度来实现的(订阅者链表的长度就是频道订阅者的数量) 。
- PUBSUB NUMPAT用于返回服务器当前被订阅模式的数量。是通过返回pubsub_patterns链表的长度来实现的,因为这个链表的长度就是 服务器被订阅模式的数量。
2、事务
Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
2.1 事务的实现
2.1.1 事务开始
MULTI命令的执行标志着事务的开始,MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的。
2.1.2 命令入队
服务器判断命令是该入队还是执行的过程如下:
2.1.3 事务队列
每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性里面:
typedef struct redisClient {
//事务状态
multiState mstate;
} redisClient;
事务状态包含一个事务队列,以及一个已入队命令的计数器
typedef struct multiState {
//事务队列,FIFO 顺序
multiCmd *commands;
//已入队命令计数
int count;
} multiState;
事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:
typedef struct multiCmd {
//参数
robj **argv;
//参数数量
int argc;
//命令指针
struct redisCommand *cmd;
} multiCmd;
2.1.4 事务执行
当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。
2.2 WATCH命令的实现
WATCH命令是一个乐观锁
, 它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话 , 服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
2.2.1 使用WATCH命令监视数据库键
每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端。
typedef struct redisDB{
//正在被WATCH命令监视的键
dict *watched_keys; //键为被监视的数据库键, 值为监视该键的客户端链表
}
2.2.2 WATCH机制的触发
当数据库执行修改命令之后,都会调用 touchWatchKey函数对 watches_keys字典进行检查, 检查是否有客户端正在监视刚刚被命令修改过的数据库键, 如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标志打开, 表示该客户端的事务安全性被破坏了。
2.2.3 判断事务是否安全
当服务端收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标志来决定是否执行事务。
2.3 事务的ACID性质
在Redis中事务总是具有原子性(Atomicity)、一致性(Consistency)、隔离性 (Isolation),当Redis运行在某种特定的持久化模式下时,事务也具有耐久性 (Durability)。Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制 (rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。
3、排序
Redis的SORT命令可以对列表键、集合键或者有序集合键的值进行排序。
3.1 SORT<key>命令的实现
服务器执行SORT numbers命令的详细步骤如下:
3.2 ALPHA选项的实现
通过使用ALPHA选项,SORT命令可以对包含字符串值的键进行排序 。
3.3 ASC选项和DESC选项的实现
默认情况,SORT命令执行升序排序,执行SORT命令时使用DESC选项,可以让命令执行降序排序,让排序后的结果按值的大小从大到小排列。
3.4 BY选项的实现
在默认情况下,SORT命令使用被排序键包含的元素作为排序的权重,元素本身决定了元素在排序后所处的位置。通过BY选项,SORT命令可以指定某些字符串键,或者某个哈希键所包含的某些域(filed)来作为元素的权重,对一个键进行排序。
3.5 带有ALPHA选项的BY选项的实现
BY选项默认假设权重键保存的值为数字值,如果权重键保存的是字符串值的话,就需要在使用BY选项时,配合使用ALPHA选项。
3.6 LIMIT选项的实现
LIMIT <offset> <count>
- offset表示要跳过已排序元素数量
- count表示跳过给定数量的已排序元素之后,要返回的已排序元素数量。
3.7 GET选项的实现
默认情况下,SORT命令在对键进行排序之后,总是返回被排序键本身所包含的元素。通过使用GET选项,我们可以让SORT命令返回GET选项所指定模式对应的某些键的值。
3.8 STORE选项的实现
通过使用STORE选项,我们可以将排序结果保存在指定的键里面,并在有需要时重用这个排序结果。
3.9 多个选项的执行顺序
排序>限制排序结果集的长度>获取外部键>保存排序结果>向客户端返回排序结果集
3.10 选项的摆放顺序
除了GET选项之外,改变选项的摆放顺序并不会硬性SORT命令执行这些选项的顺序。
4、慢查询日志
服务器配置有两个和慢查询日志相关的选项:
- slowlog-log-slower-than 选项指定执行时间超过多少微秒(1 秒等于 1,000,000 微秒)的命令请求会被记录到日志上。
- slowlog-max-len 选项指定服务器最多保存多少条慢查询日志。服务器使用先进先出的方式保存多条慢查询日志。
服务器状态中包含了几个和慢查询日志功能有关的属性:
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 链表保存了服务器中的所有慢查询日志, 链表中的每个节点都保存了一个 slowlogEntry 结构, 每个 slowlogEntry 结构代表一条慢查询日志:
typedef struct slowlogEntry {
// 唯一标识符
long long id;
// 命令执行时的时间,格式为 UNIX 时间戳
time_t time;
// 执行命令消耗的时间,以微秒为单位
long long duration;
// 命令与命令参数
robj **argv;
// 命令与命令参数的数量
int argc;
} slowlogEntry;
5、监视器
通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印服务器当前处理的命令请求的相关信息。当其他客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将这条命令请求的信息发送给所有监视器。
redisServer 维护一个 monitors 的链表,记录自己的监视器,每次收到 MONITOR 命令之后,将客户端追加到链表尾。伪代码如下:
def MONITOR():
# 打开客户端的监视器标志
client.flags |= REDIS_MONITOR
# 将客户端添加到服务器状态的monitors链表的末尾
server.monitors.append(client)
# 向客户端返回OK
send_reply("ok")