线程模型
Redis 是单线程吗?
Redis单线程指的是【接收客户端请求->解析请求->对数据进行读写操作->发送数据给客户端】这个过程由主线程完成的
在2.6版本会启动处理关闭文件和AOF刷盘任务
4.0版本会增加lazyFree线程释放内存,例如unlink key 、flushdb async、 flushall async 会把删除命令提交给后台线程操作,这样不会阻塞主线程,del会在主线程删除,对于大key最好还是使用unlink
这些都是耗时操作会阻塞主线程。三种任务都有各自的任务队列,会去消费这些任务
6.0之前线程模型
创建socket 和epoll_create() 对象
bind()绑定端口 listen监听端口
epoll_ctl把listen socket加入到epoll,注册连接事件处理函数
循环:
- 写检查发送队列中有无发送任务,有就调用write()函数将客户端缓冲区的数据发送出去,没有发完就会注册一个写事件处理函数,等到下一个epoll_wait() 函数会继续处理
- 调用epoll_wait()
- 连接事件,调用连接事件处理函数主要是调用accept()函数获取连接的socket 将连接的socket加入到epoll中 注册读事件
- 读事件,调用读事件处理函数主要是调用read函数 读取客户端发送的数据 解析数据 执行命令 将客户端添加到发送队列 将发送给客户端的数据写到缓冲区
- 写事件,调用写事件处理函数主要是调用write()函数将缓冲区的数据全部发送出去,如果还没有发完会继续注册写事件处理函数,等到下一个epoll_wait() 函数会继续处理
6.0之后
会对网络io请求进行多线程处理,默认只对写操作进行多线程处理
6.0之后的线程数
- 主线程
- 关闭文件线程、AOF刷盘线程、Lazzfree线程
- 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 | BitMap间的运算 |
应用
- 签到统计
- 判断用户登陆态
- 连续签到用户总数(与操作)
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数据结构
以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据
当发现缓冲区不够的时候会自动进行扩容,所需空间超过1MB,新长度为所需空间+1MB,反之新长度为所需空间的两倍
flags标志了不同长度和空间占用的字符串,可以减少头部长度
编译器优化,可以取消字节对齐
链表
双向链表,封装了一层,增加了长度和其他节点比较函数、复制、释放
缺陷:链表通病
- 无法利用cpu缓存
- 节点结构头开销
压缩链表
跳表
查询操作
删除操作
插入操作
哈希表
拉链法解决
哈希表 类似hashmap node数组
渐进式哈希
渐进式 rehash 步骤如下:
- 给「哈希表 2」 分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
- 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
rehash时机
负载因子=哈希表已保存的节点/哈希表长度
大于=1的时候 没有执行bgsave和bgrewriteaof命令的时候就会重写
大于=5的时候,无论什么时候都会进行rehash算法
持久化
AOF 日志
redis 里的 AOF(Append Only File) 持久化功能,注意只会记录写操作命令,读操作命令是不会被记录的,
先写命令的好处
- 避免额外检查开销(记录到硬盘之前会先检查这个语法)
- 避免阻塞写命令
坏处
- 出现停电操作的时候,部分数据会丢失(和回写策略有关)
- 由于命令都是在主进程中执行,还是可能会阻塞下一个写命令
回写策略
Always 每次写入 AOF 文件数据后,就执行 fsync() 函数
Everysec 创建一个异步任务来执行 fsync() 函数
No 永不执行 fsync() 函数
重写机制
在使用重写机制后,就会读取 name 最新的 value(键值对)
重写过程由后台子进程进行操作,因为比较耗时避免阻塞主进程,
并且如果使用线程线程共享进程的资源,对共享内存进行读写的时候需要进行加锁,而父子进程,在创建时是共享数据的,但在修改的时候会生成自己的数据副本,写时复制
具体过程(虚拟空间不同,物理空间相同,标记该物理内存的权限为只读。)
主进程
在子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到 「AOF 缓冲区」;
- 将执行后的写命令追加到 「AOF 重写缓冲区」;
在完成重写之后,主进程会收到信号,
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
- 将新的aof重命名,并将后面所有的写命令记录到新的aof文件中
RDB文件
save(主线程)和 bgsave(后台线程)
可以定时保存RDB文件,但是是一个比较重的方法频率得适当
保存快照的时候,数据被修改
可以被修改,当主线程修改了某一块的数据就会发生写时复制,极端情况会发生内存会翻倍,写操作多的时候需要留意内存变化
混合持久化
保存RDB并且在保存过程中的命令也会被写到重写缓冲区中最后以AOF的方式添加到末尾
对过期key的处理
AOF
写入阶段:创建的时候 直接增加key 过期删除的时候显示删除 del
重写阶段:会校验key的过期时间确保过期的key不会被写入到新的AOF文件当中
RDB
写入阶段:会对key进行校验
加载阶段: 与主从服务器有关,如果是主服务器则不会加载,从服务器无论是否过期都会加载,但是在主从同步阶段就会被清空数据
缓存异常
缓存雪崩
缓存雪崩是指大量的应用请求无法在Redis缓存中进行处理,从而使得大量请求发送到数据库层,导致数据库压力过大甚至宕机。
- 同一时间缓存中的数据大面积过期(增加随机时间,设置热点数据永不过期,服务降级(非核心数据可以直接返回错误))
- Redis 缓存实例发生故障宕机(前端限流,预防可部署高可用集群)
缓存击穿
缓存击穿是指某个访问非常频繁的热点数据,大量并发请求集中在这一个点访问,在这个Key失效的瞬间,持续的大并发就穿破缓存,直接请求数据库
- 提前预热,设置热点数据永不过期
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
缓存穿透
缓存穿透指用户要访问的数据既不在缓存中也不在数据库中,导致用户每次请求该数据时都要去数据库查一遍,然后返回空。
- 接口检验(用户鉴权,判断是否合法)
- 缓存空值或者缺省值(缓存空值可以设置过期时间)
- 布隆过滤器(布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在)
过期删除策略
添加带有过期时间的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
主从同步
第一次同步(全量同步)
- 协商同步,发送runid 以及offset(-1)
- 发送同步指令,从服务器保存主服务器信息
- 主服务器进行bgsave,保存RDB文件,发送RDB文件给从服务器,后面新来的指令会被写到复制缓冲区
- 从服务器加载RDB数据后发送同步完成命令,主服务器将复制缓冲区命令继续发送给从服务器
第一同步完成后会保留长连接便于后续的同步更新
如果网络断了会进行,第二次同步
发送主从同步的runid 并设置offset
增量同步,断了之后会从repl_backlog_buffer(环形buffer)中获取,主服务器会记录到自己写到哪个部分master_backlog_offset,从服务器也会记录自己的slave_backlog_offset,重新进行传输
但是掉线时过长会导致原来还未发送的数据被覆盖,所以设定repl_backlog_buffer 很重要,最少为掉线平均时间*每秒写入量
哨兵机制
选主,主从故障转义,监控
脑裂问题的话,会让旧的主节点变为从节点并进行全量更新