《Redis 设计与实现》的学习

《第一部分:内部数据结构》

redis支持的数据结构:列表list,哈希hash,集合set,有序集合zset,string。

SDS

简单动态字符串:SDS (simple dynamic string) ,简单动态字符串 是redis 底层所使用的字符串表示它被用在几乎所有的redis 模块中。

SDS 在redis 中的主要作用有两个:

1.实现字符串对象(String Object)

2.在redis 程序内部用作char * 类型的替代品。

redis 是一个键值对数据库,数据库的值可以是字符串,集合,列表,哈希,有序集合等多种类型的对象,而数据库的键则总是字符串对象。

对于那么包含字符串值的字符串对象来说,每个字符串对象都包含一个sds 值。

Note:包含字符串值的字符串对象。一个字符串对象除了可以保存字符串值之外,还可以保存long类型的值,所以说:当字符串对象保存的是字符串,它包含的才是sds值,否则的话,它就是一个long类型的值。

启动redis-server
启动 redis-cli

示例:

设置单个值
set person "i love you 577"
get person
设置值是一个集合对象:
sadd nosql "redis" "mongodb" "neo4j"
smembers nosql  

将sds 代替C默认的char * 类型:

因为char * 类型功能单一,抽象层次低,并且不能高效的支持一些redis 常用的操作[比如追加操作和长度计算操作],所以redis 用sds 代替char*。

重点:在redis 中,客户端传入服务器的协议内容,aof 缓存,返回给客户端的回复,都是由sds 类型来保存的。

redis 中的字符串:

在C语言中,字符串可以用一个\0 结尾的char 数据来表示。 ["hello world\0"]
这种简单的字符串表示在大多数情况下都可以满足要求,但是它并不能高效的支持长度计算和追加append 这两种操作:
1.每次计算字符串长度 的复杂度为0(N)。
2.对字符串进行N次追加,必定需要对字符串进行N次内存重新分配。

redis 除了处理C 字符串之外,还需要处理单纯的字节数组,以及服务器协议等内容,所以redis 的字符串表示还应该是 二进制安全的 。所以数据可以是以\0结尾的C字符串,也可以是单纯的字节数组。

redis 使用sds 类型替换了C语言的默认字符串表示:sds 既可以高效的实现追加和长度计算,并且它还是二进制安全的。

//追加字符串
append person "  do you know"

在目前的版本的redis 中,sds_max_prealloc 的值为1024*1024 ,当字符串小宇1MB 的字符串执行追加时,sdsMakeRoomFor 就为它们分配多于所需大小一倍的空间;当字符串大于1MB,那么sdsMakeRoomFor就为他们额外多分配1MB空间。

这种分配策略会浪费内存吗?

执行过append 命令的字符串会带有额外的预分配空间,这些预分配空间不会被释放,除非该字符串所对应的键被删除,或者等到redis关闭后,再次启动时重新载入的字符串对象将不会有预分配空间。因为执行append 命令的字符串数据通常不会很多,占用内存的体积通常不打,所以一般不是问题。

SDS 模块的API :

总结:

redis 的字符串表示为sds ,而不是C字符串char*。

对比char * ,sds有以下特性:
1.可以高效的执行长度计算
2.可以高效的追加操作
3.二进制安全
4.sds 会为追加操作进行优化:加快追加操作的速度,并降低内存分配的次数,代价是多占用一些内存,且内存不会被主动释放。

2.双端链表

链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型。redis 则实现了一个双端链表结构。

双端链表的应用:双端链表它既是redis 列表结构的底层实现之一,还被大量的redis 模块所使用,用于构建其他功能。

redis 列表使用两种数据结构作为底层实现
1.双端链表
2.压缩列表

因为双端链表占用的内存比压缩列表要多,所以当创建新的列表键时,列表会优先考虑使用压缩列表作为底层实现,并且在需要的时候,才从压缩列表实现转换到双端链表实现。

双端链表被很多redis 内部模块所应用:

