Redis设计与实现详解四:其他单机功能

Redis设计与实现详解一:数据结构与对象

Redis设计与实现详解二:Redis数据库实现

Redis设计与实现详解三:多机功能实现

发布与订阅

Redis的发布与订阅功能由PUBLISHSUBSCRIBEPSUBSCRIBE等命令组成

通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息

客户端还可以通过执行PSUBSCRIBE命令订阅一个或多个模式,从而成为这些模式的订阅者:每当有其他客户端向某个频道发送消息时,消息不仅会被发送给这个频道的所有订阅者,它还会被发送给所有与这个频道相匹配的模式的订阅者

image-20200822153441416

频道的订阅与退订

Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端

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

image-20200822153615645

订阅频道

当客户端执行命令订阅某个或某些频道的时候,服务器都会将客户端与被订阅的频道在pubsub_channels字典中进行关联

image-20200822153659626

SUBSCRIBE命令的实现可以用以下伪代码来描述:

def subscribe (*all_input_channels):
    
    #遍历输入的所有频道
    for channel in all_input_channels:
    
        #如果channel不存在于pubsub_channels字典(没有任何订阅者)
        #那么在字典中添加channel键,并设置它的值为空链表
        if channel not in server.pubsub_channels:
        	server.pubsub_channels[channel] = []
        
        #将订阅者添加到频道所对应的链表的末尾
        server.pubsub_channels[channel].append(client)

退订频道

当一个客户端退订某个或某些频道的时候,服务器将从pubsub_channels中解除客户端与被退订频道之间的关联:

image-20200822153933502

UNSUBSCRIBE命令的实现可以用以下伪代码来描述:

def unsubscribe(*all_input_channels):
    
    #遍历要退订的所有频道
    for channel in all_input_channels:
    
        #在订阅者链表中删除退订的客户端
        server.pubsub_channels[channel].remove(client)
        
        #如果频道已经没有任何订阅者了(订阅者链表为空)
        #那么将频道从字典中删除
        if len(server.pubsub_channels[channel]) == 0:
        	server.pubsub_channels.remove(channel)

模式的订阅与退订

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

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

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

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

image-20200822154516394

订阅模式

image-20200822154523177

PSUBSCRIBE命令的实现原理可以用以下伪代码来描述:

def psubscribe(*all_input_patterns):
    
    #遍历输入的所有模式
    for pattern in all_input_patterns:
        
        #创建新的pubsubPattern结构
        #记录被订阅的模式,以及订阅模式的客户端
        pubsubPattern = create_new_pubsubPattern()
        pubsubPattern.client = client
        pubsubPattern.pattern = pattern
        
        #将新的pubsubPattern追加到pubsub_patterns链表末尾
        server.pubsub_patterns.append(pubsubPattern)

退订模式

image-20200822154646838

PUNSUBSCRIBE令的实现原理可以用以下伪代码来描述:

def punsubscribe (*all_input_patterns):
    
    #遍历所有要退订的模式
    for pattern in all_input_patterns:
    
        #遍历pubsub patterns链表中的所有pubsubPattern结构
        for pubsubPattern in server.pubsub_patterns:
            
            #如果当前客户端和pubsubPattern记录的客户端相同
            #并且要退订的模式也和pubsubPattern记录的模式相同
            if client == pubsubPattern.client and \
                pattern == pubsubPattern.pattern:
                
                #那么将这个pubsubPattern从链表中删除
                server.pubsub_patterns.remove(pubsubPattern)

发送消息

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

  1. 将消息message发送给channel频道的所有订阅者

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

将消息发送给频道订阅者

PUBLISH命令将消息发送给频道订阅者的方法可以用以下伪代码来描述:

defchannel_publish(channel, message):
    
    #如果channel键不存在于pubsub_channels字典中
    #那么说明channel频道没有任何订阅者
    #程序不做发送动作,直接返回
    if channel not in server.pubsub_channels:
    	return
    
    #运行到这里,说明channel频道至少有一个订阅者
    #程序遍历channel频道的订阅者链表
    #将消息发送给所有订阅者
    for subscriber in server.pubsub_channels[channel]:
    	send_message(subscriber, message)

将消息发送给模式订阅者

PUBLISH命令将消息发送给模式订阅者的方法可以用以下伪代码来描述:

def pattern—publish(channel, message):
    #遍历所有模式订阅消息
    for pubsubPattern in server.pubsub_patterns:
        
        #如果频道和模式相匹配
        if match(channel, pubsubPattern.pattern):
            
            #那么将消息发送给订阅该模式的客户端
            send_message(pubsubPattern.client, message)

