Redis|基础

01:简单动态字符串SDS

SDS的用途

  • redis 讲简单动态字符串SDS作为默认的字符串。
  • 键值对的键
  • 在其他底层数据结构中用来保存具体数据
  • 用于各种缓冲区

SDS的结构

  • len属性:记录保存字符串长(最后的\0不算入)
  • free属性:记录字符串数组中未使用的字节数量。等于总长度 - len - 1, 1 是\0占用的空间
  • char数组:保存字符串
  • 设计原因:
    • 可以使用c字符串函数
    • 方便 获取字符串长度,扩容等操作

SDS与c字符串的区别

C语言使用的字符串表示形式,不能满足安全性,效率性,功能性。

  • 常数时间内获取字符串长度

    • c字符串需要遍历才能获得长度
    • SDS将长度属相保存在了len属性中中,可以直接获取,时间复杂度为O(1)。len属性由API自动完成,无需手动修改确保获取字符串长度不会成为性能瓶颈。
  • 杜绝缓冲区溢出

    • c字符串的操作,不会做内存检查,可能造成缓冲区溢出。
    • SDS在进行字符串修改时,会先检查free属性,判断现在空间是否够用,如果不够,自动扩容,然后进行字符操作。
  • 减少修改字符串,内存分配次数

    • 每次对c字符串的拼接,拷贝,裁剪操作需要手动调整内存。内存调整比较耗时。

    • SDS通过free属性,字符串的实际内存大于等于保存字符串的大小+1,剩余空间记录在字符数组中未使用的字节数(free属性)。

      扩容规则:当前free属性小于新增加的字符长度,如果剩余空间能保存下新增加的字符串,不扩容。如果不能保存下新字符串,则进行内存的重新分配。重新分配的内存不仅能保存操作后的字符串,还会分配额外空间。

      如果修改后len属性小于1M,将分配2*len + 1的空间一半用于保存字符串,另一半留作日后使用。1 字节用作保存 \0 如果修改后len属性大于等于1M,将额外分配1M未使用空间,总共len+1M+1 len字节用于保存字符串,1M留作日后使用。1 字节用作保存 \0

      通过预留空间,减少内存重新分配次数

  • 惰性内存释放规则:字符串长度减小时不会释放未使用的空间,而是保留不用的空间,留着后续使用。也可以使用API手动释放没用到的空间

  • 二进制安全

    读入获取c字符串的时候,会把空字符(回车,制表符等)当做字符串结尾标志,无法保存空字符串,故而无法保存图片音频等。 redis以二进制的方式读入数据,通过len属性判断字符串是否结束。不会对数据进行过滤,筛选。读入是什么样的,保存是什么样的 。

  • 兼容一部分c字符串函数 以\0为字符串结尾标志,可以兼容c字符串的函数。

02:链表(双向)

链表的用途

链表有高效的节点重排能力,优秀的顺序的节点访问方式,灵活的调整链表长度的能力。

列表键的底层之一是链表。发布于订阅、慢查询、监听器等功能也用到了链表。redis服务器使用链表保存多个客户端的状态信息等。

链表节点的结构

  • 指向前置节点的指针
  • 指向后置节点的指针
  • 指向值得指针

链表的实现

使用list数据结构持有链表。

  • 指向头节点的指针
  • 指向尾节点的指针
  • 记录链表长度的属性
  • 指向节点复制函数的指针
  • 指向释放节点函数的指针
  • 指向节点值对比函数的指针

总结

  • 双端:每个节点有两个指针,所以是双端链表
  • 无环:首位节点没有相互连接
  • 带表头指针和表尾指针。可以常速级别访问头结点和尾节点
  • 带链表长度计数器,可以常数级别获取链表长度
  • 多态:通过void* 类型指针指向保存的值,通过函数指针指向操作函数,所以可以保存各种不同的类型。

03:字典

字典的用途

  • 用来保存键值对,每个键都是独一无二的。
  • redis数据库本身就是一个字典。
  • 哈希键的底层实现。

哈希节点的结构

  • 指向键的指针
  • 值或者指向值得指针
  • 指向下一个哈希节点的指针

哈希表的结构

  • 指向哈希节点指针数组的指针。
  • 记录哈希表大小的属性。也就是哈希表桶的个数。
  • 记录哈希表掩码值的属性。也就是桶的个数-1
  • 记录哈希表节点数量的属性。也就是有多个哈希节点

字典的结构

  • 一个指向一簇操作键值对的函数的指针。
  • 一个指向需要传给函数的可选参数的指针。
  • 一个包含两个指向哈希表的指针的数组。
  • 一个记录rehash进度的属性。

哈希算法

  1. 根据字典对应的哈希函数计算出key的哈希值

    计算哈希值:通过type指向的函数集中的hashFunction计算出key的哈希值

  2. 根据哈希表的掩码值,计算桶的位置。

  3. 将键值对放入桶中(放在链表头)。

解决冲突

  • 不同的键值对如果对应相同的哈希值,则发生冲突。
  • redis的哈希表使用了拉链发解决冲突。
  • 放在链表头

rehash

如果拉链中节点个数过多,会影响哈希表的效率。如果哈希表中桶的个数远远大于哈希节点的个数,会浪费空间。这两种情况需要rehash。

  1. 为备用字典分配桶。
    • 扩展操作:大于等于当前节点个数*2的第一个2的次幂的值。
    • 收缩操作:大于等于当前节点个数的第一个2的次幂的值。
  2. 将ht[0]中的所有键值对rehash到ht[1]中:重新计算键值对的哈希值和索引值。然后放到备用字典上。指针操作,不会进行大范围内存拷贝。
  3. 全部节点转移完成后,释放ht[0],将ht[1]设置为ht[0],然后再ht[1]创建新的空哈希表,备下次使用

触发rehash

  • 负载因子:键值对数量/桶数量
  • 当没有执行BGSAVE或者BGREWRITEAOF,负载因子大于等于1时自动执行扩展
  • 当指向上述函数时,负载因子大于等于等于5时自动执行扩展
  • 当负载因子小于0.1的时候,自动收缩

渐进式rehash

redis的rehash不是一次性完成的,而是分多次,渐进式的完成的,减小服务器压力,防止服务器停止服务

  1. 为ht[1]分配空间,桶的空间
  2. 将rehashidx索引设置为0,表示开始rehash
  3. 每次对字典的访问,会将rehashidx所指的桶上挂的键值对进行rehash,然后rehashidx的值加1
  4. 随着3的不断进行,rehash完成

在rehash过程中,如果对哈希表进行操作,如果有需要,要同时操作所持有的两个哈希表。

04:跳表

跳表的用途

跳表是一种有序的数据结构,通过为链表增加索引节点实现快速访问节点的目的。效率可以和平衡树媲美,实现简单。

有序集合的底层是使用跳表实现的。集群节点中用作内部数据。

redis的跳表保存了成员名和成员名对应的值。

跳表节点的结构

  • 层数组:层数组可以包含多个元素,用来进行快速访问,创建节点时根据幂次定律决定层数组的大小。

    每个元素包含一个前进指针和一个记录前进跨度的整数。

    前进指针:层的元素中含有一个指向表尾方向的前进指针,进项向前遍历。

    记录层元素指针指向的节点与当期节点间的距离,计算排位的时候会用,层数就是排位。

  • 后退指针:指向表头方向的前一个节点,用来反向遍历

  • 分值:double类型的浮点数,决定排位,可以有相同值,相同值的会根据成员大小再排序

  • 成员:具体的对象,不能有相同值。本质是一个指向SDS的指针。