1.事务模块使用双端链表来按顺序保存输入的命令
2.服务器模块使用双端链表保存多个客户端
3.订阅、发送模块使用双端链表来保存订阅模式的多个客户端
4.时间模块使用双端链表来保存时间事件
双端链表的实现:由listNode 和 list 两个数据结构构成。
迭代器:redis 为双端链表实现了一个迭代器,这个迭代器可以从两个方向对双端链表进行迭代:
1.沿着节点的next指针前进,从表头向表尾迭代
2.沿着节点的prev指针前进,从表尾向表头迭代
总结:redis 实现了自己的双端链表结构。双端链表主要有两个作用:
1.作为redis 列表类型的底层实现之一。
2.作为通用数据结构,被其他功能模块所使用。

3.字典

字典字典,又称映射或关联数组。 它是一种抽象数据结构,由一集键值对组成,各个键值对的键各不相同,程序可以将新的键值对添加到字典中,或者基于键进行查找,更新或者删除等操作。

字典

的应用:

1.实现数据库键空间
2.用作hash 类型键的其中一种底层实现。

字典

的实现:

1.最简单的就是使用链表或数组,但是这种方式只适用于元素个数不多的情况下
2.要兼顾高效和简单性,可以使用哈希表
3.如果追求更为稳定的性能特征,并且希望高效的实现排序操作的做,则可以使用更为复杂的平衡树。

在众多的可能实现中,redis 选择了高效且实现简单的哈希表作为字典的底层实现。

添加新键值对时发生hash碰撞:在哈希表实现中,当两个不同的键拥有相同的哈希值时,我们称这两个键发生hash碰撞,而哈希表实现必须想办法对碰撞进行处理。字典哈希表所使用的碰撞解决方法被称为链地址法:这种方法使用链表将多个哈希值相同的节点串连在一起,从而解决冲突问题。

总结:1.字典由键值对构成的抽象数据结构
2.redis 中的数据库和哈希值都基于字典来实现
3.redis 字典的底层实现为hash表,每个字典使用两个哈希表,一般情况下只使用0号哈希表,只有在rehash 进行时,才会同时使用0和1 哈希表。
4.rehash 可以扩展或收缩哈希表
5.对hash 表的rehash 是分多次,渐进式的进行的。

4.跳跃表

跳跃表是一种随机化的数据,这种数据结构以有序的方式在层次化的链表中保存元素它的效率可以和平衡树媲美---查找,删除,添加等操作都可以在对数期望时间下完成并且比平衡树实现要简单直观。

跳跃表的作用:实现有序集数据类型跳跃表将指向有序集的score 值和member 域的指针作为元素,并以score值为索引,对有序集元素进行排序。

总结:
1.跳跃表是一种随机化数据结构,它的查找,添加,删除操作都可以在对数期望时间下完成。
2.跳跃表唯一的作用就是作为有序集合类型的底层数据结构。
3.为了适应自身的需求,redis 对跳跃表进行了修改

《第二部分:内存映射数据结构》

内存映射数据结构是一系列经过特殊编码的字节序列,创建他们所消耗的内存通常比作用类似的内部数据结构 要少的多,如果使用得当,内存映射数据结构可以为用户节省大量的内存。

1.整数集合
  整数集合用于有序,无重复的保存多个整数值,它会根据元素的值,自动选择该用什么长度的整数类型来保存元素。
  
  整数集合的应用:
  Intset 是集合键的底层实现之一,如果一个集合:
  1.只保存着整数元素
  2.元素的数量不多
  
  3.Intset 只支持升级,不支持降级。
  4.Intset 是有序的,程序使用二分查找算法来实现查找操作,复杂度为

压缩列表[ZipList]:是由一系列特殊编码的内存块构成的列表,它可以保存字符数组或整数值,他还是哈希键,列表键,有序集合键的底层实现之一。

《第三部分:不同的命令操作不同的对象》

对象处理机制:lpush llen 只能用于列表键,而sadd 和 srandmember 只能用于集合键等。del 和 ttl,type可以用于任何类型的键。

redisObject 是redis 类型系统的核心,数据库中的每个键,值,以及redis本身处理的参数,都表示为这种数据类型。

