线程模型

Redis 是单线程吗?

Redis单线程指的是【接收客户端请求->解析请求->对数据进行读写操作->发送数据给客户端】这个过程由主线程完成的

在2.6版本会启动处理关闭文件和AOF刷盘任务

4.0版本会增加lazyFree线程释放内存,例如unlink key 、flushdb async、 flushall async 会把删除命令提交给后台线程操作,这样不会阻塞主线程,del会在主线程删除,对于大key最好还是使用unlink

这些都是耗时操作会阻塞主线程。三种任务都有各自的任务队列,会去消费这些任务

redis线程模型

6.0之前线程模型

创建socket 和epoll_create() 对象

bind()绑定端口 listen监听端口

epoll_ctl把listen socket加入到epoll,注册连接事件处理函数

循环:

  1. 写检查发送队列中有无发送任务,有就调用write()函数将客户端缓冲区的数据发送出去,没有发完就会注册一个写事件处理函数,等到下一个epoll_wait() 函数会继续处理
  2. 调用epoll_wait()
  3. 连接事件,调用连接事件处理函数主要是调用accept()函数获取连接的socket 将连接的socket加入到epoll中 注册读事件
  4. 读事件,调用读事件处理函数主要是调用read函数 读取客户端发送的数据 解析数据 执行命令 将客户端添加到发送队列 将发送给客户端的数据写到缓冲区
  5. 写事件,调用写事件处理函数主要是调用write()函数将缓冲区的数据全部发送出去,如果还没有发完会继续注册写事件处理函数,等到下一个epoll_wait() 函数会继续处理

6.0之后

会对网络io请求进行多线程处理,默认只对写操作进行多线程处理

6.0之后的线程数

  1. 主线程
  2. 关闭文件线程、AOF刷盘线程、Lazzfree线程
  3. io线程*3(默认参数是4 主线程也算进io线程当中)

数据类型

Redis 提供了丰富的数据类型,常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)

String

String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于 C 的原生字符串:

  • SDS 不仅可以保存文本数据,还可以保存二进制数据
  • **SDS 获取字符串长度的时间复杂度是 O(1)**。SDS 结构里用 len 属性记录了字符串长度
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。会自动扩容

内部编码(encoding)有 3 种 :int、raw(字符串的长度大于 32 字节)和 embstr(字符申的长度小于等于 32 字节)

应用

分布式锁

缓存计数

共享session

List

如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节,Redis 会使用压缩列表作为 List 类型的底层数据结构,反之Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表

应用

基于 List 类型的消息队列,满足消息队列的三大需求(消息保序、处理重复的消息和保证消息可靠性)。

  • 消息保序:使用 LPUSH + RPOP;
  • 阻塞读取:使用 BRPOP;
  • 重复消息处理:生产者自行实现全局唯一 ID;
  • 消息的可靠性:使用 BRPOPLPUSH

Set

Set 类型的底层数据结构是由哈希表或整数集合实现的:

如果集合中的元素都是整数且元素个数小于 512 个,Redis 会使用整数集合作为 Set 类型的底层数据结构,反之则 Redis 使用哈希表作为 Set 类型的底层数据结构。

集合的主要几个特性,无序、不可重复、支持并交差等操作。

Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞

在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。

应用

抽奖活动(去重)

共同关注(交并集)

Zset

使用压缩列表或者跳表实现

当元素个数小于512的,且每个元素的值小于 64 字节时时候,使用压缩列表实现,反之使用跳表

应用

排行榜(有序列表,且可以单独增加value的值,并且能查询区间)

BitMap

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组

1
2
3
4
5
6
# BitMap间的运算
# operations 位移操作符,枚举值
AND 与运算 &
OR 或运算 |
XOR 异或 ^
NOT 取反 ~

应用

  1. 签到统计
  2. 判断用户登陆态
  3. 连续签到用户总数(与操作)

HyperLogLog

HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。提供不精确的去重计数

应用

统计网页访问者

GEO

直接使用了 Sorted Set 集合类型。

应用

查找以这个经纬度为中心的 5 公里内的车辆信息

Stream

Redis Stream 是 Redis 5.0 版本新增加

在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:

  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。

基于 Stream 实现的消息队列

  • 消息保序:XADD/XREAD
  • 阻塞读取:XREAD block
  • 重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;
  • 消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;
  • 支持消费组形式消费数据

但是与专业的消息队列对比,会存在丢消息的情况因为aof命令并不是先写到硬盘才写入到内存当中的

数据结构

SDS

c语言字符串缺陷:获取长度O(n):缓冲区溢出;不能保存二进制;

SDS数据结构

img

  1. 以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据

  2. 当发现缓冲区不够的时候会自动进行扩容,所需空间超过1MB,新长度为所需空间+1MB,反之新长度为所需空间的两倍

  3. flags标志了不同长度和空间占用的字符串,可以减少头部长度

  4. 编译器优化,可以取消字节对齐

链表

双向链表,封装了一层,增加了长度和其他节点比较函数、复制、释放

缺陷:链表通病

  1. 无法利用cpu缓存
  2. 节点结构头开销

压缩链表

img

跳表

查询操作

img

删除操作

img

插入操作

img

哈希表

拉链法解决

哈希表 类似hashmap node数组

渐进式哈希

渐进式 rehash 步骤如下:

  • 给「哈希表 2」 分配空间;
  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

rehash时机

负载因子=哈希表已保存的节点/哈希表长度

大于=1的时候 没有执行bgsave和bgrewriteaof命令的时候就会重写

