云社区 博客 博客详情 《三次给你聊清楚Redis》之简单聊聊单机
2.6对象
刚写了redis主要的数据结构:
动态字符串、双端链表、字典、压缩列表、整数集合、跳表等
redis肯定不能直接使用这些数据结构来实现数据库,它用这些数据库建立了一个对象系统,包含:
字符串对象、列表对象、哈希对象、集合对象、有序集合对象
我们可以针对不同的使用场景,为对象设置多种分不同的数据结构实现,从而优化对象在不同场景下的效率。
1)键值对
对于redis的键值对来说:key只有字符串类型,而v可以是各种类型,
我们习惯把“这个键所对应的值是一个列表”表达为这是一个“列表键。
TYPE 命令的实现方式也与此类似, 当我们对一个数据库键执行 TYPE 命令时, 命令返回的结果为数据库键对应的值对象的类型, 而不是键对象的类型:
2)对象
我们看一下redis对象的组成:
通过 encoding
属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 因为 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。
字符串对象
字符串对象的编码可以是 int
、 raw
或者 embstr
。
如果一个字符串对象保存的是整数值, 并且这个整数值可以用 long
类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 ptr
属性里面(将 void*
转换成 long
), 并将字符串对象的编码设置为 int
。
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于 39
字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为 raw
。
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 39
字节, 那么字符串对象将使用 embstr
编码的方式来保存这个字符串值。
embstr
编码是专门用于保存短字符串的一种优化编码方式, 这种编码和 raw
编码一样, 都使用 redisObject
结构和 sdshdr
结构来表示字符串对象,但 raw
编码会调用两次内存分配函数来分别创建 redisObject
结构和 sdshdr
结构,而 embstr
编码则通过调用一次内存分配函数来分配一块连续的空间, 空间中依次包含 redisObject
和 sdshdr
两个结构。
embstr
编码有以下好处:
embstr
编码创建删除字符串对象只需操作一次内存- 因为数据都保存在一块连续的内存, 所以这种编码的字符串对象比
raw
编码字符串对象能更好地利用缓存带来的优势。
3)列表对象
列表对象的编码可以是 ziplist
或者 linkedlist
。
当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist
编码:
- 列表对象保存的所有字符串元素的长度都小于
64
字节; - 列表对象保存的元素数量小于
512
个;
不能满足这两个条件的列表对象需要使用 linkedlist
编码。
4)哈希对象
哈希对象的编码可以是 ziplist
或者 hashtable
。
当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist
编码:
- 哈希对象保存的所有键值对的键和值的字符串长度都小于
64
字节; - 哈希对象保存的键值对数量小于
512
个;
不能满足这两个条件的哈希对象需要使用 hashtable
编码。
5)集合对象
集合对象的编码可以是 intset
或者 hashtable
。
当集合对象可以同时满足以下两个条件时, 对象使用 intset
编码:
- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过
512
个;
不能满足这两个条件的集合对象需要使用 hashtable
编码。
6)有序集合对象
有序集合的编码可以是 ziplist
或者 skiplist
。
当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist
编码:
- 有序集合保存的元素数量小于
128
个; - 有序集合保存的所有元素成员的长度都小于
64
字节;
不能满足以上两个条件的有序集合对象将使用 skiplist
编码。
这里多说两句,各个语言的对象其实都差不多,底层实现也就那几个,比如java中的容器,c++的STL。java的hashset就是一个哈希而已,hashmap就是k带了一个v,而”有序的“Treemap使用了红黑树这种有平衡性的搜索二叉树。
redis的有序集合并没有再采取hash+红黑树的操作,而是把平衡树换成了跳表,实际上性能真的没差多少,甚至有时比红黑树有优势,比如跳表的性能较为平均,红黑树攒了很多次不平衡要调整可能会带来资源需求的一个高峰,再加上跳表实现简单的优点,红黑树真的没什么优势。
并且就算是真的想用一种带平衡性的搜索树,现在竞赛也是用的华人之光发明的SB树。
有序集合的优点就是它的有序操作,比如拿最大最小值,红黑树时间o(logN),而哈希表只能一个一个遍历。缺点在于插入一个值的时间也是o(logN),跳表也是。而哈希表插入数是o(1).
要了解底层和这些优缺点
《三天给你聊清楚redis》第2天看看redis怎么被搞出来的(22036字)
三、单机实现
3.1、数据库概述
redis服务器将所有数据库都保存在redis/redisServer中,数组db存放所有数据库,每一项是一个redisdb结构。dbnum代表数据库数量。
客户端有一个指针指向当前数据库,可以切换,也就是移动指针。
3.1.1键空间
现在稍微介绍一下redisdb结构,它的字典保存了所有键值对
键空间的键也就是数据库的键, 每个键都是一个字符串对象。
键空间的值也就是数据库的值, 每个值可以是字符串对象、列表对象、哈希表对象、集合对象、有序集合对象
所有数据库的操作,添加一个键值对, 删除一个键值对, 获取某个键值对, 等等,都是通过对键空间字典进行操作来实现的。
3.1.2维护
读写键空间的时候,服务器会执行一些额外操作,比如:
- 读一个键后(读操作写操作都要对键读取), 会根据键是否存在, 更新键空间命中(hit)次数或不命中(miss)次数。
- 读取一个键后, 服务器会更新键的 LRU (最后一次使用)时间, 这个值可以用于计算键的闲置时间。
- 如果服务器在读一个键时, 该键已经过期, 服务器会删除这个键, 然后执行其他操作。
- 如果客户使用 WATCH 监视某个键,在对这个键进行修改之后, 会将这个键记为脏(dirty),让事务程序知到这个键被修改
- 服务器每次修改一个键之后, 都会对脏(dirty)键计数器的值增一, 这个计数器会触发服务器的持久化以及复制操作执行
- 如果服务器开启了数据库通知功能, 那么在对键进行修改之后, 服务器将按配置发送相应的数据库通知。
3.1.3时间
用户可以给某个键设置生存时间,过期时间是一个UNIX时间戳,到时间自动删除这个键。
redisdb结构的expires字典保存了所有的键的过期时间,我们称这个字典为过期字典。
3.1.4三种过期键删除策略
1)定时删除:创建一个定时器,到时间立即执行删除操作(对内存友好,因为能保证过期了立马删除,但是对cpu不友好)
2)惰性删除:键过期不管,每次获取键时检查是否过期,过期就删除(对cpu友好,但是只有在使用的时候才可能删除,对内存不友好)
3)定期删除:隔一段时间检查一次(具体算法决定检查多少删多少,需要合理设置)
3.1.5淘汰策略
当Redis占用内存超出最大限制 (maxmemory) 时,可采用如下策略 (maxmemory-policy) ,让Redis淘汰一些数据,以腾出空间继续提供读写服务 :
noeviction: 对可能导致增大内存的命令返回错误 (大多数写命令,DEL除外) ;
volatile-ttl: 在设置了过期时间的key中,选择剩余寿命 (TTL) 最短的key,将其淘汰;
volatile-lru: 在设置了过期时间的key中,选择最少使用的key (RU) ,将其淘汰;
volatile-random: 在设置了过期时间的key中,随机选择一些key,将其淘汰;
allkeys-1Lru: 在所有的key中,选择最少使用的key (LRU) ,将其淘汰;
allkeys-random: 在所有的key中,随机选择一些key,将其淘汰;
3.2、持久化
因为redis是内存数据库,他把数据都存在内存里,所以要想办法实现持久化功能。
3.2.1、RDB
RDB持久化可以手动执行,也可以配置定期执行,可以把某个时间的数据状态保存到RDB文件中,反之,我们可以用RDB文件还原数据库状态。
生成
有两个命令可以生成RDB文件:
- SAVE 命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器,服务器不能接受其他指令。
- BGSAVE 命令由子进程执行保存操作,所以该命令不会阻塞服务器,服务器可以接受其他指令。。
禁止BGSAVE和SAVE同时执行,也就是说执行其中一个就会拒绝另一个,这是为了避免父进程和子进程同时执行两个rdbsave,防止产生竞争条件。
载入
RDB载入工作是服务器启动时自动执行的。
自动保存
用户可以通过save选项设置多个保存条件,服务器状态中会保存所有用 save
选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行 BGSAVE 命令。
比如
save 900 1
save 300 10
满足:服务器在900秒之内被修改至少一次或者300秒内修改至少十次。就会执行BGSAVE。
当服务器启动时,用户可以通过指定配置文件或者传入启动参数来设置save选项,服务器会把条件放到一个结构体里,结构体有一个数组,保存了所有条件。
serverCron函数默认100毫秒检查一次,他会遍历数组依次检查,符合条件就会执行BGSAVE。
RDB文件结构
一个完整 RDB 文件所包含的各个部分:
REDIS,
长度5
字节, 保存着 "REDIS"
五个字符。 通过这五个字符, 可以在载入文件时, 快速检查载入文件是否 RDB 文件。
db_version
,长度 4
字节, 它的值是一个字符串表示的整数, 这个整数记录了 RDB 文件的版本号
databases
部分包含着零个或任意多个数据库, 以及各个数据库中的键值对数据
EOF
常量的长度为 1
字节, 这个常量标志着 RDB 文件正文内容的结束
check_sum
是一个 8
字节长的无符号整数, 保存着一个校验和,以此来检查 RDB 文件是否出错或损坏
我并不想深入探究databases的组成。就是知道
- RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们即可。
3.2.2、AOF
AOF持久化是通过保存服务器执行的命令来记录状态的。还原的时候再执行一遍即可。
功能的实现可以分为命令追加、文件写入、文件同步三个步骤。
当 AOF 持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的 aof_buf
缓冲区的末尾:
Redis 服务器进程就是一个事件循环
循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复,
而时间事件则负责执行像 serverCron
函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到 aof_buf
缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用 flushAppendOnlyFile
函数, 考虑是否需要将 aof_buf
缓冲区中的内容写入和保存到 AOF 文件里面, 这个过程可以用伪代码表示:
flushAppendOnlyFile
函数的行为由服务器配置的 appendfsync
选项的值来决定
值为 always
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件并且同步 AOF 文件, 所以 always
的效率最慢的一个, 但从安全性来说, always
是最安全的, 因为即使出现故障停机, AOF 持久化也只会丢失一个事件循环中所产生的命令数据。
值为 everysec
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 每隔超过一秒就要在子线程中对 AOF 文件进行一次同步: 从效率上来讲, everysec
模式足够快, 并且就算出现故障停机, 数据库也只丢失一秒钟的命令数据。
值为 no
时, 服务器在每个事件循环都要将 aof_buf
缓冲区中的所有内容写入到 AOF 文件, 至于何时对 AOF 文件进行同步, 则由操作系统控制。
因为处于 no
模式下的 flushAppendOnlyFile
调用无须执行同步操作, 所以该模式下的 AOF 文件写入速度总是最快的, 不过因为这种模式会在系统缓存中积累一段时间的写入数据, 所以该模式的单次同步时长通常是三种模式中时间最长的: 从平摊操作的角度来看,no
模式和 everysec
模式的效率类似, 当出现故障停机时, 使用 no
模式的服务器将丢失上次同步 AOF 文件之后的所有写命令数据。
重写
AOF持久化是保存了一堆命令来恢复数据库,随着时间流逝,存的会越来越多,如果不加以控制,文件过大可能影响服务器甚至计算机。而且文件过大,恢复时需要时间也太长。
所以redis提供了重写功能,写出的新文件不会包含任何浪费时间的冗余命令。
接下来,我们就介绍重写的原理。
其实重写不会对现有的AOF文件进行读取分析等操作,而是通过当前服务器的状态来实现。
当前列表键list在数据库中的值就为["C", "D", "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"代替前面的6条命令。
- 伪代码表示如下
AOF后台重写
aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程被长时间的阻塞,因为服务器使用单线程来处理命令请求;所以如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求;
Redis不希望AOF重写会造成服务器无法处理请求,所以将AOF重写程序放到子进程(后台)里执行。这样处理的好处是:
1)子进程进行AOF重写期间,主进程可以继续处理命令请求;
2)子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
还有一个问题,可能重写的时候又有新的命令过来,造成信息不对等,所以redis设置了一个缓冲区,重写期间把命令放到重写缓冲区。
总结
AOF重写的目的是为了解决AOF文件体积膨胀的问题,使用更小的体积来保存数据库状态,整个重写过程基本上不影响Redis主进程处理命令请求;
AOF重写其实是一个有歧义的名字,实际上重写工作是针对数据库的当前状态来进行的,重写过程中不会读写、也不适用原来的AOF文件;
AOF可以由用户手动触发,也可以由服务器自动触发。
3.3、事件
redis服务器是一个事件驱动程序。
需要处理两类事件:
1)文件事件:redis是通过套接字与客户端或者其他服务器连接的,而文件事件就是服务器对套接字操作的抽象。
2)时间事件:服务器对一些定时操作的抽象。
3.3.1、文件事件
redis基于reactor模式开发了自己的网络事件处理器,这个处理器被称作文件事件处理器,它使用IO多路复用程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器,当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
文件事件处理器的构成:
I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
I/O 多路复用程序会把所有产生事件的套接字放到一个队列, 以有序(sequentially)、同步(synchronously)、每次一个套接字的方式,向文件事件分派器传送套接字。
I/O 多路复用程序可以监听多个套接字的 ae.h/AE_READABLE
事件和 ae.h/AE_WRITABLE
事件
1)当套接字变得可读时(客户端对套接字执行 write
操作,或者执行 close
操作), 或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行 connect
操作), 套接字产生 AE_READABLE
事件。
2)当套接字变得可写时(客户端对套接字执行 read
操作), 套接字产生 AE_WRITABLE
事件。
如果一个套接字又可读又可写的话, 那么服务器将先读套接字, 后写套接字。
下面介绍各种处理器:
1)连接应答处理器:服务器进行初始化时, 程序会将连接应答处理器和服务器监听套接字的 AE_READABLE
事件关联, 当有客户端连接(connect
)服务器监听套接字的时候, 套接字就会产生 AE_READABLE
事件, 引发连接应答处理器执行, 并执行相应的套接字应答操作。
2)命令请求处理器:客户端连接到服务器后, 服务器会将客户端套接字的 AE_READABLE
事件和命令请求处理器关联起来, 当客户端发送命令请求时, 套接字就会产生 AE_READABLE
事件, 引发命令请求处理器执行, 并执行相应的套接字读入操作
3)命令回复处理器:服务器有命令回复需要传送给客户端, 服务器会将客户端套接字的 AE_WRITABLE
事件和命令回复处理器关联起来, 当客户端准备好接收服务器传回的命令回复时, 就会产生 AE_WRITABLE
事件, 引发命令回复处理器执行, 并执行相应的套接字写入操作。
一次完整的连接事件实例:
3.3.2、时间事件
redis时间事件可以分为两类:定时事件、周期性事件,他们的特点就像他们的名字一样。
而一个时间事件主要有三部分:
id:服务器为时间事件创建的全局唯一id,按时间递增,越新的越大
when:unix时间戳,记录到达时间
timeProc:时间事件处理器,是一个函数,时间事件到达时,服务器就会调用处理器来处理事件。
目前版本的redis只使用周期性事件
来看看实现:
服务器把所有时间事件放在一个链表中,每当时间事件执行器执行时,它就遍历链表,调用相应的事件处理器。
但是注意:链表是无序的,不按when属性来排序,当时间事件执行器运行时,必须遍历整个链表。但是,无序链表并不影响时间事件处理器的性能,因为在目前版本中,redis服务器只使用serverCron一个时间事件,就算在benchmark模式下也只有两个事件,服务器几乎是把链表退化成指针使用了。
3.3.3、事件的调度和执行
文件事件和时间事件之间是合作关系, 服务器会轮流处理这两种事件,对两种事件的处理都是同步、有序、原子地进行的,处理事件的过程中也不会进行抢占,所以时间事件的实际处理时间通常会比设定的到达时间晚一些。
大概流程为:
是否关闭服务器?---->等待文件事件产生---->处理已经产生的文件事件---->处理已经达到的时间事件---->是否关闭服务器?........
3.4、客户端
redis服务器是典型的一对多服务器,通过使用由IO多路复用技术实现的文件事件处理器,redis服务器使用了单线程单进程的方式来处理请求。
3.4.1客户端的属性
- 描述符
客户端状态的 fd
属性记录了客户端正在使用的套接字描述符:
- 伪客户端
fd
值为-1
: 伪客户端处理的命令请求来源于 AOF 文件或者 Lua 脚本, 而不是网络, 所以这种客户端不需要套接字连接。 - 普通客户端
fd
值为大于-1
的整数: 普通客户端使用套接字来与服务器进行通讯, 所以服务器会用fd
属性来记录客户端套接字的描述符。
- 标志
客户端的标志属性 flags
记录了客户端的角色(role), 以及客户端目前所处的状态:
flags
属性的值可以是单个标志:
也可以是多个标志的二进制或, 比如:
每个标志使用一个常量表示, 一部分标志记录了客户端的角色:
- 在主从服务器进行复制操作时, 主服务器会成为从服务器的客户端, 而从服务器也会成为主服务器的客户端。
REDIS_MASTER
标志表示客户端代表的是一个主服务器,REDIS_SLAVE
标志表示客户端代表的是一个从服务器。 REDIS_LUA_CLIENT
标识表示客户端是专门用于处理 Lua 脚本里面包含的 Redis 命令的伪客户端。
另一部分标志记录了客户端目前所处的状态:
以上提到的所有标志都定义在 redis.h
文件里面。
PUBSUB
命令和 SCRIPT LOAD
命令的特殊性
通常情况下, Redis 只会将那些对数据库进行了修改的命令写入到 AOF 文件, 并复制到各个从服务器: 如果一个命令没有对数据库进行任何修改, 那么它就会被认为是只读命令, 这个命令不会被写入到 AOF 文件, 也不会被复制到从服务器。
以上规则适用于绝大部分 Redis 命令, 但 PUBSUB 命令和 SCRIPT_LOAD 命令是其中的例外。
PUBSUB 命令虽然没有修改数据库, 但 PUBSUB 命令向频道的所有订阅者发送消息这一行为带有副作用, 接收到消息的所有客户端的状态都会因为这个命令而改变。 因此, 服务器需要使用 REDIS_FORCE_AOF
标志, 强制将这个命令写入 AOF 文件, 这样在将来载入 AOF 文件时, 服务器就可以再次执行相同的 PUBSUB 命令, 并产生相同的副作用。
SCRIPT_LOAD 命令的与 PUBSUB 命令类似
3.4.2输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求:
redisClient 实例:
3.4.3命令相关
在服务器将客户端发送的命令请求保存到客户端状态的 querybuf
属性之后, 服务器将对命令请求的内容进行分析, 并将得出的命令参数以及命令参数的个数分别保存到客户端状态的 argv
属性和 argc
属性:
argv
属性是一个数组, 数组中的每个项都是一个字符串对象: 其中 argv[0]
是要执行的命令, 而之后的其他项则是传给命令的参数。
argc
属性则负责记录 argv
数组的长度。
3.3.4实现函数
当服务器从协议内容中分析并得出 argv
属性和 argc
属性的值之后, 服务器将根据项 argv[0]
的值, 在命令表中查找命令所对应的命令实现函数。
(命令表是一个字典,字典的键是一个 SDS 结构, 保存了命令的名字, 字典的值是命令所对应的 redisCommand
结构, 这个结构保存了命令的实现函数、 命令的标志、 命令应该给定的参数个数、 命令的总执行次数和总消耗时长等统计信息。)
3.3.5、输出缓冲区
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面, 每个客户端都有两个输出缓冲区:
- 固定大小的缓冲区用于保存那些长度比较小的回复, 比如
OK
、简短的字符串值、整数值、错误回复,等等。 - 可变大小的缓冲区用于保存那些长度比较大的回复, 比如一个非常长的字符串值, 一个由很多项组成的列表, 一个包含了很多元素的集合, 等等。
3.3.6、其它
客户端状态的 authenticated
属性用于记录客户端是否通过了身份验证,还有几个和时间有关的属性,叙述是一件挺无聊的事情,不再写。
3.4、命令的执行过程
3.4.1发送命令请求
当用户在客户端中键入一个命令请求时, 客户端会将这个命令请求转换成协议格式, 然后通过连接到服务器的套接字, 将协议格式的命令请求发送给服务器。
3.4.2读取命令请求
当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时, 服务器将调用命令请求处理器来执行以下操作:
- 读取套接字中协议格式的命令请求, 并将其保存到客户端状态的输入缓冲区里面。
- 对输入缓冲区中的命令请求进行分析, 提取出命令请求中包含的命令参数, 以及命令参数的个数, 然后分别将参数和参数个数保存到客户端状态的
argv
属性和argc
属性里面。 - 调用命令执行器, 执行客户端指定的命令。
3.4.3命令执行器:查找命令实现
命令执行器要做的第一件事就是根据客户端状态的 argv[0]
参数, 在命令表(command table)中查找参数所指定的命令, 并将找到的命令保存到客户端状态的 cmd
属性里面。
命令表是一个字典, 字典的键是一个个命令名字,比如 "set"
、 "get"
、 "del"
,等等; 而字典的值是一个个 redisCommand
结构, 每个 redisCommand
结构记录了一个 Redis 命令的实现信息。
命令名字的大小写不影响命令表的查找结果
因为命令表使用的是大小写无关的查找算法, 无论输入的命令名字是大写、小写或者混合大小写, 只要命令的名字是正确的, 就能找到相应的 redisCommand 结构。
比如说, 无论用户输入的命令名字是 "SET" 、 "set" 、 "SeT" 又或者 "sEt" , 命令表返回的都是同一个 redisCommand 结构。
3.4.4命令执行器:执行预备操作
到目前为止, 服务器已经将执行命令所需的命令实现函数(保存在客户端状态的 cmd
属性)、参数(保存在客户端状态的 argv
属性)、参数个数(保存在客户端状态的 argc
属性)都收集齐了, 但是在真正执行命令之前, 程序还需要进行一些预备操作, 从而确保命令可以正确、顺利地被执行, 这些操作包括:
- 检查客户端状态的
cmd
指针是否指向NULL
, 如果是的话, 那么说明用户输入的命令名字找不到相应的命令实现, 服务器不再执行后续步骤, 并向客户端返回一个错误。 - 根据客户端
cmd
属性指向的redisCommand
结构的arity
属性, 检查命令请求所给定的参数个数是否正确, 当参数个数不正确时, 不再执行后续步骤, 直接向客户端返回一个错误。 比如说, 如果redisCommand
结构的arity
属性的值为-3
, 那么用户输入的命令参数个数必须大于等于3
个才行。 - 检查客户端是否已经通过了身份验证, 未通过身份验证的客户端只能执行 AUTH 命令, 如果未通过身份验证的客户端试图执行除 AUTH 命令之外的其他命令, 那么服务器将向客户端返回一个错误。
- 如果服务器打开了
maxmemory
功能, 那么在执行命令之前, 先检查服务器的内存占用情况, 并在有需要时进行内存回收, 从而使得接下来的命令可以顺利执行。 如果内存回收失败, 那么不再执行后续步骤, 向客户端返回一个错误。 - 如果服务器上一次执行 BGSAVE 命令时出错, 并且服务器打开了
stop-writes-on-bgsave-error
功能, 而且服务器即将要执行的命令是一个写命令, 那么服务器将拒绝执行这个命令, 并向客户端返回一个错误。 - 如果客户端当前正在用 SUBSCRIBE 命令订阅频道, 或者正在用 PSUBSCRIBE 命令订阅模式, 那么服务器只会执行客户端发来的 SUBSCRIBE 、 PSUBSCRIBE 、 UNSUBSCRIBE 、 PUNSUBSCRIBE 四个命令, 其他别的命令都会被服务器拒绝。
- 如果服务器正在进行数据载入, 那么客户端发送的命令必须带有
l
标识(比如 INFO 、 SHUTDOWN 、 PUBLISH ,等等)才会被服务器执行, 其他别的命令都会被服务器拒绝。 - 如果服务器因为执行 Lua 脚本而超时并进入阻塞状态, 那么服务器只会执行客户端发来的 SHUTDOWN nosave 命令和 SCRIPT KILL 命令, 其他别的命令都会被服务器拒绝。
- 如果客户端正在执行事务, 那么服务器只会执行客户端发来的 EXEC 、 DISCARD 、 MULTI 、 WATCH 四个命令, 其他命令都会被放进事务队列中。
- 如果服务器打开了监视器功能, 那么服务器会将要执行的命令和参数等信息发送给监视器。
当完成了以上预备操作之后, 服务器就可以开始真正执行命令了。
3.4.5命令执行器:调用命令的实现函数
在前面的操作中, 服务器已经将要执行命令的实现保存到了客户端状态的 cmd
属性里面, 并将命令的参数和参数个数分别保存到了客户端状态的 argv
属性和 argc
属性里面, 当服务器决定要执行命令时, 它只要执行以下语句就可以了:
因为执行命令所需的实际参数都已经保存到客户端状态的 argv
属性里面了, 所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。
3.4.6命令执行器:执行后续工作
在执行完实现函数之后, 服务器还需要执行一些后续工作:
- 如果服务器开启了慢查询日志功能, 那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
- 根据刚刚执行命令所耗费的时长, 更新被执行命令的
redisCommand
结构的milliseconds
属性, 并将命令的redisCommand
结构的calls
计数器的值增一。 - 如果服务器开启了 AOF 持久化功能, 那么 AOF 持久化模块会将刚刚执行的命令请求写入到 AOF 缓冲区里面。
- 如果有其他从服务器正在复制当前这个服务器, 那么服务器会将刚刚执行的命令传播给所有从服务器。
当以上操作都执行完了之后, 服务器对于当前命令的执行到此就告一段落了, 之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求了。
3.4.7将命令回复发送给客户端
前面说过, 命令实现函数会将命令回复保存到客户端的输出缓冲区里面, 并为客户端的套接字关联命令回复处理器, 当客户端套接字变为可写状态时, 服务器就会执行命令回复处理器, 将保存在客户端输出缓冲区中的命令回复发送给客户端。
当命令回复发送完毕之后, 回复处理器会清空客户端状态的输出缓冲区, 为处理下一个命令请求做好准备。
3.4.8客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后, 它会将这些回复转换成人类可读的格式, 并打印给用户观看(假设使用的是 Redis 自带的 客户端)
- 点赞
- 收藏
- 关注作者
评论(0)