当执行一个处理数据类型的命令时,redis 执行以下步骤:
1.根据给定key,在数据库字典中查找和它相对应的redisObject ,如果没找到,就返回null。
2.检查redisObject 的type 属性和执行命令所需的类型是否相符,如果不相符,返回类型错误。
3.根据redisObject的encoding 属性所指定的编码,选择合适的操作函数来处理底层的数据结构。
4.返回数据结构的操作结果作为命令的返回值。

总结:
1.redis 使用自己实现的对象机制来实现类型判断,命令多台和基于引用计数的垃圾回收。
2.一种redis 类型的键可以有多种底层实现
3.redis会分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁的为小对象分配内存。

字符串编码:字符串类型分别使用redis_encoding_int 和 redis_encoding_raw 两种编码:
1.redis_encoding_int 使用long 类型来保存long类型值
2.redis_encoding_raw 则使用sdshdr 结构来保存sds,long,double类型的值。

哈希表:redis_hash 是 hset,hlen 等命令的操作对象,它使用redis_encoding_ziplist 和 redis_encoding_ht 两种编码方式。

列表:redis_list 是lpush ,lrange 等命令的操作对象,它使用redis_encoding_ziplist 和redis_encoding_linkedlist 两种方式编码。

当客户端被阻塞之后,脱离阻塞状态三种方式:
1.被动脱离:有其他客户端为造成阻塞的键推入了新元素
2.主动脱离:到达执行阻塞原语时设定的最大阻塞时间
3.强制脱离:客户端强制终止和服务器的链接,或者服务器停机。

redis_zset 有序集是 zadd,zcount 等命令的操作对象,它使用redis_encoding_ziplist 和 redis_encoding_skiplist 两种方式编码。

事务:

redis 通过multi ,discard,exec,watch 四个命令来实现事务功能

事务提供了一种将多个命令打包,然后一次性,按顺序的执行 的机制,并且事务在执行的期间不会主动终端---服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他命令。

事务从开始到执行会经历三个阶段:
1.开始事务
2.命令入队
3.执行事务
 

multi 命令的执行标记着事务的开始。该命令将客户端的redis_multi选项打开,让客户端从非事务状态切换到事务状态。

事务状态下的discard ,multi,watch 命令。

事务的ACID性质:
redis 事务保证了其中的一致性,和隔离性,但并不保证原子性和持久性。

原子性Atomicity: 单个Redis 命令的执行时原子性的,但是redis没有在事务上增加任何维持原子性的机制,所以redis事务的执行并不是原子性的。

一致性Consistency: redis 的一致性问题可以分为三部分:入队错误,执行错误,redis 进程被终结。

   1.入队错误:在命令入队的过程中,如果客户端向服务器发送了错误的命令,比如命令的参数数量不对,那么服务器将向客户端返回一个错误信息,并且将客户端的事务状态设为redis_ditry_exec。当客户端执行exec 命令时,redis 会拒绝执行状态为redis_dirty_exec的事务,并返回失败信息。
 
   注解:带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。

  2.执行错误:如果命令在事务执行的过程中发生错误,比如说:对一个不同类型的key执行了错误的操作,那么redis 只会将错误包含在事务的结果中,这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令,所以它对事务的一致性也没有影响。

  3.redis 进程被终结: 如果redis 服务器进程在执行事务的过程中被其他进程终结,或者管理员强制kill,那么根据redis 所使用持久化的模式,可能会出现
     1.内存模式:如果redis 没有采取任何持久化机制,那么重启之后的数据库总是空白的,所以数据总是一致的。
     2.RDB模式:在执行事务时,redis 不会中断事务去执行保存RDB的工作,只有在事务执行中途被kill ,事务内执行的命令,不管成功了多少,都不会被保存到RDB文件中。恢复数据库需要使用现有的RDB文件,而这个RDB文件的数据保存的是最近一次的数据库快照,所以它的数据可能不是最新的,但只要RDB文件本身没有因为其他问题而出错,那么还原后的数据库就是一致性的。

    3.AOF模式:因为保存AOF文件的工作在后台线程进行,所以即使是在事务执行的中途,保存AOF文件的工作也可以继续进行,因此事务是否被写入并保存到AOF文件,会发生两种情况。

