redis底层设计(四)——功能的实现
redis中我们会经常用到事务、订阅与发布、Lua脚本以及慢查询日志,接下来我们就一一对他们进行探讨学习。
4.1事务
redis通过MULTI、DISCARD、EXEC和WATCH四个命令来实现事务功能。
4.1.1 事务
事务提供了一种“将多个命令打包,一次性按顺序地执行”的机制,并且事务在执行的期间不会主动中断——服务器在执行完所有的命令之后,才会继续处理其他客户端的其他命令。如下:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
) OK
) "Mastering C++ in 21 days"
) (integer)
) ) "Mastering Series"
) "C++"
) "Programming"
一个事务从开始到执行会经历3个过程:
1)开始事务
2)命令入队
3)执行事务
下面分别介绍这三个过程:
4.1.2 开始事务
MULTI命令标志着事务的开始。该命令的作用是将客户端的REDIS_MULTI选项打开,让客户端从非事务状态切换到事务状态。
4.1.3 命令入队
当客户端进入事务状态时,服务器在收到来自客户端的命令时,不会立即执行,而是将这些命令全部放进QUEUE事务队列中,然后返回QUEUE,表示命令已入队。
事务队列是一个数组,每个数组包含3个属性:
1)要执行的命令(cmd);
2)命令的参数(argv);
3)参数的个数(argc);
比如:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那么程序将为客户端创建以下事务队列:
4.1.4 执行事务
当客户端正处在事务状态,若接收到EXEC、DISCARD、MULTI和WATCH这四个命令时还是会直接执行,而其他的命令是会被放到QUEUE队列中去的。服务器会以先进先出(FIFO)的方式执行,将执行命令得到的结果以FIFO的方式存放到一个回复队列中
4.1.5 事务状态下的DISCARD、MULTI和WATCH命令
DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,然后将客户端从事务状态调整回非事务状态,最后返回字符串OK 给客户端,说明事务已被取消。
Redis 的事务是不可嵌套的,当客户端已经处于事务状态,而客户端又再向服务器发送MULTI时,服务器只是简单地向客户端发送一个错误,然后继续等待其他命令的入队。MULTI 命令的发送不会造成整个事务失败,也不会修改事务队列中已有的数据。
WATCH 只能在客户端进入事务状态之前执行,在事务状态下发送WATCH 命令会引发一个错误,但它不会造成整个事务失败,也不会修改事务队列中已有的数据(和前面处理MULTI的情况一样)。
4.1.6 带WATCH的事务
WATCH 命令用于在事务开始之前监视任意数量的键:当调用EXEC 命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。
如下:
在时间T4 ,客户端B 修改了name 键的值,当客户端A 在T5 执行EXEC 时,Redis 会发现name 这个被监视的键已经被修改,因此客户端A 的事务不会被执行,而是直接返回失败。
4.1.7 WATCH命令的实现
在每个代表数据库的redis.h/redisDb结构类型中,都保存了一个watch_keys字典,字典的键是这个数据库被监视的键,而字典的值是一个链表,链表中保存着所有监视这个键的客户端。如下:
其中,key1正在被client2、client5、client1三个客户端监视,其他键也在被其他的客户端监视着。而WATCH的作用就是将客户端和要监视的键进行关联。
4.1.8 WATCH的触发
在任何对数据库键空间(key space)进行修改的命令成功执行之后(比如FLUSHDB 、SET、DEL 、LPUSH 、SADD 、ZREM ,诸如此类),multi.c/touchWatchKey 函数都会被调用——它检查数据库的watched_keys 字典,看是否有客户端在监视已经被命令修改的键,如果有的话,程序将所有监视这个/这些被修改键的客户端的REDIS_DIRTY_CAS 选项打开
当客户端发送EXEC 命令、触发事务执行时,服务器会对客户端的状态进行检查:
• 如果客户端的REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
• 如果REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
4.1.9 事务的ACID性质
redis保证了事务的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
1)原子性(A):
单个redis命令的执行是原子性的,但redis没有在事务上增加任何维持原子性的机制,所以redis事务的执行并不是原子性的。
如果redis服务器进程在执行任务的过程中被停止——比如接到KILL命令、宿主机器停机等等,那么事务执行失败,当事务执行失败时,redis是不会做任何的重试或回滚的。
2)一致性(C):
redis的一致性可以从下面三个方面来讨论:入队错误、执行错误和redis进程被终结。
a.入队错误:
在命令入队的过程中,如果客户端想服务器发送了错误的命令,比如命令的参数数量错误等等,那么服务器将想客户端返回一个出错的信息,并且将客户端的事务状态设为REDIS_DIRTY_EXEC。当客户端执行EXEC命令的时候,redis会拒绝执行 REDIS_DIRTY_EXEC的事务,并返回失败信息。因此,带有不正确的入队命令的事务不会被执行,也不会影响数据库的一致性。
b.执行错误:
如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的key 执行了错误的操作,那么Redis 只会将错误包含在事务的结果中,这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响。
c.redis进程被终结:
如果Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死,那么根据Redis 所使用的持久化模式,可能有以下情况出现:
• 内存模式:如果Redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。
• RDB 模式:在执行事务时,Redis 不会中断事务去执行保存RDB 的工作,只有在事务执行之后,保存RDB 的工作才有可能开始。所以当RDB 模式下的Redis 服务器进程在事务中途被杀死时,事务内执行的命令,不管成功了多少,都不会被保存到RDB 文件里。恢复数据库需要使用现有的RDB 文件,而这个RDB 文件的数据保存的是最近一次的数据库快照(snapshot),所以它的数据可能不是最新的,但只要RDB 文件本身没有因为其他问题而出错,那么还原后的数据库就是一致的。
• AOF 模式:因为保存AOF 文件的工作在后台线程进行,所以即使是在事务执行的中途,保存AOF 文件的工作也可以继续进行,因此,根据事务语句是否被写入并保存到AOF文件,有以下两种情况发生:
1)如果事务语句未写入到AOF 文件,或AOF 未被SYNC 调用保存到磁盘,那么当进程被杀死之后,Redis 可以根据最近一次成功保存到磁盘的AOF 文件来还原数据库,只要AOF 文件本身没有因为其他问题而出错,那么还原后的数据库总是一致的,但其中的数据不一定是最新的。
2)如果事务的部分语句被写入到AOF 文件,并且AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在AOF 文件里,当重启Redis 时,程序会检测到AOF 文件并不完整,Redis 会退出,并报告错误。需要使用redis-check-aof 工具将部分成功的事务命令移除之后,才能再次启动服务器。还原之后的数据总是一致的,而且数据也是最新的(直到事务执行之前为止)。
3)隔离性(I):
redis是但进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,redis的事务总是带有隔离性的。
4)持久性(D):
因为事务不过是用队列包裹起了一组Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由Redis 所使用的持久化模式决定:
• 在单纯的内存模式下,事务肯定是不持久的。
• 在RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以RDB 模式下的Redis 事务也是不持久的。
• 在AOF 的“总是SYNC ”模式下,事务的每条命令在执行成功之后,都会立即调用fsync或fdatasync 将事务数据写入到AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段
非常小的间隔,所以这种模式下的事务也是不持久的。其他AOF 模式也和“总是SYNC ”模式类似,所以它们都是不持久的。
4.1.10 小结:
* 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制;
* 事务在执行过程中不会被打断,所有事务命令执行完之后,事务才会结束;
* 多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行;
* 带WATCH命令的事务会在数据库的WATCHED_KEYS字典中将客户端和被监视的键进行关联;当键被修改时,程序会将所有监视被修改键的客户端的REDIS_DIRTY_CAS选项打开;
* 只有在客户端的REDIS_DIRTY_CAS选项未被打开时,才能执行事务,否则事务直接返回失败;
* redis保持了事务的一致性和隔离性,但并不保证事务的原子性和持久性;
4.2 订阅与发布
Redis 通过PUBLISH 、SUBSCRIBE 等命令实现了订阅与发布模式,这个功能提供两种信息机制,分别是订阅/发布到频道和订阅/发布到模式,下文先讨论订阅/发布到频道的实现,再讨论订阅/发布到模式的实现。
4.2.1 频道的订阅
每个Redis 服务器进程都维持着一个表示服务器状态的redis.h/redisServer 结构,结构的pubsub_channels 属性是一个字典,这个字典就用于保存订阅频道的信息:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};
其中,字典的键为正在被订阅的频道,而字典的值则是一个链表,链表中保存了所有订阅这个频道的客户端。
通过pubsub_channels 字典,程序只要检查某个频道是否字典的键,就可以知道该频道是否正在被客户端订阅;只要取出某个键的值,就可以得到所有订阅该频道的客户端的信息。
4.2.2 发送信息到频道
了解了pubsub_channels 字典的结构之后,解释PUBLISH 命令的实现就非常简单了:当调用PUBLISH channel message 命令,程序首先根据channel 定位到字典的键,然后将信息发送给字典值链表中的所有客户端。
4.2.3 退订频道
使用UNSUBSCRIBE 命令可以退订指定的频道,这个命令执行的是订阅的反操作:它从pubsub_channels 字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的信息就不会再发送给这个客户端。
4.2.4 模式的订阅与信息发送
当使用PUBLISH 命令发送信息到某个频道时,不仅所有订阅该频道的客户端会收到信息,如果有某个/某些模式和这个频道匹配的话,那么所有订阅这个/这些频道的客户端也同样会收到信息。
上图展示了一个带有频道和模式的例子,当有信息发送到tweet.shop.kindle 频道时,信息除了发送给clientX 和clientY 之外,还会发送给订阅tweet.shop.* 模式的client123 和client256。
2.4.5 订阅模式
redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:
struct redisServer {
// ...
list *pubsub_patterns;
// ...
};
链表中的每个节点都包含一个redis.h/pubsubPattern 结构:
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;
client 属性保存着订阅模式的客户端,而pattern 属性则保存着被订阅的模式。每当调用PSUBSCRIBE 命令订阅一个模式时,程序就创建一个包含客户端信息和被订阅模式的pubsubPattern 结构,并将该结构添加到redisServer.pubsub_patterns 链表中。
作为例子,下图展示了一个包含两个模式的pubsub_patterns 链表,其中client123 和client256 都正在订阅tweet.shop.* 模式:
通过遍历整个pubsub_patterns 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。
2.4.6 发送信息到模式
发送信息到模块的工作是由PUBLISH命令完成的,PUBLISH除了将message发送到所有订阅channel的客户端上,他还会将channel和pubsub_patterns中的模式进行对比,如果channel和某种模式匹配的话,那么也将message发送到订阅那个模式的客户端。
例如:redis服务器的pubsub_patterns状态如下:
那么当某个客户端发送信息"Amazon Kindle, $69." 到tweet.shop.kindle 频道时,除了所有订阅了tweet.shop.kindle 频道的客户端会收到信息之外,客户端client123 和client256也同样会收到信息,因为这两个客户端订阅的tweet.shop.* 模式和tweet.shop.kindle 频道匹配。
4.2.7 退订模式
使用PUNSUBSCRIBE 命令可以退订指定的模式,这个命令执行的是订阅模式的反操作:程序会删除redisServer.pubsub_patterns 链表中,所有和被退订模式相关联的pubsubPattern结构,这样客户端就不会再收到和模式相匹配的频道发来的信息。
4.2.8 小结
* 订阅消息由服务器进程维持的RedisServer.pubsub_channels字典保存,字典的键是被订阅的频道,字典的值是订阅该频道的所有客户端;
* 当有新消息发送到频道时,程序遍历频道(键)所对应的客户端(值),然后将信息发送到所有订阅频道的客户端上;
* 订阅模式的信息由服务器进行维持的RedisServer_pubsub_patterns链表保存,链表的每个节点都保存着一个pubsubPattern 结构,结构中保存着被订阅的模式,以及订阅模式的客户端。程序通过遍历链表来查询某个频道是否和某个模式匹配;
* 当有新消息发送到频道时,除了订阅频道的客户端会接收到消息之外,所有与频道模式相匹配的客户端也会收到消息;
* 退订频道和退订模式都分别是订阅频道和订阅模式的反操作。
4.3Lua脚本
Lua是redis 2.6 版本最大的亮点,通过内嵌对Lua 环境的支持,Redis 解决了长久以来不能高效地处理CAS (check-and-set)命令的缺点,并且可以通过组合使用多个命令,轻松实现以前很难实现或者不能高效实现的模式。
4.3.1 初始化Lua环境
在初始化redis服务器的时候,对Lua环境的初始化也会一并进行;
整个初始化Lua环境的步骤如下:
1)调用lua_open函数,创建一个新的Lua环境;
2)载入指定的Lua函数库:基础库(base lib)、表格库(table lib)、字符串库(string lib)、数学库(math lib)、调试库(debug lib)、用于处理JSON对象的cjson库、在Lua值和C结构之间进行切换的struct库和处理MessagePack数据的cmsgpack库
3)屏蔽一些可能对Lua环境产生安全问题的函数,比如loadfile;
4)创建一个redis字典,保存Lua脚本,并在复制脚本是使用,字典的键为SHA1校验和,字典的值为Lua脚本;
5)创建一个redis全局表格到Lua环境,表格中包含了各种对redis进行操作的函数;
6)用redis自己定义的随机生成函数,替换math表原有的math.random函数和math.randomseed函数,新的函数具有这样的特质:每次执行Lua 脚本时,除非显式地调用math.randomseed ,否则math.random 生成的伪随机数序列总是相同的;
7)创建一个对Redis 多批量回复(multi bulk reply)进行排序的辅助函数;
8)对Lua 环境中的全局变量进行保护,以免被传入的脚本修改;
9)因为Redis 命令必须通过客户端来执行,所以需要在服务器状态中创建一个无网络连接的伪客户端(fake client),专门用于执行Lua 脚本中包含的Redis 命令:当Lua 脚本需要执行Redis 命令时,它通过伪客户端来向服务器发送命令请求,服务器在执行完命令之后,将结果返回给伪客户端,而伪客户端又转而将命令结果返回给Lua 脚本;
10)将Lua 环境的指针记录到Redis 服务器的全局状态中,等候Redis 的调用;
以上就是Redis 初始化Lua 环境的整个过程,当这些步骤都执行完之后,Redis 就可以使用Lua 环境来处理脚本了。严格来说,步骤1 至8 才是初始化Lua 环境的操作,而步骤9 和10 则是将Lua 环境关联到服务器的操作,为了按顺序观察整个初始化过程,我们将两种操作放在了一起。另外,步骤6 用于创建无副作用的脚本,而步骤7 则用于去除部分Redis 命令中的不确定性(non deterministic),关于这两点,请看下面一节关于脚本安全性的讨论。
4.3.2 脚本的安全性
当将Lua 脚本复制到附属节点,或者将Lua 脚本写入AOF 文件时,Redis 需要解决这样一个问题:如果一段Lua 脚本带有随机性质或副作用,当这段脚本在附属节点运行时,或从AOF 文件载入重新运行时,它得到的结果可能和之前运行的结果完全不同。
注意:只有在带有随机性的脚本进行写入时,随机性才是有害的,如果一个脚本只是执行只读操作,那么随机性是无害的
和随机性质类似,如果一个脚本的执行对任何副作用产生了依赖,那么这个脚本每次执行的结果都可能会不一样。为了解决这个问题,Redis 对Lua 环境所能执行的脚本做了一个严格的限制——所有脚本都必须是无副作用的纯函数(pure function)。
为此,Redis 对Lua 环境做了一些列相应的措施:
• 不提供访问系统状态状态的库(比如系统时间库)。
• 禁止使用loadfile 函数。
• 如果脚本在执行带有随机性质的命令(比如RANDOMKEY ),或者带有副作用的命令(比如TIME )之后,试图执行一个写入命令(比如SET ),那么Redis 将阻止这个脚本继续运行,并返回一个错误。
• 如果脚本执行了带有随机性质的读命令(比如SMEMBERS ),那么在脚本的输出返回给Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
• 用Redis 自己定义的随机生成函数,替换Lua 环境中math 表原有的math.random 函数地调用math.randomseed ,否则math.random 生成的伪随机数序列总是相同的。
经过这一系列的调整之后,Redis 可以保证被执行的脚本:
* 无副作用。
* 没有有害的随机性。
* 对于同样的输入参数和数据集,总是产生相同的写入命令。
4.3.3 脚本的执行
在脚本环境的初始化工作完成以后,Redis 就可以通过EVAL 命令或EVALSHA 命令执行Lua脚本了。
redis> EVAL "return 'hello world'" 0
"hello world"
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de910 // 上一个脚本的校验和
"hello world"
4.3.4 EVAL命令的实现
EVAL 命令的执行可以分为以下步骤:
1) 为输入脚本定义一个Lua 函数。
2) 执行这个Lua 函数。
定义Lua函数:
所有被Redis 执行的Lua 脚本,在Lua 环境中都会有一个和该脚本相对应的无参数函数:当调用EVAL 命令执行脚本时,程序第一步要完成的工作就是为传入的脚本创建一个相应的Lua函数。
举个例子,当执行命令EVAL "return 'hello world'" 0 时,Lua 会为脚本"return 'hello world'" 创建以下函数:
function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
end
其中,函数名以f_ 为前缀,后跟脚本的SHA1 校验和(一个40 个字符长的字符串)拼接而成。而函数体(body)则是用户输入的脚本。以函数为单位保存Lua 脚本有以下好处:
• 执行脚本的步骤非常简单,只要调用和脚本相对应的函数即可。
• Lua 环境可以保持清洁,已有的脚本和新加入的脚本不会互相干扰,也可以将重置Lua环境和调用Lua GC 的次数降到最低。
• 如果某个脚本所对应的函数在Lua 环境中被定义过至少一次,那么只要记得这个脚本的SHA1 校验和,就可以直接执行该脚本——这是实现EVALSHA 命令的基础,稍后在介绍EVALSHA 的时候就会说到这一点。
在为脚本创建函数前,程序会先用函数名检查Lua 环境,只有在函数定义未存在时,程序才创建函数。重复定义函数一般并没有什么副作用,这算是一个小优化。
另外,如果定义的函数在编译过程中出错(比如,脚本的代码语法有错),那么程序向用户返回一个脚本错误,不再执行后面的步骤。
执行Lua函数:
在定义好Lua 函数之后,程序就可以通过运行这个函数来达到运行输入脚本的目的了。不过,在此之前,为了确保脚本的正确和安全执行,还需要执行一些设置钩子、传入参数之类的操作,整个执行函数的过程如下:
1. 将EVAL 命令中输入的KEYS 参数和ARGV 参数以全局数组的方式传入到Lua 环境中。
2. 设置伪客户端的目标数据库为调用者客户端的目标数据库: fake_client->db =caller_client->db ,确保脚本中执行的Redis 命令访问的是正确的数据库。
3. 为Lua 环境装载超时钩子,保证在脚本执行出现超时时可以杀死脚本,或者停止Redis服务器。
4. 执行脚本对应的Lua 函数。
5. 如果被执行的Lua 脚本中带有SELECT 命令,那么在脚本执行完毕之后,伪客户端中的数据库可能已经有所改变,所以需要对调用者客户端的目标数据库进行更新:caller_client->db = fake_client->db 。
6. 执行清理操作:清除钩子;清除指向调用者客户端的指针;等等。
7. 将Lua 函数执行所得的结果转换成Redis 回复,然后传给调用者客户端。
8. 对Lua 环境进行一次单步的渐进式GC 。
以下是执行EVAL "return 'hello world'" 0 的过程中,调用者客户端(caller)、Redis 服务器和Lua 环境之间的数据流表示图:
4.3.5 小结
初始化Lua脚本环境需要一系列步骤,其中包括:
* 创建Lua环境;
* 载入Lua库,比如字符串库、数学库、表格库、调试库等;
* 创建redis全局表格,包含各种对redis进行操作的函数,比如redis.call 和 redis.log等;
* 创建一个无网络的伪客户端,专门用于执行Lua脚本中的redis命令;
redis通过一系列的措施保证被执行的Lua脚本无副作用,也没有有害的写随机性:对于同样的输入参数和数据集,总是产生相同的写入命令。
EVAL命令为输入脚本定义一个Lua函数,然后通过执行这个函数来执行脚本。
EVALSHA通过构建函数名,直接调用Lua中已定义的函数,从而执行相应的脚本。
4.4 慢查询日志
慢查询日志是redis提供的一个用于观察系统性能的功能。一句话就是将那些执行时间比较长的命令记录到日志中。
4.4.1 相关数据结构:
每条慢查询日志都以一个slowlog.h/slowlogEntry结构定义:
typedef struct slowlogEntry {
// 命令参数
robj **argv;
// 命令参数数量
int argc;
// 唯一标识符
long long id; /* Unique entry identifier. */
// 执行命令消耗的时间,以纳秒(1 / 1,000,000,000 秒)为单位
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令执行时的时间
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
记录服务器状态的redis.h/redisServer 结构里保存了几个和慢查询有关的属性:
struct redisServer {
// ... other fields
// 保存慢查询日志的链表
list *slowlog; /* SLOWLOG list of commands */
// 慢查询日志的当前id 值
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 慢查询时间限制
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 慢查询日志的最大条目数量
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
// ... other fields
};
slowlog 属性是一个链表,链表里的每个节点保存了一个慢查询日志结构,所有日志按添加时间从新到旧排序,新的日志在链表的左端,旧的日志在链表的右端。
slowlog_entry_id 在创建每条新的慢查询日志时增一,用于产生慢查询日志的ID (这个ID在执行SLOWLOG RESET 之后会被重置)。slowlog_log_slower_than 是用户指定的命令执行时间上限,执行时间大于等于这个值的命令会被慢查询日志记录。
slowlog_max_len 慢查询日志的最大数量,当日志数量等于这个值时,添加一条新日志会造成最旧的一条日志被删除。
下图展示了一个slowlog 属性的实例:
4.4.2 慢查询日志的记录:
在每次执行命令之前,Redis 都会用一个参数记录命令执行前的时间,在命令执行完之后,再计算一次当前时间,然后将两个时间值相减,得出执行命令所耗费的时间值duration ,并将duration 传给slowlogPushEntryIfNeed 函数。
如果duration 超过服务器设置的执行时间上限server.slowlog_log_slower_than 的话,slowlogPushEntryIfNeed 就会创建一条新的慢查询日志,并将它加入到慢查询日志链表里。
4.4.3 慢查询日志的操作:
针对慢查询日志有三种操作,分别是查看、清空和获取日志数量:
• 查看日志:在日志链表中遍历指定数量的日志节点,复杂度为O(N) 。命令是:slowlog get
• 清空日志:释放日志链表中的所有日志节点,复杂度为O(N) 。命令是:slowlog reset
• 获取日志数量:获取日志的数量等同于获取server.slowlog 链表的数量,复杂度为O(1) 。 命令是:slowlog len
4.4.4 小结:
* redis用一个链表以FIFO的顺序保存着所有慢查询日志;
* 每条慢查询日志以一个慢查询节点表示,节点中记录着执行超时的命令、命令的参数、命令执行时的时间,以及执行命令所消耗的时间等信息。
redis底层设计(四)——功能的实现的更多相关文章
- redis底层设计(五)——内部运作机制
5.1 数据库 5.1.1 数据库的结构: Redis 中的每个数据库,都由一个redis.h/redisDb 结构表示: typedef struct redisDb { // 保存着数据库以整数表 ...
- redis底层设计(一)——内部数据结构
redis是一个key-value存储系统.和Memcached类似,它支持存储的value类型相对更多,包括string(字符串).list(链表).set(集合).zset(sorted set ...
- redis底层设计(三)——redis数据类型
今天我们来看一下redis的数据类型.既然redis的键值对可以保存不同类型的值,那么很自然就需要对键值对的类型进行检查以及多态处理.下面我们将对redis所使用的对象系统进行了解,并分别观察字符串. ...
- redis底层设计(二)——内存映射数据结构
我们继续接着上一篇博客,今天来看看内存映射数据结构. 上篇我们讲了内部数据结构,虽然内部数据结构非常强大,但是创建一系列完整的数据结构本身也是一件相当耗费时间的工作,当一个对象包含的元素数量并不多,或 ...
- Redis 详解 (四) redis的底层数据结构
目录 1.演示数据类型的实现 2.简单动态字符串 3.链表 4.字典 5.跳跃表 6.整数集合 7.压缩列表 8.总结 上一篇博客我们介绍了 redis的五大数据类型详细用法,但是在 Redis 中, ...
- Redis核心设计原理(深入底层C源码)
Redis 基本特性 1. 非关系型的键值对数据库,可以根据键以O(1) 的时间复杂度取出或插入关联值 2. Redis 的数据是存在内存中的 3. 键值对中键的类型可以是字符串,整型,浮点型等,且键 ...
- 10w+QPS 的 Redis 真的只是因为单线程和内存?360° 深入底层设计为你揭开 Redis 神秘面纱!
原文链接:10w+QPS 的 Redis 真的只是因为单线程和内存?360° 深入底层设计为你揭开 Redis 神秘面纱! 你以为 Redis 这么快仅仅因为单线程和基于内存? 那么你想得太少了,我个 ...
- redis底层实现的几种数据结构
redis底层数据结构 一.简单动态字符串(SDS) 定义: struct sdshdr{ int len; //SDS所保存的字符串长度 int free //记录buf数组中为使用的字节数量 ...
- 【转】分析Redis架构设计
一.前言 因为近期项目中开始使用Redis,为了更好的理解Redis并应用在适合的业务场景,需要对Redis设计与实现深入的理解. 我分析流程是按照从main进入,逐步深入分析Redis的启动流程.同 ...
随机推荐
- viewPager+fragment如何刷新缓存fragment
最近在做一个项目,有一个功能是答题翻页.于是需要实现在这一页的时候就缓存下一页. 刚刚开始我是用 setOnPageChangeListener方法监听,滑到这一页的时候才刷新这一页: public ...
- Codeup
问题 I: 习题5-10 分数序列求和 时间限制: 1 Sec 内存限制: 12 MB提交: 611 解决: 537[提交][状态][讨论版][命题人:外部导入] 题目描述 有如下分数序列 求出次 ...
- 复杂的字符串数组解析:{"setting":"简单:10:5,一般:5:10,困难:2:20"},使用split多次截取
"[0,{"id":563,"name":"测试题1","dscr":null,"picId&quo ...
- eclipse配置环境变量 (特别是输入javac无显示问题)
下载JDK:http://www.oracle.com/technetwork/java/javase/downloads/index.html 最近win10恢复了一下系统,重新给eclipse配一 ...
- 如何在 Flickr 上找到又酷,又有趣,且版权自由的照片?
[编者按]本文作者为 Alex Walker,主要介绍在 Flickr 上进行照片搜索时的一些技巧.本文系国内 ITOM 管理平台 OneAPM 编译呈现. 我们一直都在寻找新奇的,与众不同的设计.图 ...
- 商家服务无法上架提示没有授权信息解决FAQ
1.地址授权:https://openauth.alipay.com/oauth2/appToAppAuth.htm?app_id=2018032002416255&redirect_uri= ...
- 四、Tableau如何设置数据格式
一.要求 ‘销售额’:K为单位 ‘利润’: M为单位,负值用括号括起来,但是正值 ‘利润率’:带百分号,负值用括号括起来仍然时负值 二.解决方案 1.‘销售额’:m为单位 2.‘利润’: ...
- 用好lua+unity,让性能飞起来——lua与c#交互篇
前言 在看了uwa之前发布的<Unity项目常见Lua解决方案性能比较>,决定动手写一篇关于lua+unity方案的性能优化文. 整合lua是目前最强大的unity热更新方案,毕竟这是唯一 ...
- ELK收集tomcat状态日志
1.先查看之前的状态日志输出格式:在logs/catalina.out这个文件中 最上面的日志格式我们可能不太习惯使用,所以能输出下面的格式是最好的,当然需要我们自定义日志格式,接下来看看如何修改 2 ...
- LeetCode算法题-Longest Palindrome(五种解法)
这是悦乐书的第220次更新,第232篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第87题(顺位题号是409).给定一个由小写或大写字母组成的字符串,找到可以用这些字母构 ...