跳表的结构

  • header:指向跳跃表的表头
  • tail:指向跳跃表的表尾
  • length:记录跳跃表的节点数量
  • leval:最高层

05:整数集合

整数集合的用途

整数集合,集合中不包含重复元素,保存整数。

集合的底层实现之一,当集合只包含数值并且数量不多时,使用整数集合作为集合对象的实现。

整数集合的结构

  • encoding:决定数组保存整数的类型,int_16t,int_32t,int_64t
  • length:记录数组的长度
  • contents数组:各个整数在数组中按照从小到大排列,且不包含重复项

升级

将一个新的元素添加到整数集合中,如果新元素的长度大于当前数组元素长度,需要对整数集合升级,来保存新元素

  1. 根据新元素的类型,计算新数组大小,分配空间
  2. 将现有元素转换成新元素类型,放置到正确位置,保持有序性
  3. 将新元素添加到数组中

时间复杂度是O(n)

新元素保存位置:引起升级的元素要么大于现在有元素,要么小于现有元素,所以放在开头或者结尾

升级的优点

  • 提升灵活性:通过升级,可以向int15_t数组添加int_32t的元素
  • 节约内存:自动选择最合适的类型,保存数据。只有需要升级的时候才升级。

降级

不支持

重点

  1. 整数集合是集合键的底层实现之一
  2. 整数集合的底层是数组,有序,无重复的方式保存元素。
  3. 在有需要的时候,会进行升级,但是不会降级。
  4. 升级操作带了灵活性和内存的节约

06:压缩列表

压缩列表的用途

压缩列表是列表和哈希表的底层之一。当列表键只有少量类表项,并且每个列表项是小整数或者短字符串的时候,会使用压缩列表做底层结构,哈希表也是。

目的:节约内存

压缩列表节点的结构

每个节点可以保存一个一个字节数组或者整数数值。

  • previous_entry_length:记录ziplit前一个节点长度,单位是字节。

压缩列表的结构

构成:一系列特殊编码的连续内存块组成的顺序序列数据结构。一个压缩列表可以有任意多个节点,每个节点保存一个整数或者字节数组。

08:对象

对象的用途

redis没有直接使用底层数据结构作为数据对象,而是基于这些数据结构构建了对象系统。包括:字符串,列表,集合,哈希表,有序集合。

用一个对象在不同的场景对应了不同的底层结构,提高了效率。

redis基于对象实现了引用计数的内存回收。节约内存。

对象的结构

  • type属性:记录了对象的类型。键:总是字符串对象。值:字符串对象,列表对象,哈希对象,集合对象,有序集合对象。type 键名:显示值的对象类型
  • encoding属性:决定对象的编码,也就是指明对象所用的底层数据结构。OBJECT ENCODING 键名:返回值的底层数据结构。针对不同场景,对象的长度等属性,决定不同的底层数据结构,提高灵活性。
  • ptr指针:指向具体的数据结构。
  • refcount:引用该对象的数量
  • lru:对象最后一次被访问的时间。

字符串对象

  • type属性为:REDIS_STRING

  • encoding属性有三种:int,raw,embstr。

  • 如果字符串对象是整数值,且可以用long类型表示,就将整数值保存在ptr属性里,encoding 设置为int。

  • 如果字符串长度大于32字节,使用SDS保存,encoding设置为raw。 两次分配内存,一次创建对象的结构,一次创建SDS的结构。

  • 如果字符串长度小于等于32字节,使用embstr编码保存字符串,encoding设置为embstr。 一次分配内存,连续空间,一次保存对象结构和SDS结构。

    好处:将两次分配内存降低为了1次。释放embstr只需要进行一次内存释放。因为存储空间连续,所以能更好的利用缓存的优势。

  • long double 类型表示的浮点数是作为字符串保存的,会先将浮点数转换成字符串,然后保存字符串,取用时转换成浮点数,用完转换为字符串保存。

  • int 编码和 embstr 编码在一定条件下会自动向raw(直接到这个类型)转换:

    int 不再是int。 embstr编码的字符串是只读的,任何修改命令都会转换成raw.

列表对象

  • encoding是:ziplist 或者 linkedlist

  • ziplist:ptr指向压缩列表,每个节点entry保存一个列表元素(字符串对象)

  • linkedlist:ptr指向双端链表,双端链表中记录了指向具体链表的指针以及一些属性。具体链表的每个节点保存一个指向字符串对象的指针和指向前驱和后继的指针。

    对象底层是链表,链表保存的是字符串对象,嵌套结构。只有字符串对象能被嵌套包含。

  • 编码转换:当所有字符串元素的长度都小于64字节且类表元素数量小于512个的时候使用ziplist。否则自动转成linkedlist,使用链表存储。

    可以在配置文件修改:list-max-ziplist-valua和list-max-ziplist-entries。

哈希对象

  • encoding是:ziplist或者hashtable

  • ziplist:ptr指向了一个压缩队列。一次将一对键和值压入压缩队列。键和值紧挨在一起,键在前,值在后。

    先添加的在表头方向,后添加的在表尾方向。

  • hastable:ptr指向一个字典。一个字典有两个哈希表以及其他一些属性。哈希表有一个指向桶的指针以及其他属性。桶里保存了指向键值对的指针。字典的节点有一个指向键的指针,一个指向值的指针,一个指向下一个键值对的指针。

  • 编码转换:当所有的键和值的长度都小于64字节且键值对数量小于512个的时候使用ziplist。否则自动转成hashtable编码,使用字典存储。 可以在配置文件修改:hash-max-ziplist-valua和hash-max-ziplist-entries

集合对象

  • encoding是:intset 或者 hashtable

  • intset:ptr指向一个整数集合。整数集合中元素有序,无重复。

  • hashtable:ptr指向一个字典,键是key,value为NULL

  • 编码转换:所有对象都是整数,且元素个数小于等于512个使用intset。否则自动转换成字典。

    元素个数的上限值可以用配置文件的:set-max-intest-entries 选项修改。

有序集合

  • encoding是:ziplist 或者 skiplist。

  • ziplist:ptr指向一个压缩列表。每个元素元素使用两个紧挨着的节点保存,第一个节点保存成员,第二个节点保存分值,按分值大小排序。

  • skiplist:ptr指向包含指向一个字典的指针和指向一个跳表的指针的数据结构。

    跳表中保存了指向名称和分值的指针。可以快速的进行范围操作。

    字典保存了成员到分值的映射,常数级别查找到成员的分值。

    跳表和字典会通过指针共享成员和分值,不会产生重复的成员和分值,不来浪费额外空间。 使用两种结构的优点:即能快速查找成员的分值,又能快速的根据分值进行遍历。

  • 使用量中结构的优点 提高效率:机能快速查找分值,又能快速进行条件遍历 编码转换:有序集合元素数量小于128且各个元素长度小于64字节,使用ziplist编码。否则使用skiplist编码。 可以通过修改配置文件中的:zset-max-ziplist-entries和zset-max-ziplist-value 改变自动转换的条件。

类型检查与命令多态

  • 类型检查:命令执行前先检查该命令能否用在该类型上。然后在选择执不执行该命令。通过对象的type属性进行检查

  • 多态:根据指针指向的底层结构,选择正确的函数来完成命令。

    多态检查