隔离性 isolation: redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。

持久性:因为事务不过是用队列包裹起了一组redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由redis 所使用的持久化模式决定的。

   1.在淡出的内存模式下,事务肯定不持久的。
   2在RDB模式下,服务器可能是在事务执行后,RDB文件更新之前的这段时间失败,所以RDB模式下的redis 事务也是不持久的。
   3.在AOF的总是SYNC 模式下,事务的每条命令在执行成功之后,都会立即调用fsync 或者fdatasync 将事务数据写入到AOF文件。但是这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以命令执行成功到数据保存到硬盘之间,还有一段非常小的间隔,所以这种模式下的事务也是不持久的。

Lua 脚本:lua 脚本功能是redis 2.6 版本的功能,通过内嵌对lua 环境的支持,redis 解决了不能高效的处理cas (check-and-set)命令的缺点,并且可以通过组合多个命令,轻松实现以前不能或者不能高效实现的模式。

初始化lua 环境:

在初始化redis 服务器时,对lua 环境的初始化也会一并进行。

为了让lua 环境符合redis 脚本功能的需求,redis 对lua环境进行了一系列的修改,包括添加函数库,更换随机函数,保护全局变量等。

整个初始化lua环境的步骤:
1.调用lua_open 函数,创建一个新的lua 环境。
2.载入指定的lua函数库,包括:
  1.基础库base lib
  2.表格库 table lib
  3.字符串库 string lib
  4.数学库 math lib
  5.调试库 debug lib
  6.用于处理json 对象的cjson库
  7.在lua 值和C结构(struct) 之间进行转换的struct库
  8.处理messagePack 数据的cmsgpack库
3.屏蔽一些可能对lua 环境产生安全问题的函数,比如loadfile
4.创建一个redis 字典,保存lua 脚本,并在复制replication 脚本时使用。字典的键为sha1校验和,字典的值为lua 脚本。

5.创建一个redis 全局表格到lua 环境,表格中包换 了对各种redis 进行操作的函数,包括:
  1.用于执行redis 命令的redis.call 和redis.pcall 函数
  2.用于发送日志的redis.log 函数,以及相应的日志级别level

6.用redis 自己定义的随机生成函数,替换math表原有的math.random函数和math.randomseed函数,新的函数具有:每次执行lua 脚本时,除非显式的调用math.randomseed,否则math.random 生成的伪随机数序列总是相同的。

7.创建一个队redis 多批量回复进行排序的辅助函数。

8.对lua环境的全局变量进行保护,以免被传入的脚本修改。
 

redis 分别提供了RDB和AOF 两种持久化机制:

1.RDB

RDB 将数据库的快照以二进制的方式保存到磁盘中。
2.AOFAOF 则以协议文本的方式,将所有对数据库进行过写入的命令及其参数记录到AOF 文件,以此达到记录数据库状态的目的。

同步命令到AOF 文件的整个过程可以分为三个阶段:

1.命令传播:redis 将执行完的命令,命令的参数,命令参数个数等信息发送到AOF程序中。
 
2.缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器AOF缓存中。
3.文件写入和保存:AOF 缓存中的内容被写入到AOF文件末尾,如果设定的AOF保存条件被满足的话,fsync 函数或者fdatasync 函数会被调用,将写入的内容真正的保存到磁盘中。

命令传播:当一个redis 客户端需要执行命令时,它通过网络连接,将协议文本发送给redis 服务器。

缓存追加:

  当命令被传播到AOF程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换会原来的协议文本。

缓存追加可以分为三步
  1.接受命令,命令的参数,以及参数的个数,所使用的数据库等信息。
 2.将命令还原成redis 网络通讯协议
 3.将协议文本追加到aof_buf末尾。
AOF 保存模式:redis 目前支持三种AOF 保存模式,他们分别是
AOF_FSYNC_NO

不保存

在这种模式下,每次调用flushAppendOnlyFile 函数,write 都会被执行,但 save 会被忽略掉。