查看订阅信息

PUBSUB CHANNELS

PUBSUB CHANNELS [pattern]子命令用于返回服务器当前被订阅的频道,其中pattern参数是可选的

  • 如果不给定pattern参数,那么命令返回服务器当前被订阅的所有频道
  • 如果给定pattern参数,那么命令返回服务器当前被订阅的频道中那些与pattern模式相匹配的频道
def pubsub_channels(pattern=None):
    
    # 一个列表,用于记录所有符合条件的頻道
    channel_list =[]
    
    #遍历服务器中的所有频道
    # (也即是pubsub_channels字典的所有键)
    for channel in server.pubsub_channels:
    
        #当以下两个条件的任意一个满足时,将频道添加到链表里面:
        #1 )用户没有指定pattern参数
        #2 )用户指定了 pattern参数,并且channel和pattern匹配
        if (pattern is None) or match(channel, pattern):
        	channel_list.append(channel)
     
    #向客户端返回频道列表
    return channel_list

PUBSUB NUMSUB

PUBSUB NUMSUB [channel-1 channel-2 ... channel-n]子命令接受任意多个频道作为输入参数,并返回这些频道的订阅者数量

def pubsub_numsub (*all_input_channels):
    
    #遍历输入的所有频道
    for channel in all_input_channels:
        
        #如果pubsub_channels字典中没有channel这个键
        #那么说明channel频道没有任何订阅者
        if channel not in server.pubsub_channels:
        	
            #返回频道名
        	reply_channel_name(channel)
        	
            #订阅者数量为0
        	reply_subscribe_count(0)
    
        #如果pubsub_channels字典中存在channel键
        #那么说明channel频道至少有一个订阅者
        else:
            #返回频道名
            reply_channel_name(channel)
            #订阅者链表的长度就是订阅者数量
            reply_subscribe_count(len(server.pubsub_channels[channel] ))

PUBSUB NUMPAT

PUBSUB NUMPAY子命令用于返回服务器当前被订阅模式的数量

def pubsub_numpat():
    # pubsub_patterns链表的长度就是被订阅模式的数量
    reply_pattern_count(len(server.pubsub一patterns))

重点回顾

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

事务

Redis通过MULTIEXECWATCH等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求

事务的实现

事务开始

MULTI命令的执行标志着事务的开始

MULTI命令将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成的,MULTI命令的实现可以用以下伪代码来表示:

def MULTI():
    
    #打开事务标识
    client.flags |= REDIS一MULTI
    
    #返回OK回复
    replyOK()

命令入队

  • 如果客户端发送的命令为EXECDISCARDWATCHMULTI四个命令中的一个,服务器立即执行这个命令
  • 如果客户端发送的命令是EXECDISCARDWATCHMULTI四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复

事务队列

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

typedef struct redisClient {
    // ...
    //事务状态
    multistate mstate; /* MULTI/EXEC state */
    //...
} redisClient;

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

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

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

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

image-20200822200256746

执行事务

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

def EXEC():
    
    #创建空白的回复队列
    reply_queue =[]
    
    #遍历事务队列中的每个项
    #读取命令的参数,参数的个数,以及要执行的命令
    for argv, argc, cmd in client.instate.commands:
    
        #执行命令,并取得命令的返回值
        reply = execute_command(cmd, argv, argc)
        
        #将返回值追加到回复队列末尾
        reply_queue.append(reply)
        
    #移除标识,让客户端回到非事务状态
    client.flags &= ~REDIS_MULTI
    
    #清空客户端的事务状态,包括:
    #1 )清零入队命令计数器
    #2 )释放事务队列
    client.mstate.count = 0
    release_transaction_queue(client.mstate.commands)
    
    #将事务的执行结果返回给客户端
    send_reply_to_client(client, reply_queue)

WATCH命令的实现

WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检査被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务

使用WATCH命令监视数据库键

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

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

image-20200822200813980

监视机制的触发

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

def touchWatchKey(db, key):
    
    #如果键key存在于数据库的watched_keys字典中
    #那么说明至少有一个客户端在监视这个key
    if key in db.watched_keys:
    
    #遍历所有监视键key的客户端
    for client in db.watched_keys[key]:
    
        #打开标识
        client.flags |= REDIS_DIRTY_CAS

判断事务是否安全

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

事务的ACID性质

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

原子性

事务具有原子性指的是,数据库将事务中的多个操作当作一个整体来执行,服务器要么就执行事务中的所有操作,要么就一个操作也不执行

对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis的事务是具有原子性的