内存回收

  • 使用引用计数进行对象管理。初始为1,被新程序使用+1,不再被一个程序使用-1,为0时释放对象空间。

对象共享

  • 只对整数值的字符串对象进行共享。目的是节约内存。
  • redis会在初始化服务器的时候,默认创建 0 - 9999的字符串对象(int类型)以供后续共享。可以通过修改文件改变初始化的数量。
  • 这些共享对象会被服务器程序持有,引用计数最低为1。不会删除。
  • 不共享字符串对象是为了方式因为比较共享对象是否相等浪费大量时间。整数比较O(1),字符串比较o(n)。

对象的空转时长

  • 对象最后一次被命令访问的时间。
  • 可以通过OBJECT IDLETIME 键名 打印空转时间,该命令不会更新空转时长。
  • 如果服务器打开了maxmemory选项,并且使用了对应内存回收算法,空转时间较长的对象会先被释放

重点

  1. 每个键值对的键和值都是一个对象。最底层都是字符串对象。
  2. 5种对象:字符串,列表,哈希,集合,有序集合。六种底层结构:字符串,链表,字典,整数集合,跳表,压缩列表。
  3. 服务器在指向命令前会检查给定键的类型能否执行指定命令
  4. 引用计数回收机制
  5. 会共享值为 0 - 9999 的字符串对象
  6. 记录空转时长。

09:redis数据库的实现

服务器 redis Server 的结构

  • saveparams:指向保持备份条件的数组

  • dirty计数器:记录上一次备份到现在执行了说少次修改操作。

  • lastSave属性:记录了上次备份的时间。

  • resid serve 服务器结构体中有一个指向db数组的指针,db数组中包含若干个数据库,默认初始化成16个。可以通过database选项修改创建数据库数量

    服务器结构

数据库 redisDB 的结构

  • 一个指向存储键值对的字典的指针。
  • 一个指向存储键过期时间的字典的指针。

切换数据库

  • 服务器内部保存了客户端对象。客户端对象中,有一个指向数据库(db数组的一个元素)的指针,操作数据库的命令是发送给指向数据库的。
  • 默认情况下,目标数据库是0号数据库。可以通过SEKECT x 切换到x号数据库。select命令原理:修改客户端对象的db指针,让它指向目标数据库。

数据库键空间

  • 每个数据库都是用一个redisDb结构表示,其中的dict指针指向一个字典,称为键空间。

    键空间中键就是数据库的键,是一个字符串对象。

    键空间的值是数据库的值,可以是字符串对象,列表对象,集合对象,哈希表对象,有序结合对象。

  • 添加新键:将一个新的键值对添加到字典里,键为字符串对象,值为任意对象

  • 删除键:将一个键值对在键空间里删除

  • 更新键:将键空间里,键对应的值进行更新

  • 对键取值:取出键空间中键对应的值

  • 每次读写后,会更新键对象的LRU属性。等