在这种模式下,save 只会在以下任意一种情况中被执行。
1.redis 被关闭
2.AOF 功能被关闭
3.系统的写缓存被刷新(缓存已经被写满,或者定期保存操作被执行)。

总结:这三种情况下的save 操作都会引起redis 主进程阻塞。

性能:写入和保存都由主进程执行,两个操作都会阻塞主进程。

AOF_FSYNC_EVERYSEC

每一秒钟保存一次

在这种情况下,save原则上每隔一秒钟都会执行一次,因为save 操作是由后台子线程调用的,所以不会引起服务器主线程阻塞。[注意:实际和原则略有不同,程序在这种模式下对fsync 或者fdatasync 的调用并不是每秒执行一次,它和调用flushApeendOnlyFile 函数时redis 所处的状态有关]。

每当flushAppendOnlyFile函数被调用时,可能会出现以下四种情况:

1.子线程正在执行save ,并且:

   1.这个save的执行时间未超过2秒,那么程序直接返回,并不执行write 或者新的save。
   2.这个save的执行时间已超过2秒,那么程序执行write,但不执行新的save。因为write
     的写入必须等待子线程先完成save,因此这里write 会比平时阻塞时间更长。

2.子线程没有在执行save ,并且:
   3.上次执行成功save距今不超过1秒,那么程序执行write,但不执行save。
   4.上次执行成功save距今已超过1秒,那么程序执行write 和save 。

总结:如果程序宕机,那么也只会丢失2秒的数据而已。

性能:写入由主进程执行,阻塞主进程。保存由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。

AOF_FSYNC_ALWAYS

每执行一个命令保存一次

在这种模式下,每次执行完一个命令之后,write和save都会被执行。

总结:save 是由redis主进程执行的,所以在save 执行期间,主进程会被阻塞,不能接受命令请求。

性能:写入和保存都由主进程执行,两个操作都会阻塞主进程。

性能总结:

1.不保存:写入和保存都由主进程执行,两个操作都会阻塞主进程。
2.每秒钟保存一次:写入由主进程执行,阻塞主进程。保存由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
3.每执行一个命令保存一次:写入和保存都由主进程执行,两个操作都会阻塞主进程。

总结:因为阻塞操作会让redis 主进程无法持续处理请求,所以,阻塞操作执行的越少,完成的越快,redis 的性能就越好。

安全总结:

1.不保存的操作只会在AOF关闭或redis 关闭时执行,或者由操作系统触发,一般情况下,只需要写入阻塞,所以它的写入性能要比后面两种模式要高。但一旦宕机,那么丢失数据的数量由操作系统的缓存冲洗策略决定。

2.每秒一次,性能上高于3,并且最多丢失2秒的数据,安全性高于1。

3.每执行一个命令保存一次。 安全性最高,性能最差,因为服务器必须阻塞直到命令信息被写入并保存到磁盘后,才能继续处理请求。

AOF 文件的读取和数据还原:

AOF 文件保存了redis 的数据库状态,而文件里面包含的都是符合redis 通信协议格式的命令文本。即只要根据AOF文件里的协议,重新执行一遍里面指示的所有指令,就可以还原redis 的数据库状态了。

redis 读取AOF 文件并还原数据库的详细步骤如下:

1.创建一个不带网络连接的伪客户端(fake client)
2.读取AOF 所保存的文本,并根据内容还原出命令,命令的参数以及命令的个数。
3.根据命令,命令的参数和命令的个数,使用伪客户端执行该命令。
4.执行2和3,直到AOF 文件中的所有命令执行完毕。

结果:[完成第四步之后,AOF 文件所保存的数据库就会被完整的还原出来]。

注意:因为redis 的命令只能在客户端的上下文中被执行,而AOF 还原时所使用的命令来自于AOF文件,而不是网络,所以程序使用了一个没有网络连接的伪客户端来执行命令。伪客户端执行命令的结果,和带网络连接的客户端执行命令的效果完全一样。
 