在这个例子中,即使命令在执行期间出现了错误,事务的后续命令也会继续执行下去,并且之前执行的命令也不会有任何影响

redis> SET msg "hello" # msg 键是一个字符串
OK
redis> MULTI
OK
redis> SADD fruit "apple" "banana" "cherry"
QUEUED
redis> RPUSH msg "good bye" "bye bye" #错误地对字符串键msg执行列表键的命令
QUEUED
redis> SADD alphabet "a” ”b” "c"
QUEUED
redis> EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3

—致性

事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的

“一致”指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据

入队错误

如果一个事务在入队命令的过程中,出现了命令不存在,或者命令的格式不正确等情况,那么Redis将拒绝执行这个事务

因为服务器会拒绝执行入队过程中出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响

执行错误

即使在事务的执行过程中发生了错误,服务器也不会中断事务的执行,它会继续执行事务中余下的其他命令,并且已执行的命令(包括执行命令所产生的结果)不会被出错的命令影响(前面那个例子)

服务器停机

Redis服务器运行在哪种持久化模式下,事务执行中途发生的停机都不会影响数据库的一致性

隔离性

事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同

Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的

耐久性

事务的耐久性指的是,当一个事务执行完毕时,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失

Redis并没有为事务提供任何额外的持久化功能,所以Redis事务的耐久性由Redis所使用的持久化模式决定

只有服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里面,因此这种配置下的事务是具有耐久性的

但是,当no-appendfsync-on-rewrite配置选项开启时:在执行BGSAVE命令或者BGREWRITEAOF命令期间,服务器会暂时停止对AOF文件进行同步,从而尽可能地减少I/O阻塞。会使关于“always模式的AOF持久化可以保证事务的耐久性”这一结论将不再成立

重点

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

Lua脚本

排序

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

SORT命令使用ALPHA选项,对一个包含字符串值的集合进行排序

SORT命令和BY选项,例:

# 以jack_number、peter_number、tom_number三个键的值为权重(weight),对有序集合test-result中的"jack"、"peter"、"tom”三个成员(member)进行排序

redis> ZADD test-result 3.0 jack 3.5 peter 4.0 tom
(integer) 3

#按元素的分值排列
redis> ZRANGE test-result 0 -1
1) "jack"
2) "peter"
3) "tom"

#为各个元素设置序号
redis> MSET peter_number 1 tom_number 2 jack_number 3
OK

#以序号为权重,对有序集合中的元素进行排序
redis> SORT test-result BY *_number
1) "peter"
2) "tom"
3) "jack"

SORT <key> 命令的实现

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

  1. 创建一个和numbers列表长度相同的数组,该数组的每个项都是一个 redis.h/redisSortObject 结构

image-20200822210549350

  1. 遍历数组,将各个数组项的obj指针分别指向numbers列表的各个项,构成obj指针和列表项之间的一对一关系

image-20200822210559080

  1. 遍历数组,将各个obj指针所指向的列表项转换成一个double类型的浮点数,并将这个浮点数保存在相应数组项的u . score属性里

image-20200822210610742

  1. 根据数组项u. score属性的值,对数组进行数字值排序,排序后的数组项按u.score属性的值从小到大排列

image-20200822210625568

  1. 遍历数组,将各个数组项的obj指针所指向的列表项作为排序结果返回给客户端

redisSortObject结构的完整定义:

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

ALPHA选项的实现

根据obj指针所指向的集合元素,对数组进行字符串排序,排序后的数组项按集合元素的字符串值从小到大排列

这一步代替前面的三四两步

ASC选项和DESC选项的实现

ASC是从小到大、DESC是从大到小

BY选项的实现

redis> SADD fruits "apple" "banana" "cherry"
(integer) 3
redis> SORT fruits ALPHA
1) "apple"
2) "banana"
3) "cherry"

redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple"
  1. 创建一个redisSortObject结构数组,数组的长度等于fruits集合的大小

  2. 遍历数组,将各个数组项的obj指针分别指向fruits集合的各个元素

3) 遍历数组,根据各个数组项的obj指针所指向的集合元素,以及BY选项所给定的模式*-price,查找相应的权重键

  • 对于"apple"元素,査找程序返回权重键"apple-price"
  • 对于"banana"元素,査找程序返回权重键"banana-price"
  • 对于"cherry"元素,査找程序返回权重键"cherry-price"
  1. 将各个权重键的值转换成一个double类型的浮点数,然后保存在相应数组项的u. score属性里面
  • "apple"元素的权重键"apple-price"的值转换之后为8.0
  • "banana"元素的权重键"banana-price"的值转换之后为5.5
  • "cherry"元素的权重键"cherry-price"的值转换之后为7.0
  1. 以数组项u. score属性的值为权重,对数组进行排序,得到一个按u . score属性的值从小到大排序的数组
  • 权重为5.5的"banana"元素位于数组的索引0位置上
  • 权重为7.0的"cherry"元素位于数组的索引1位置上
  • 权重为8.0的"apple"元素位于数组的索引2位置上
  1. 遍历数组,依次将数组项的obj指针所指向的集合元素返回给客户端。