大于=5的时候,无论什么时候都会进行rehash算法

持久化

AOF 日志

img

redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的

先写命令的好处

  1. 避免额外检查开销(记录到硬盘之前会先检查这个语法)
  2. 避免阻塞写命令

坏处

  1. 出现停电操作的时候,部分数据会丢失(和回写策略有关)
  2. 由于命令都是在主进程中执行,还是可能会阻塞下一个写命令

回写策略

Always 每次写入 AOF 文件数据后,就执行 fsync() 函数

Everysec 创建一个异步任务来执行 fsync() 函数

No 永不执行 fsync() 函数

重写机制

在使用重写机制后,就会读取 name 最新的 value(键值对)

重写过程由后台子进程进行操作,因为比较耗时避免阻塞主进程,

并且如果使用线程线程共享进程的资源,对共享内存进行读写的时候需要进行加锁,而父子进程,在创建时是共享数据的,但在修改的时候会生成自己的数据副本,写时复制

具体过程(虚拟空间不同,物理空间相同,标记该物理内存的权限为只读。)

img

主进程

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

  • 执行客户端发来的命令;
  • 将执行后的写命令追加到 「AOF 缓冲区」;
  • 将执行后的写命令追加到 「AOF 重写缓冲区」;

在这里插入图片描述

在完成重写之后,主进程会收到信号,

  1. 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  2. 将新的aof重命名,并将后面所有的写命令记录到新的aof文件中

RDB文件

save(主线程)和 bgsave(后台线程)

可以定时保存RDB文件,但是是一个比较重的方法频率得适当

保存快照的时候,数据被修改

可以被修改,当主线程修改了某一块的数据就会发生写时复制,极端情况会发生内存会翻倍,写操作多的时候需要留意内存变化

混合持久化

保存RDB并且在保存过程中的命令也会被写到重写缓冲区中最后以AOF的方式添加到末尾

对过期key的处理

AOF

写入阶段:创建的时候 直接增加key 过期删除的时候显示删除 del

重写阶段:会校验key的过期时间确保过期的key不会被写入到新的AOF文件当中

RDB

写入阶段:会对key进行校验

加载阶段: 与主从服务器有关,如果是主服务器则不会加载,从服务器无论是否过期都会加载,但是在主从同步阶段就会被清空数据

缓存异常

缓存雪崩

缓存雪崩是指大量的应用请求无法在Redis缓存中进行处理,从而使得大量请求发送到数据库层,导致数据库压力过大甚至宕机。

  1. 同一时间缓存中的数据大面积过期(增加随机时间,设置热点数据永不过期,服务降级(非核心数据可以直接返回错误))
  2. Redis 缓存实例发生故障宕机(前端限流,预防可部署高可用集群)

缓存击穿

缓存击穿是指某个访问非常频繁的热点数据,大量并发请求集中在这一个点访问,在这个Key失效的瞬间,持续的大并发就穿破缓存,直接请求数据库

  1. 提前预热,设置热点数据永不过期
  2. 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

缓存穿透

缓存穿透指用户要访问的数据既不在缓存中也不在数据库中,导致用户每次请求该数据时都要去数据库查一遍,然后返回空。

  1. 接口检验(用户鉴权,判断是否合法)
  2. 缓存空值或者缺省值(缓存空值可以设置过期时间)
  3. 布隆过滤器(布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在

过期删除策略

添加带有过期时间的key的时候,会把其key和过期时间加入到过期字典当中,查询时会先判断是否在过期字典当中

过期判断流程

删除策略

定时删除(设置key的时候,增加定时事件,定时删除;优点:内存友好;缺点:过期较多的时候会占用较多cpu时间)

惰性删除(不主动删除key,访问key时查询是否过期,然后再删除;优点:占用cpu资源少;缺点:内存不友好)

定期删除(每隔一段时间 清理一次过期key)

Redis选择惰性+定期

定期删除是每隔一段时间「随机」从数据库中取出一定数量(20)的 key 进行检查,并删除其中的过期key,如果过期key超过25%,会继续进行新一轮的定期删除,如果执行时间超过25ms就不会执行新的一轮。

redis默认1s进行10次key过期检查

内存淘汰策略

不淘汰

过期key里 随机 最早过期淘汰 lru lfu

所有key 随机 lru lfu

主从同步

第一次同步(全量同步)

图片

  1. 协商同步,发送runid 以及offset(-1)
  2. 发送同步指令,从服务器保存主服务器信息
  3. 主服务器进行bgsave,保存RDB文件,发送RDB文件给从服务器,后面新来的指令会被写到复制缓冲区
  4. 从服务器加载RDB数据后发送同步完成命令,主服务器将复制缓冲区命令继续发送给从服务器

第一同步完成后会保留长连接便于后续的同步更新

如果网络断了会进行,第二次同步

发送主从同步的runid 并设置offset

图片

增量同步,断了之后会从repl_backlog_buffer(环形buffer)中获取,主服务器会记录到自己写到哪个部分master_backlog_offset,从服务器也会记录自己的slave_backlog_offset,重新进行传输

但是掉线时过长会导致原来还未发送的数据被覆盖,所以设定repl_backlog_buffer 很重要,最少为掉线平均时间*每秒写入量

哨兵机制

选主,主从故障转义,监控

脑裂问题的话,会让旧的主节点变为从节点并进行全量更新

作者

leon Yan

发布于

2023-06-19

更新于

2023-09-21

许可协议


评论