重点:为了避免对数据的完整性产生影响,在服务器载入数据的过程中,只有和数据库无关的订阅和发布功能可以正常使用,其他命令一律返回错误。

AOF 重写:AOF 文件通过同步redis 服务器所执行的命令,从而实现了数据库状态的记录,但是这种同步方式会造成一个问题:随着运行时间的流失,AOF文件会变得越来越大。为了解决这一问题,redis 需要对AOF文件进行重写:创建一个新的AOF 文件来代替原有的AOF文件,新AOF文件和原有的AOF文件保持的数据库状态完全一致,但新的AOF 文件的体积小宇等于原有AOF文件的体积。

AOF后台重写:redis 将AOF 重写程序放到后台子进程里执行,好处:

1.子进程进行AOF重写期间,主进程可以继续处理命令请求。
2.子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

好处:

1.子进程进行AOF 重写期间,主进程可以继续处理命令请求。
2.子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

问题:使用子进程有一个问题需要解决:因为子进程在进行AOF重写期间,主进程还需要继续处理命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF 文件的数据不一致。

解决方案: redis 增加了一个AOF 重写缓存,这个缓存在fork 出子进程之后开始启用,redis 主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到现有的AOF文件之外,还会追加到这个缓存中。


当子进程在执行AOF 重写时,主进程需要执行以下三个工作:

1.处理命令请求
2.将写命令追加到现有的AOF文件中
3.将写命令追加到AOF重写缓存中

实现了:

1.现有的AOF 功能会继续执行,即使在AOF 重写期间发生停机,也不会有任何数据丢失。

2.所有对数据库进行修改的

总结:
1.AOF文件通过保存所有修改数据库的命令来记录数据库的状态。
2.AOF文件中的所有命令都以redis 通信协议的格式保存。
3.不同的AOF保存模式对数据的安全性,以及redis 的性能有很大的影响。
4.AOF重写的目的是用更小的体积来保存数据库状态,整个重新过程基本上不影响redis主进程处理命令请求。
5.AOF 重写是一个有歧义的名字,实际的重写工作是针对数据库的当前值来进行的,程序既不读写,也不使用原有的AOF文件。
6.AOF可以由用户手动触发,也可以由服务器自动触发。

事件:
   事件是redis 服务器的核心,它处理两项重要的任务:
   1.处理文件事件:在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执行结果返回给客户端。
   2.时间事件:实现服务器常规操作(server cron job)。

文件事件:
   redis 服务器通过多个客户端至今进行多路复用,从而实现高效的命令请求处理:多个客户端通过套接字连接到redis 服务器中,但只有在套接字可以无阻塞的进行读或者写时,服务器才会和这些客户端进行交互。redis 将这类因为对套接字进行多路复用而产生的事件成为文件事件,文件事件可以分为读事件和写事件两类。

读事件:读事件标志着客户端命令请求的发送状态。当一个新的客户端连接到服务器时,服务器会为该客户端绑定读事件,直到客户端断开连接之后,这个读事件才会被移除。
 

读事件在整个网络连接的生命周期内,都会在等待和就绪两种状态之间切换:

1.当客户端只是连接到服务器,并没有向服务器发送命令时,该客户端的读事件就处于等待状态。
2.当客户端给 服务器发送命令请求,并且请求已到达时,该客户端的读事件处于就绪状态。

写事件: 写事件标志着可达护短对命令结果的接收状态。和客户端自始至终都关联着读事件不同,服务器只会在有命令结果要传回给客户端时,才会为客户端关联写事件,并且在命令结果传送完毕之后,客户端和写事件的关联就会被移除。

一个写事件会在两种状态之间切换:
1.当服务器有命令结果需要返回给客户端,但客户端还未能执行无阻塞写,那么事件处于等待状态。
2.
当服务器有命令结果需要返回给客户端,并且客户端可以进行无阻塞写,那么事件处于就绪状态。

写事件:写事件标志着客户端对命令结果的接收状态。

和客户端自始至终都关联着读事件不同,服务器只在有命令结果要传回客户端时,才会为客户端关联写事件,并且在命令结果传送完毕后,客户端和写事件的关联就会被移除。