设置键的生存时间/过期时间

  • expire 时间:过多少秒后到期, pexpire 时间:过多少毫秒后到期 expireat 时间秒:在什么时间过期,精度为秒 pexpireat 时间秒:在什么时候过期,精度为毫秒 TTL 键值 :还有多少秒过期 PTTL 键` :还有多少毫秒过期
  • 设置过期时间: EXPIRE key 数值:多少秒后key过期 PEXPIRE key 数值:多少毫秒后key过期 EXPIREAT key 数值:到达时间时,key期 PEXPIREAT key 数值:到达时间时,key期 底层都是使用PEXPIREAT 函数实现的.
  • 保存过期时间: 数据库的expires字典保存了各个键的过期时间成为过期字典 过期字典的键:一个指针,指向键空间字典的一个键对象。 过期字典的值:一个longlong型的整数,保存了过期的时间。
  • 移除过期时间: PERSIST key : 移除 键的过期时间 在过期时间字典中,删除与key的关联,相当于删除了键值对
  • 计算剩余生存时间 TTL key:返回key的剩余时间,秒 PTTL key : 返回key的剩余时间,单位毫秒 底层:过期是时间-当前时间的差值
  • 键过期判定
    1. 先检查是否存在过期字典,如果是,获取过期时间。
    2. 在检查过期时间是否大于当前时间,如果是,则没过期,如果不是,则过期。

redis删除键的策略

  • 惰性删除策略和定期删除策略结合 惰性删除由expireIfNeeded函数实现,每次读写数据库之前会调用该函数,如果过期,就从数据库中删除,如果未过期,正常执行后续命令。

    定期删除由activeExpireCycle函数实现,周期性执行该函数,每次处理一部分数据,从每个被处理的数据库的expires字典中随机抽查一部分键检查过期时间,过期就删除

AOF、RDB 和复制功能对过期键的处理

  • 生存RDB文件:执行 SAVE 或者 BGSAVE命令时,会生成RDB文件。 生成RDB过程中对键进行检查,确保不会保存到RDB中 过期键不会对RDB文件造成影响
  • 载入RDB文件:redis在启动时,如果开启了RDB功能,会自动载入RDB文件 主服务器载入RDB:对键进行检查,未过期的载入,过期的忽略 从服务器载入RDB:不论是否过期,全部载入数据库。不过主从服务器同步的时候,从服务器数据被清空,过期键不会有影响。
  • AOF文件写入 如果数据库中的某个键过期了,但是没有删除,对AOF文件没有任何影响。 当过期键被惰性删除或者定期删除后,会在AOF文件追加一条DEL命令,显示的删除该键
  • AOF重写:在执行AOF重写的过程中,会对键进行检查,过期键不会保持到重写的AOF中。 过期键不会对AOF重写造成影响。
  • 复制 主服务器在删除一个过期键后,会显示的向所有从服务器发送DEL命令,告诉从服务器删除过期键 当从服务器在执行客户端命令的时候,遇到过期键不会删除,而是继续返回键值(就是这样)。从服务器只有在收到主服务器的DEL命令后,才会删除过期键。

数据库通知

  • 通知功能:客户端可以订阅给定的频道或模式,来获知数据库中键的变化,以及数据库命令的执行情况 键空间通知:某个键执行了什么命令 键事件通知:某个命令被什么键执行了 通过 notify-keyspace-events选项可以更改服务器发送通知的类型
  • 发送通知:通过调用notifyKeyspaceEvent函数实现.当需要发送通知的时候,调用该函数 发送通知的实现:
    1. 检查通知类型是不是服务器允许的发送类型
    2. 检测服务器是否允许发送键通知
    3. 构建并发送事件

10:RDB持久化

综述

  • redis是内存数据库,程序退出后数据库也会消失
  • 持久化:将数据库中的数据保存到磁盘中,避免丢失
  • RDB文件:一个经过压缩的二进制文件,保存了服务器状态,可以通过该文件还原生成RDB文件是数据库的状态

RDB文件的创建和载入

  • 生成RDB文件的命令:SAVE 或者 BGSAVE

    SAVE命令执行时服务器状态:阻塞redis服务器进程,直到RDB文件创建完成,创建期间不提供服务

    BGSAVE命令执行时服务器的状态:不会阻塞服务器进程,会生成子进程,由子进程负责创建RDB文件,主进程继续处理命令。

    BGSAVE期间,会拒绝执行SAVE,BGSAVE命令,延迟执行BGREWRITEAOF命令。

    BGREWRITEAOF期间,会拒绝执行BGSAVE。

  • RDB文件载入时服务器状态:服务器启动时自动执行,会阻塞服务器,不提供服务。

    没有专门的载入命令。

    AOF文件的更新频率比RDB高,所以:如果开启了AOF持久化,优先使用AOF文件进行数据还原。只有AOF关闭的时候,才会RDB。

自动间隔性保存

  • 设定save选项,定时自动备份数据库,执行BGSAVE命令。

  • 设置保存条件

    save属性可以通过指定配置文件或载入启动参数更改默认值

    save结构数组和redisServer结构中的saveparems关联

  • 两个重要属性

    dirty计数器:记录上次执行SAVE或者BGSAVE命令之后,服务器进行了多少次修改操作,记录在redis Serve 的结构提里面,服务器级别的

    lastsave属性:记录了上次成功生成RDB文件的时间。记录在redis Serve 的结构提里面,服务器级别的

  • 检查条件是否满足

    周期性,每100毫秒执行一次serveCreon函数。对服务器进行维护,检查save选项中的条件是否有被满足,如果有,执行BGSAVE命令.

RDB文件结构

一个经过压缩的二进制文件。由多部分构成。

  • REDIS部分:表示这是一个RDB文件。

  • db_version部分:记录数据库的版本号。

  • database部分:

    一个数据库对应一个database,多个database挨着排列。

    selectdb 和 db_number部分:告诉服务器要操作那个数据库。

    key_value_pair部分:记录了具体值的类型、(过期时间)键、值。不同类型的键值对,会以不同形式保存。

  • check_sum部分:记录校验和。

11:AOF持久化

综述

和RDB保存数据库的所有键值对不同,AOF持久化是通过保存服务器所执行的写命令记录数据库状态的。服务器启动时,可以通过载入和执行AOF文件中的命令,恢复关闭之前的状态。

AOF持久化实现

  • 命令追加。当aof持久化打开的时候,会将修改命令依次放入服务器的aof_buf缓冲区。

  • 文件的写入和同步:

    在每次结束事件循环执之前,调用flushAppendOnlyFile函数。来决定同步方式。行为由服务器配置的appendfsync选项决定。

    写入:放到系统的内存缓冲区。同步:写到磁盘。

    always:将aof_buf缓冲区的所有内容写入并同步到AOF文件。效率慢,安全性高。

    everysec:默认值,将缓冲区的所有内容写入AOF文件,如果距离上次同步超过一分钟,才进行同步。

    no:写入AOF,但是不同步,什么时候同步有操作系统决定。

AOF文件载入与数据还原

  • 执行AOF中的所有命令,服务器就能恢复
    1. 创建伪客户端
    2. 从AOF文件中读入命令
    3. 伪客户端执行命令

AOF重写

  • AOF文件累积所有修改命令,体积会越来越大。为了解决这个问题,提出了AOF从写功能 通过AOF文件生成一个新的,不带冗余数据的AOF文件,体积会小的多。

  • 实质:没有对旧AOF文件进行访问,实际上是通过分析服务器状态实现的。

  • 将多条命令的最终执行结果用一条命令代替(实际上为了避免客户端缓冲区溢出,每条命令最多包含64个元素)

  • 重写是在后台子进程中进行的,这样服务器进程在重写过程中可以继续提供服务。子进程有服务器进程的数据副本,可以避免锁

  • 数据一致性问题:AOF重写过程中,服务器可能处理新的命令,导致服务器当前状态和AOF文件不一致

    解决办法:设置AOF重写缓冲区,在进行重写过程中,会将写命令同时发送到AOF缓冲区和AOF重写缓冲区。

    1. 指行客户端命令
  1. 将命令追加到AOF缓冲区
  2. 将命令追加到AOF重写缓冲区
  3. 重写AOF完成后,向服务器发送信号,将AOF重写缓冲区的所有内容写入新的AOF文件中,保证和服务器状态一致。(阻塞)
  4. 对新的AOF文件进行改名,覆盖旧的AOF文件,完成重写这个过程,服务器是被阻塞的,无法提供服务。

保证了AOF缓冲区的内容被写入和同步到AOF文件。重写AOF时的所有命令保存在了AOF重写缓冲区。

备份时的冲突:

BGSAVE期间,会拒绝执行SAVE,BGSAVE命令,延迟执行BGREWRITEAOF命令。

BGREWRITEAOF期间,会拒绝执行BGSAVE。

13:客户端的实现

redis 一个服务器可以对应多个客户端。每个客户端可以向服务器发送命令请求,服务器接受命令并向客户端返回结果。

redis 通过io多路复用技术实现文件事件的处理。使用单进程的方式与多个客户端进行网络通信。

客户redis clienet端结构

  • fd属性:记录套接字的描述符
  • name指针:指向保持名字的字符串对象
  • flag属性:记录客户端的角色。
  • querybuf指针:指向输入缓冲区(一个sds对象),最大1G
  • 输出缓冲区:分为两种,一种固定大小的用来回复OK这种,最大18K。一种可变大小的,用来回复其他的
  • 客户端链接时间

客户端的创建与关闭

  • 普通客户端的创建

    当客户端连接服务器的时候,服务器会创建一个redis client 的对象,保存在clients 链表中。

  • 普通客户端的关闭

    关闭原因:

    1. 客户端程序退出或者被杀死。
    2. 客户端发送了不符合协议的命令请求
    3. 空转时间超市
    4. 发送的命令大于缓冲区大小(默认1G)
    5. 回复给客户端的内容大于缓冲区大小
  • Lua脚本伪客户端

    放在lua_client属性中

  • AOF文件的伪客户端

    需要时创建,完成时关闭。

14:服务器的实现

服务器负责与多个客户端建立网络链接,处理客户端请求。维护数据库正常运转。

命令请求的执行过程

  • 发送命令请求:

    用户输入请求->客户端转换请求到协议格式->发送到服务器

  • 读取命令请求

    1. 将套接字中的请求保持到客户端的输入缓冲区中
    2. 分析输入缓冲区中的命令,提取相关参数放入argv 和 argc中。
    3. 执行命令

serveCron函数

  • 每100毫米自动执行一次,管理服务器资源,维护服务器运转。

  • 更新服务时间缓存:更服务器保持当前时间的属性值。

    为了减少系统调用,服务器将当前时间保存了下来。

    当需要不太精确的当前时间时候,直接从服务器获得,减少系统调用。打印日志,决定是否执行持久化等

    对于高精度的时间,仍需要调用系统时间。设置键的过期时间,添加慢查询日志等

  • 更新LRU时钟:每10m更新一次,不精确。

    LRU时钟用于计算对象的空转时长。

  • 更新服务器每秒执行次数:每100毫秒一次

  • 更新服务器内存峰值:与记录峰值比较,更新为较大的值

  • 处理sigterm信号:

  • 管理服务器资源:对一定数量的客户端进行检查

    1. 客户端空转时间过长,关闭客户端
    2. 缓冲区溢出,关闭缓冲区
  • 管理数据库资源:检查一定数量的数据库,删除数据库中的过期键(过期键的定期删除),收缩字典(不是自动的吗)等。

  • 执行被延迟的BGREWRITEAOF:BGSAVE期间收到的BGREWRITEAOF命令会被延迟执行。会定期检查能否开始执行BGREWRITEAOF命令。

  • 检查持久化操作的运行状态:当发现正在执行bgSave或bgRewriteAof,会检查是否执行完毕,如果完毕进行后续操作。

    如果没有执行持久化,则检擦是否有需要的持久化操作需要进行

    1. 是否需要新的BGSAVE

    2. 是否需要重写AOF

    3. 是否有推迟的重写的AOF

      是否需要持久化

  • 将AOF缓冲区的内容写入AOF文件。

  • 关闭异常客户端。

初始化服务器

  • 初始化服务器的状态结构:运行id,配置文件路径,运行架构,端口号等
  • 载入配置项:
  • 初始化服务器的数据结构:
  • 还原数据库状态:载入AOF,如果没有AOF则载入RDB。
  • 执行循环事件。

15:主从复制

SLAVEOF 命令或设置slaveof选项实现复制。SLAVEOF IP 端口号 主服务器:被复制的服务器 从服务器:复制别的服务器

旧版本的主从复制原理

  • 同步

    用于从服务器的状态更新到主服务器的状态。当指向SLAVE命令时,需要首先执行同步操作。

    1. 从发送SYNC命令
    2. 主服务器收到SYNC命令后执行BGSAVE命令,生成RDB文件,同时用缓冲区记录下开始执行的所有写命令
    3. 主服务器将RDB文件发送给从服务器,从服务器载入RDB文件
    4. 主服务器将记录在缓区的写命令发送给从服务器,从服务器更新至和主服务器状态一致

    主从同步

  • 同步命令传播:主服务器被修改时,导致从服务器也被修改,保持主从服务器一致。主服务器将自己的写命令发送给从服务器,从服务器执行该命令,达到主从一致。

  • 缺陷:初次复制能很好的完成任务,断线复制必须生产完整的RDB文件,虽然能恢复到主从一致,但是效率低。

    SYNC命令时非常耗费资源的:主服务器生成RDB文件,消耗CPU和I/O,主发送RDB文件,消耗网络带宽从载入RDB文件期间无法提供服务。

新版主从复制原理

  • 主要解决断线重连的效率低问题。PSYNC名代替SYNC命令

  • 完整同步。用于初次复制,和SYNC命令一致。

  • 部分重同步:用于断线后的重复制。恢复重连后,主服务器将断线期间的所有写命令发送给从服务器,从服务器接收命令后执行,从而保持主从一致性。

  • 优点:消耗的资源比SYNC少,速度快

    主从部分同步

部分重同步实现

  • 复制偏移量

    主服务器维持复制偏移量,每次向从服务器传播N个字节命令,就将自己的复制偏移量+N。

    从服务器维持复制偏移量,每次收到主服务的N个字节数据时,就将自己的复制偏移量+N。

    通过对比主从服务器的偏移量,可以知道服务器装填是否一致

  • 复制积压缓冲区

    主服务器维护的固定长度的先进先出的队列,默认1M。

    主服务器进行命令传播时,同时会将命令入队到复制积压缓冲区中,因此,里面保存着一部分最近传播的写命令和写命令的偏移量。

    大小设置:修改配置文件,大小一般为2倍的平均每秒写命令字节*平均断线时长。

  • 重连后的同步

  1. 从服务器发送PSYNC命令将自己的复制偏移量告诉主服务器
  2. 如果偏移量 offset 之后的数据扔保留在复制积压缓冲区,主服务器进行部分重同步操作
  3. 如过 offset 之后的数据已经不在复制积压缓冲区中,则进行完整重同步操作
  • 服务器运行ID

    每个服务器,无论主从,都有自己的运行ID 启动服务器时自动生成,40位随机十六进制数 从服务器初次复制时,将主服务器自己的ID发送给从服务器,从服务器保存 从服务器重连后,向主服务器发送保存的ID ,主服务器进行判段,和自己一样,则是重连的从服务器先尝试执行部分重同步操作,不一样则是新的,执行完整重同步操作

  • PSYNC命令实现

    完整同步操作:PSYNC ? -1 部分重同步操作:PSYNC <runid><offset> 主服务器根据PSYNC命令来决定是进行完整同步操作还是部分重同步操作

复制的实现

  • 设置主服务器地址和端口号:SLAVEOF <master_ip> <master_port>。异步命令,指向后主服务器返回ok,开始执行复制。

  • 建立套接字连接:从服务器建立一个专门处理复制工作的处理器。主服务器将从服务器看做一个自己的客户端。从服务器可以向主服务器发送命令。

  • 发送ping命令:从服务器向主服务器发送。检查套接字读写是否正常,检查主服务器是否工作正常。

  • 身份验证:通过AUTH命令发送密码。

  • 发送端口信息:从服务器向主服务器发送自己的端口号。主服务器记录该端口号。

  • 同步:主服务器将自己的数据复制从服务器。

    从服务器发送PSYNC命令,执行后,双方互为客户端。因为主服务器要主动发送同步信息给从服务器,并且日后的同步操作也需向从服务器发送命令。

    主从互为客户端

  • 命令传播:主服务器将自己的写命令发送给从服务器,从服务器接受并执行命令,保持主从一致性。

心跳检测

  • 从服务器每秒一次向主服务器送送心跳检测命令
    1. 检测主从服务器的网络连接状态:主服务器超过1m没收到重服务器的REPLCONF ACK命令,就认为从服务器掉线。
    2. 复制实现min-slaves配置选项:当从服务器数量过少,或者延迟过长时,主服务器拒绝写入。如果从服务器出现异常,主服务器拒绝写入操作。
    3. 检测操作数据库命令丢失。从服务器会发送给主服务器自己的复制偏移量,如果和主服务器不一致,在复制积压缓冲区中找到丢失命令,然后补发。 旧版本不会处理这种情况,造成主从不一致。

16:哨兵机制

目的:监视主从服务器,主服务器发生故障时,完成主从转换。

主服务器下线时长超过设定时长,自动将某个从服务器升级为主服务器。重新上线后是从服务器

启动并初始化哨兵

  • 命令:redis-sentinel/paht/to/your/sentinel.conf或者redis-server/path/to/your/sentinel.conf --sentinel
  • 执行步骤
    1. 初始化一个redis服务器 sentinel本质上是一个运行在特殊模式下的redis服务器。 不会载入RDB或者AOF文件
    2. 将初始化的redis的代码替换成Sentinel专用代码 更改端口号, 更改命令列表
    3. 初始化sentinel
    4. 根据配置文件,初始化监听的主服务器列表
    5. 创建连接主服务器的网络连接 创建连接被监视主服务器的网络连接。Sentinel将成为主服务器的客户端。 命令连接:向主服务器发送命令,并接收回复命令 订阅连接:订阅主服务器的 sentinel:hello频道 为什么两个?

获取主服务器信息

  • 每10秒一次,通过命令连接向主服务器发送INFO命令。分析返回的INFO命令获取主服务器信息。
    1. 获取主服务器的ID以及角色
    2. 获取主服务器下的所有从服务器信息,并创建从服务器的命令连接订阅模式
    3. 可以自动根据主服务器获取从服务器信息

获取从服务器信息

  • 根据从服务器信息,sentinel创建和从服务器的命令连接和订阅连接。
  • 每10秒一次通过命令连接从服务器发送INFO命令,通过回复分析从服务器状态。

向主/从服务器发送信息

  • 每2秒一次通过命令连接向所有的主/从服务器订阅模式发出消息 。消息包括:
    1. 自己的信息
    2. 自己保存的主服务器信息

接收主/从服务器的频道信息

  • 每个哨兵保存了一个哨兵字典,字典中记录了所有哨兵消息。根绝接收到的频道信息,可以更新哨兵字典。
  • 创建向新哨兵的连接。各个哨兵之间会有命令连接。
  • 通过订阅连接向所有的服务器发送订阅 hello 的命令。该订阅会一直持续到与服务器的连接断开,监视同一个服务器的多个sentinel会收到一个sentinel发送的hello命令,从而更新对应嘱咐其的实例结构。
  • 更新sentinels字典:sentinel 为主服务器创建的字典中,保存了所有监听这个服务器的sientinel的资料。可以根据订阅频道的hello命令进行更新和管理
  • 创建连向其他sentinel的命令连接。当发现新的sentinel时,会和新的sentinel创建命令连接,各自创建一条,分别用于通信。供下线检测。sentinel之间不会创建订阅连接。

检查主观下线状态

  • 哨兵每秒一次向连接的主从服务器,其他哨兵通过连接命令发送ping命令。根据返回的ping命令回复,判断他们是否掉线。
  • 通过配置文件设置下线时长,如果时长内内有收到有效的回复,则认为下线。包括主服务器,从服务器,其他哨兵。
  • 由于配置下线时长的不同,不同的哨兵对其他实例的下线与否的认知不同。

检查客观下线状态

  • 当哨兵主观认为主服务器下线后,会向其他哨兵通过命令连接询问主服务器询问主服务器的状态,如果认为主服务器下线的哨兵数量足够后,则判定主服务器为下线状态,执行故障转移。
    1. 向其他sentinel 发送 SENTINEL is-master-down-by-addr <主服务器ip> <主服务器端口号> <current-epoch> <runid>
    2. 其他sentinel 收到上述命令后,会根据ip 和端口号检测自己对该服务器的态度,并返回给发送者。
    3. 接收返回的信息,获得其他sentinel对主服务器的态度。当同意主服务器下线的sengtinel达到一定数量(可以在配置文件中设置)时,就将主服务器设置为客观下线状态。

选举领头哨兵

  • 当一个主服务器被判定为下线状态时,监视这个服务器的各个sentinel会进行协作,选出一个领头Sentinel,并由领头sentinel对下线的主服务器进行故障转移。
    1. 任何一个在线的sentinel都有成为领头的资格
    2. 规则是先到先得,将发送命令的sentinel设置为局部领头。如果支持者超过半数,则为领头sentinle。如果一轮下来没有选出,则过一段时间再次选择。

故障转移

  • 领头sentine负责进行故障转移。
    1. 在从服务器中挑选一个性能良好作为主服务器。
    2. 将下线的主服务器的从服务器改为新的主服务器。sentinel向服务器发送SLAVEOF 命令。
    3. 将下线的主服务器设置为新的主服务器的从服务器,这个旧的主服务器重新上线时,sentinel向他发送SLAVEOF 命令,使他成为新的主服务器的从服务器。

17:集群

若干个主从节点构成一个集群,集群能够进行分发指令到对应的节点。

节点

  • 一个集群有若干个节点(主从服务器)构成。刚开始的时候,各个节点相互独立,自已属于一个集群。

    通过客户端向服务器发送命令 CLUSTER MEET <IP> <PORT>后,服务器与指定的地址的另一个节点进行握手,成功后,服务器会将地址指向的节点添加到自己所在的集群中。

  • 启动节点

    节点就是一个运行在集群模式下的redis服务器,服务器启动的时候,会根据cluster-enabled的配置项来决定是否开启集群模式。

    是否开启集群

  • 保存集群的消息

    每个服务器有一个clusterState结构,该结构中保存了集群的所有信息。

    clusterNode结构保存了一个节点在集群中的功能。

    clusterState结构中保存了自己的clusterNode,还保持了一个存储clusterNode的字典。字典的键是服务器的标识符,值时clusterNode。

    clusterState结构中保存了一个数组,记录个各个槽有那个节点处理。

    一个键与槽关系的跳表。

    集群节点保存信息

  • CLUSTER MEET的实现

    A节点与B节点进行握手,来确认彼此的存在。

    1. A节点为B节点创建一个clusterNode的结构,并将该结构添加到自己的clusterState.node字典中。
    2. A节点根据地址向B节点发送命令。
    3. B节点收到A节点的消息后,创建一个clusterNode的结构,并将该结构添加到自己的clusterState.node字典中。
    4. B节点向A节点返回一条PONG消息。
    5. A节点接收到PONG消息后,向B返回一条PING消息。
    6. B节点接受得到PING消息后,握手完成
    7. 之后A节点会将B节点的消息通知给集群中的其他节点,让其他节点也与B节点握手,B节点被集群中的所有节点认识。

节点数据库的实现

  • 在存储键值对方面与单机数据库一样,但是只使用0号数据库。
  • clusterState结构中保存了还保存一个键与槽关系的跳表。方便的取出某个槽或者某些槽中所有键。

槽指派

集群通过分片的方式保存键值对。集群的整个数据库被分为16384个槽,每个键值对都属于其中一个槽。集群中的每个节点可以处理一个或者多个槽。

当所有槽都有节点处理时,集群属于上线状态,如果有任何一个槽没有节点处理,则集权属于下线状态。

用客户端向服务器发送cluster addslots 槽数字可以为该服务器指定一个或者多个槽。

  • 记录节点槽指派的消息

    clusterNode结构的slotsnumslot属性记录了节点负责处理哪些槽。

    slots是一个bitmap,如果为 1,则表示该节点处理这个槽,如果为0,该节点不处理该槽。

    numslot记录该节点处理槽的个数,也就是bitmap中1的个数。

    检查节点是否处理某个槽或者某个槽是否指派给了该节点负责,时间复杂度为O(1).

  • 传播节点的槽指派信息

    每个节点会把自己的槽信息发送给集群中的其他节点,其他节点收到这个消息后,会更新他所保存的该节点的槽指派消息。从而每个节点都知道各个节点处理哪些槽。

  • 记录集群所有槽的指派消息

    clusrerState结构中的sloat数组记录了各个槽指派的节点。

    如果sloat[i] 指向空,则该槽没有节点处理。如果他指向了某个节点,则该槽有对应节点处理。

    添加个这个属性的原因:

    1. 如果只使用node节点的sloat属性记录,那么查看槽 i 是否有节点处理,需要遍历所有的state.node的所有节点,检查节点的sloat数组,时间复杂度时O(N).
    2. 如果把槽信息存储在sloat数组中,只需要访问clusterState.sloat[i]是否为空即可,时间复杂度为O(1)。

    slusterNode结构的slots保存的这个节点处理那个槽。clusterState.sloat保存的各个槽由那个节点处理。

  • CLUSTER ADDSLOTS 命令的实现

该命令接收一个或者多个槽为参数,将这些槽指派给接收该命令的节点。

如果这些槽有被其他节点处理,则改命令报错。

clusrerState结构中的sloat[i] 指向自己的clusterNode,并跟新clusterNode的slot二进制数组。

通过消息将自己处理的槽告知其他节点。

在集群中执行命令

当客户端向集群中的服务器发送命令的时候,服务器会计算该命令应该由那个槽处理,如果是自己,咋处理节点,如果不是自己,则放客户端返回moved错误,并指引客户端到正确的节点。

集群节点接收命令

  • 计算节点处于那个槽

    通过哈希函数和取模计算键属于哪个槽。

  • 判断槽是否由当前节点执行

    如果计算出的sloat[i]指向自己,则处理,如果不是,则通过moved返回sloat[i]所指向的节点对应的地址。

  • moved 错误

    MOVED 槽 ip 端口:告诉客户端该命令对应的槽以及处理该槽的服务器地址。

    客户端收到moved错误后,会根据提供的转向对应节点,重新发送命令,得到正反馈。

    一个客户端会与集群中的多个节点相连,节点转向就是换个套接字发送命令。

    集群客户端并不会打印出moved错误,而是自动转向,单机客户端会报错,他不知道该命令的作用

重新分片

重新分片可以将已经指派个某个的槽改为指派给另一个节点,将槽对应的节点转移到目标节点。

重新分片可以在线进行,执行过程中不需要下线,并且源节点和目标节点可以继续处理命令请求。

  • 重新分片原理

    有个专门管理redis集群的软件叫:redis-trib。他提供了重新分片所需要的命令。他们够同时向源节点和目标节点发送命令来进行重新分片。

    对单个槽重新分片步骤:

    1. 向目标节点发送命令 cluster setslot <槽值> import <源id>命令,让目标节点准备好导入键值对。

    2. 对源节点发送cluster setslot <槽值> migrating <目标id>命令,让源节点准备好迁移槽对应的键值对到目标节点。

    3. 向源节点发送cluster getkeysinslot <槽名> <数量>命令,获得槽中最多数量个的

    4. 对于获得的每个键,向源节点发送migrate <目标ip> <目标端口> <键> 0 <timeout>,将选中的键原子的由源服务器迁移到目标服务器。

    5. 重复执行 3 4 步骤,直到源节点保存的属于槽的所有键值对都迁移到目标服务器。

      槽迁移

    6. 向集群中的任意一个节点发送槽迁移消息,这个消息会传播到整个集群,集群中的所有节点都知道了槽迁移消息。

ASK错误

再进行重新分片过程中,可能处于一部分键值对已经迁出的情况。这时候客户单如果发送一个请求该槽的命令:

  • 源节点先在自己的数据库里查找指定的键,如果找到,就执行客户端命令。

  • 如果没有找到,那个这个键可能迁移到了目标服务器,会给客户端返回个 ASK错误,指引客户端转向正确的槽对应的目标节点,去执行命令。

    ASK错误不会被客户端打印,而是自动的转向目标服务器重新执行命令。

  • **cluster setslot <槽值> import <源id>**命令的实现

    clusrerState结构中的importing_slots_form数组中保存了当前节点正从其他节点导入的槽,数组下标代表槽号。如果这个数组中的某个元素不为空,则代表该元素所对应下标的槽号,正在从这个元素所指向的节点导入。

    槽迁入数组

  • **cluster setslot <槽值> migrating <目标id>**命令的实现

    clusrerState结构中的migrating_slots_to数组中保存了当前节点迁移到其他节点,数组下标代表槽号。如果这个数组中的某个元素不为空,则代表该元素所对应下标的槽号,正在向这个元素所指向的所指向的节点迁出。

    槽迁出数组

  • ASK错误

    节点收到冠以键的命令请求,先检查键所对应的槽是否是自己的,如果不是,则返回moved 错误,如果是则在自己的数据库中查找该键,如果找到,就执行命令。

    如果没找到,则检查自己的迁出槽数组,查看键对应的槽是否正在迁出。如果正在迁移,则向客户端返回ask错误,指导客户端到正在迁入的服务器去查找键(会先发送asking命令)。

  • AKSING命令

    如果客户端向某个节点发送一个关于槽i的命令,而槽i又没有指派给该节点,那么该节点会返回给客户端一个moved错误。但是如果该节点导入槽数组显示该槽正在导入,并且客户端带有asking表示,则执行该命令。

    当客户端收到ask错误。转向目标节点重新执行命令的时候,会先发送一个asking命令,以强制目标服务器执行该命令。

    asking命令生效一次,执行下一个命令后失效。

  • ask错误与moved错误区别

    moved错误:客户端找错了服务器,服务器告诉客户端正确的服务器地址

    ask错误:客户端没找错服务器,但是服务器的这个槽正在迁出,同时服务器不能处理该命令,则服务器告诉客户端。我处理不了,这个键可能已经迁出了,你去目标服务器执行试试吧。是个应对迁移过程中存在槽分散问题的措施。

复制与故障转移

redis集群中的节点分为主节点和从节点。主节点用来处理槽,从节点复制主节点,当主节点下线时,从节点代替主节点处理槽。

  • 设置从节点

    cluster replicate <节点id> 收到该命令的节点会成为 节点id 对应节点的从节点。将clusrerState结构中的myself所执行的节点结构的slave of指向节点id,并且关闭自己节点信息中的主节点标志。

    调用复制代码,根据slave of指向的节点信息,获得主节点地址,复制主节点。和单机的主从复制类似。

    一个节点成为从节点,并开始复制主节点这个消息会发送给集群中的其他节点,修改主节点的clusterNoded的属性,记录他的从节点数量和节点。最终集群中的所有节点都知道了。

    主节点结构

  • 故障检测

    集群中的每个节点会定式向其他节点发送ping消息来检测对方是否在线。如果没有在规定时间内收到ping的恢复,则将发送ping消息的节点将接受ping消息的节点标记为疑似下线

    节点被标记下线

    集群中的各个节点会通宵相互发送消息交换各个节点的状态,当一个节点从其他节点发送的消息得知另外节点下线,会修改另外节点对应的clusterNode结构中fail_reports属性,标记疑似下线。

    如果有半数以上的主节点认为某个主节点疑似下线,将发送一个广播到集群中的所有节点,广播节点下线下次,收到广播的节点,会将对应节点标记为下线。

    节点交换下线消息

  • 故障转移

    1. 复制下线主节点中的一个从节点会被选中。
    2. 被选中的节点执行slaveof no one命令成为新的主节点。
    3. 新的主节点会撤销所有对已下线的主节点槽,并将槽指派给自己。
    4. 广播自己成为新节点消息。其他节点更新槽消息等
    5. 新的主节点处理对应的槽消息,完成故障转移。
  • 选举新的主节点

    和选取领头哨兵一致。

18: 发布与订阅

19:事务

redis 通过multi exec watch等命令实现事务功能。事务提供了一种将命令请求打包,然后一次性,按顺序的执行多个命令的机制,服务器在执行事务期间,不会中断事务去指向其他客户端的命令请求,知道事务中的所有命令执行完毕,才会去处理其他的命令请求。

事务执行语句

事务的实现

  • 事务开始

    MULTI命令标志着事务的开始。

    客户端向服务器发送该命令,服务器将该客户端的状态置为事务状态。

  • 命令入队

    当一个客户端处于非事务状态的时候,客户端的命令会被立即执行。

    当一个客户端处于事务状态的时候,服务器会根据客户端发来的不同命令执行不同操作

    • 如果是事务相关的命令,则立即执行命令
    • 如果是非事务相关的命令,不会立即执行,而是放入事务队列中,然后向客户端返回queued。
  • 事务队列

    服务器维护的客户端的对象中,一个事务状态属性

    • 计数器:记录事务命令个数
    • 事务队列:保存命令
  • 执行事务

    如果客户端处于事务状态,当客户端发送exec命令时,exec命令会被立刻执行。服务器遍历事务队列。执行事务队列中的所有命令。然后将结果全部返回给客户端。

    这个是事务对应的命令才被开始执行,事务开始

watch 命令的实现

watch 命令是一种乐观锁,他可以在exec命令执行前执行,监视任意数量的键,并在exec命令执行时,检查被监视的键是否从监视开始后被修改,如果修改了则拒绝执行事务。

  • 使用 watch命令接收键

    数据库结构中保存了一个watched_key字典,字典中保存了被监视的键和监视这个键的客户端。通过这个字典,服务器可以知道哪些键被哪些客户端监视。

    字典的键是被监视的键,值时监视这个键的客户端组成的链表。

  • 监视机制的乘法

    所有的数据库进行修改的命令,在执行后会调用函数检查watched_key字典,查看是否有客户端正在监视该键,如果有,则将修改客户端的标志,表示该客户端的事务安全性被破坏。

  • 判断事务是否安全

    服务器收到客户端的exec命令时,会先检查客户端的事务安全标示

    如果安全性被破坏,拒绝执行事务

    如果安全性没被破坏,执行事务

事务的ACID性质

  • 原子性

    如果事务正确,且顺利执行,支持原子性

    如果事务中有错误命令,则不能入队,事务的所有命令都不会背执行。

    如果入队的命令中错误的,则事务会执行,错误命令会报错,所有命令执行完。不会回滚

  • 一致性

    事务执行前后,数据库中的数据符合数据库本身的定义和要求。

    入队错误:如果事务中的命令在入队过程中,出现了语法错误,redis会拒绝执行该事务。

    执行错误:入队是检查不出的错误,redis会执行并保存,然后继续执行后续命令。

    服务器停机:重启后,无持久化数据库会一篇空白,一致的。RDB模式下的,一致的。AOF模式下,一致的。

  • 隔离性

    各个事务不会相互影响。redis单线程,天然具有隔离性。

  • 持久性

    事务实现完毕,肯定不会丢失,叫做持久性

    无持久化:丢失

    RDB模式下:可能丢失,可能不丢失。不具备,

    AOF模式下:always时,每条语句执行后,同步到硬盘,具有持久性。其他模式下,不具有持久性。no-appendfsync-on-rewrite 需要关闭。

20:Lua 脚本

21:排序

redis 使用 sort 命令可以对列表键,集合键,有序集合的值进行排序。

快排

sort numbers

可以使用 ALPHA选项,对一个包含字符串值的集合键进行排序。

sort aset alpha

**sort key**命令的实现

  1. 创建一个长度为集合长度的数组,数组的每一项包含一个指向值的指针和一个数字。
  2. 遍历数组,让数组元素的指针指向待排序的元素。
  3. 遍历数组,将指针指向的元素转换成浮点数
  4. 排序数组。
  5. 遍历排序后的数组,将每个元素中的指针指向的键返回。

alpha 选项的实现

  • 可以使用 sort <key> alpha对字符串进行排序。
  1. 创建一个长度为集合长度的数组,数组的每一项包含一个指向值的指针和x。
  2. 遍历数组,让数组元素的指针指向待排序的元素。
  3. 根据指针指向的元素,对数组进行字符串排序,排序后数组元素按顺序指向元素。
  4. 遍历数组,按顺序返回指针指向元素。

ASC 和 DESC 选项的实现

sort <key> ALPHA DEAC

  • 默认按升序排序,ASC
  • 最后加desc,会按照降序排序。
  • 实质是排序数组的排序规则改变了。并不是倒序输出。

BY 选项的实现

  • 通过MSET设置多对key-val。
  • sort 集合名 by 正则表达式。
  • 其实就是把字符串对应的权重保存成了键值对的形式,排序的时候,从键值对中取出待排序的字符的权重
  1. 创建一个长度为集合长度的数组,数组的每一项包含一个指向值的指针和一个数字。
  2. 遍历数组,让数组元素的指针指向待排序的元素。
  3. 遍历数组,通过by后面的规则查找权重,存在数字里。
  4. 排序数组。
  5. 遍历排序后的数组,将每个元素中的指针指向的键返回。

带有 ALPHA 和 BY 选项的实现

  • by 默认权重保存的值为数字,如果权重保存的是字符串,就要搭配 alpha选项。
  • sort <key> by *** alpha
  1. 创建一个长度为集合长度的数组,数组的每一项包含一个指向值的指针和一个指向权重的指针。
  2. 遍历数组,让数组元素的指针指向待排序的元素。
  3. 根据指针指向的元素,查找权重,并用指向权重的指针指向查找结果。
  4. 按照权重排序数组
  5. 遍历排序后的数组,将每个元素中的指针指向的键返回。

LIMIT 选项的实现

  • 默认情况下,将排序后所有元素返回给客户端。
  • limt 可以返回一部分limt <offset> <count>
  • 先排序,然后找到偏移位置,然后按照个数返回。

GET 选项的实现

  • sort <key> get ** get **
  • 返回时,去查找,然后返回查找结果。

store 选项的实现

  • 默认情况系,排序后只像客户端返回结果,服务器并不保存结果
  • sort <key> store <键>会将结果保存在键里面,值时列表。会先情况键对应的列表,然后再保存。

多个选项的执行顺序

  1. 排序 sort
  2. 限制长度 limit
  3. 获取外键 get
  4. 保存 stored
  5. 返回给客户端

多个选项的摆放顺序

  • 都可以执行。无关。、
  • get 的先后决定查找的先后。

22:二进制数组

setbit <key> 位置 状态:设置位置的状态

getbit <key> 位置:返回位置的状态

bitcount <key>:返回1的个数

bittop <and / or / xor> <key> <key1> <key2> <key3>将key1-n 的二进制运算保存在key中。

位数组的表示

  • 使用字符串对象保存维数组(SDS)
  • 先存低位后存高位,能够在不移动的情况下,扩展bitmap.
  • 扩展时会多分配一些空间。2x+1

bitcount 的实现

  • 不是遍历计数法
  • 创建二进制键与1的个数的表,查表计算。但是受到内存限制和cpucash限制
  • 汉明重量计算法。常数时间,但是费内存。
  • redis采用的查表法和汉明重量计算相结合,小的查表,大的计算汉明重量。

bitop 的实现

  • 直接操作

23:慢查询日志

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

  • slowlog-log-slower-than 指定限定时长,单位微妙
  • slowlog-max-len:最多保存多少天慢查询日志
  • 如果超过长度,会被删除最旧的

慢查询日志的保存

  • 保存在一个链表里
  • 链表节点包含:id,执行时刻,执行时间,命令等

慢查询日志的阅览和删除

  • 阅览:按照数量打印出来
  • 删除:reset 命令删除所有慢查询日志

添加新日志

  • 开始执行命令和执行完命令的时间差和慢查询时长比较,如果超过,则新建节点,添加到链表头。
  • 检查长度,如果超过最大长度,则删除最旧的(链表尾部的)

24:监视器

客户端通过 MONITOR 命令,成为服务器的监视器,服务器中保存了他的监视器的链表,当执行命令之前,会将该命令发送给客户端。

Job-Hunter 文章被收录于专栏

2024年最新整理的八股文。 包括计算机网络,操作系统,MySQL,linux,设计模式,数据结构和算法,等等。 题目来源于网友爆料,GZH摘录,CSDN等等。 根据考察知识点,将题目进行分类,方便背诵。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务