带有ALPHA选项的BY选项的实现

LIMIT选项的实现

仅改变前面例子的最后一步

GET选项的实现

redis> SADD students "peter" "jack" "tom"
(integer) 3

#设置peter、jack、tom的全名
redis> SET peter-name "Peter White"
OK
redis> SET jack-name "Jack Snow"
OK
redis> SET tom-name "Tom Smith"
OK

#然后根据这些结果,获取并返回键jack-name、peter-name和tom-name的值
redis> SORT students ALPHA GET *-name
1) "Jack Snow"
2) "Peter White"
3) "Tom Smith"

类似

STORE选项的实现

过使用STORE选项,可以将排序结果保存在指定的键里面,并在有需要时重用这个排序结果

多个选项的执行顺序

选项的执行顺序

  1. 排序:在这一步,命令会使用ALPHAASCDESCBY这几个选项,对输入键进行排序,并得到一个排序结果集

  2. 限制排序结果集的长度:在这一步,命令会使用LIMIT选项,对排序结果集的长度进行限制,只有LIMIT选项指定的那部分元素会被保留在排序结果集中

  3. 获取外部键:在这一步,命令会使用GET选项,根据排序结果集中的元素,以及GET选项指定的模式,査找并获取指定键的值,并用这些值来作为新的排序结果集

  4. 保存排序结果集:在这一步,命令会使用STORE选项,将排序结果集保存到指定的键上面去

  5. 向客户端返回排序结果集:在最后这一步,命令遍历排序结果集,并依次向客户端返回排序结果集中的元素

选项的摆放顺序

除了 GET 选项之外,改变选项的摆放顺序并不会影响命令执行这些选项的顺序

重点

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

二进制位数组

慢日志查询

Redis的慢查询日志功能用于记录执行时间超过给定时长的命令请求,用户可以通过这个功能产生的日志来监视和优化查询速度

服务器配置有两个和慢査询日志相关的选项:

  • slowlog-log-slower-than选项指定执行时间超过多少微秒(1秒等于1 000 000微秒)的命令请求会被记录到日志上

  • slowlog-max-len选项指定服务器最多保存多少条慢査询日志

    服务器使用先进先岀的方式保存多条慢査询日志,当服务器存储的慢査询日志数量等于slowlog-max-len选项的值时,服务器在添加一条新的慢査询日志之前,会先将最旧的一条慢查询日志删除

使用SLOWLOG GET 命令査看服务器所保存的慢查询日志

慢查询记录的保存

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链表保存了服务器中的所有慢査询日志,链表中的每个节点都保存了一个slowlogEntry结构,每个slowlogEntry结构代表一条慢査询日志

typedef struct slowlogEntry {
    
    //唯一标识符
    long long id;
    
    //命令执行时的时间,格式为时间戥
    time_t time;
    
    //执行命令消耗的时间,以微秒为单位
    long long duration;
    
    //命令与命令参数
    robj **argv;
    
    //命令与命令参数的数量
    int argc;
} slowlogEntry;

image-20200823144628506

慢查询曰志的阅览和删除

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)

添加新曰志

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

void slowlogPushEntrylfNeeded(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));
}

重点

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

监视器

通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息

每当一个客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求之外,还会将关于这条命令请求的信息发送给所有监视器

image-20200823145245720

成为监视器

发送MONITOR命令

def MONITOR():
    
    #打开客户端的监视器标志
    client.flags |= REDIS_MONITOR
    
    #将客户端添加到服务器状态的monitors链表的末尾
    server.monitors.append(client)
    
    #向客户端返回ok
    send一reply("OK")

image-20200823145359501

向监视器发送命令信息

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

def replicationFeedMonitors(client, monitors, dbid, argv, argc):

    #根据执行命令的客户端、当前数据库的号码、命令参数、命令参数个数等参数
    #创建要发送给各个监视器的信息
    msg = create_message(client, dbid, argv, argc)
    
    #遍历所有监视器
    for monitor in monitors:
        
        #将信息发送给监视器
        send_message(monitor, msg)

重点

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

整理于《Redis设计与实现》——黄建宏,侵删

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值