一个写事件会在两种状态之间切换:
1.当服务器有命令结果需要返回给客户端,但客户端还未能执行无阻塞写,那么写事件处于等待状态。
2.当服务器有命令结果需要返回给客户端,并且客户端可以进行无阻塞写,那么写事件处于就绪状态。

当客户端向服务器发送命令请求,并且请求被接受并执行之后,服务器就需要将保存在缓存内的命令执行结果返回给客户端,这时服务器就会为客户端关联写事件。


重要:在同一次文件事件处理器的调用中,单个客户端只能执行其中一种事件(要么读,要么写,但不能同时执行),当出现读写同时就绪的情况时,事件处理器优先处理读事件。

时间事件:时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务器状态中。

每个时间事件主要由三个属性组成:
1.when:以毫秒格式的UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数。

2.timeProc: 事件处理函数

3.next: 指向下一个时间事件,形成链表。

时间事件应用实例:服务器常规操作。对于持续运行的服务器来说,服务器需要定期对自身资源和状态进行必要的检查和整理,从而让服务器维持在一个健康稳定的状态,这类操作被统称为常规操作(cron job)。

在redis 中,常规操作由redis/serverCron 实现,它主要执行以下操作:

1.更新服务器的各类统计信息,比如时间,内存占用,数据库占用情况等。
2.清理数据库中的过期键值对。
3.对不合理的数据库进行大小调整。
4.关闭和清理连接失效的客户端。
5.尝试进行AOF或RDB持久化操作。
6.如果服务器是主节点的话,对附属节点进行定期同步。
7.如果处于集群模式的话,对集群进行定期同步和连接测试。

redis 将serverCron 作为时间事件来运行,从而确保它每隔一段时间就会自动运行一次,又因为serverCron需要在redis 服务器运行期间一直定期运行,所以它是一个循环时间事件:serverCron 会一直定期执行,直到服务器关闭为止。

redis2.6版本中,程序规定serverCron 每隔10毫秒就会被运行一次。从redis 2.8开始,10毫秒是serverCron 运行的默认间隔,而具体的间隔可以由用户自己调整。

事件的执行与调度:文件事件和时间事件呈合作关系,它们之间包含以下三种属性:

1.一种事件等待另一种事件执行完毕之后,才开始执行,事件之间不会出现抢占。
2.事件处理器先处理文件事件,再执行时间事件(调用serverCron)
3.文件事件的等待事件,由距离到达时间最短的时间事件决定。

总结:

1.redis 的事件分为时间事件和文件事件两类。
2.文件事件分为读事件和写事件两类:读事件实现了命令请求的接收,写事件实现了命令结果的返回。
3.时间事件分为单次执行事件和循环执行事件,服务器常规操作serverCron 就是循环事件。
4.文件事件和时间事件之间是合作关系:一种事件会等待另一种事件完成之后再执行,不会出现抢占情况。
5.时间事件的实际执行时间通常会比预定时间晚一些。

服务器与客户端:

初始化服务器:从启动redis 服务器,到服务器可以接收外来客户端的网络的网络连接的这段时间,redis 需要执行一系列初始化操作。

整个初始化过程可以分为以下六个步骤:
1.初始化服务器全局状态
2.载入配置文件
3.创建daemon 进程
4.初始化服务器功能模块
5.开始事件循环

初始化服务器全局状态:

1.服务器中的所有数据库
2.命令表:在执行命令时,根据字符来查找相应命令的实现函数
3.事件状态
4.服务器的网络连接信息:套接字地址,端口,以及套接字描述符
5.所有已连接客户端的信息
6.Lua 脚本的运行环境及相关选项
7.实现订阅和发布功能所需的数据结构
8.日志log 和查询日志slowlog 的选项和相关信息
9.数据持久化aof 和rdb 的配置和状态
10.服务器配置选项:比如要创建多少个数据库,是否将服务器进程作为daemon 进程来运行,最大连接多少个客户端,压缩结构zip structure 的实体数量等。
11.统计信息:比如键有多少次命令,不命中,服务器的运行时间,内存占用等。


 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值