Redis线程模型
Redis单线程的理解,Redis 为什么早期版本选择单线程?
官方解释
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是 机器内存的大小 或者 网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
首先,我们需要明白,多线程解决的问题。当一个线程处理的任务是I/O密集型的,而I/O传输数据很慢,因此线程总是处于等待数据传输,从而造成CPU资源的浪费。这时我们可以开辟多个线程,当一个线程在等待I/O传输时,调度其他线程进行任务处理。
然而,redis所有的数据都存储在内存中,此时并不需要I/O操作。而多线程调度还会造成额外的开销且实现复杂。
最终,选用单线程即可。
一种解释:
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
简单总结一下
- 使用单线程模型能带来更好的 可维护性,方便开发和调试;
- 使用单线程模型也能 并发 的处理客户端的请求(I/O 多路复用机制epoll);
- Redis 服务中运行的绝大多数操作的 性能瓶颈都不是 CPU;
Redis为什么这么快?
简单总结:
- 纯内存操作:读取不需要进行磁盘 I/O,所以比传统数据库要快上不少;*(但不要有误区说磁盘就一定慢,例如 Kafka 就是使用磁盘顺序读取但仍然较快)*
- 单线程,无锁竞争:这保证了没有线程的上下文切换,不会因为多线程的一些操作而降低性能;
- 多路 I/O 复用模型,非阻塞 I/O:采用多路 I/O 复用技术可以让单个线程高效的处理多个网络连接请求(尽量减少网络 IO 的时间消耗);
- 高效的数据结构,加上底层做了大量优化:Redis 对于底层的数据结构和内存占用做了大量的优化,例如不同长度的字符串使用不同的结构体表示,HyperLogLog 的密集型存储结构等等..
Redis数据结构
Redis 的 SDS 和 C 中字符串相比有什么优势?
先简单总结一下
C 语言使用了一个长度为 N+1
的字符数组来表示长度为 N
的字符串,并且字符数组最后一个元素总是 \0
,这种简单的字符串表示方式 不符合 Redis 对字符串在安全性、效率以及功能方面的要求。
再来说 C 语言字符串的问题
这样简单的数据结构可能会造成以下一些问题:
- 获取字符串长度为 O(N) 级别的操作 → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组;
- 不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题;
- C 字符串 只能保存文本数据 → 因为 C 语言中的字符串必须符合某种编码(比如 ASCII),例如中间出现的
'\0'
可能会被判定为提前结束的字符串而识别不了;
Redis 如何解决的 | SDS 的优势
如果去看 Redis 的源码 sds.h/sdshdr
文件,你会看到 SDS 完整的实现细节,这里简单来说一下 Redis 如何解决的:
- 多增加 len 表示当前字符串的长度:这样就可以直接获取长度了,复杂度 O(1);
- 自动扩展空间:当 SDS 需要对字符串进行修改时,首先借助于
len
和alloc
检查空间是否满足修改所需的要求,如果空间不够的话,SDS 会自动扩展空间,避免了像 C 字符串操作中的覆盖情况; - 有效降低内存分配次数:C 字符串在涉及增加或者清除操作时会改变底层数组的大小造成重新分配,SDS 使用了 空间预分配 和 惰性空间释放 机制,简单理解就是每次在扩展时是成倍的多分配的,在缩容是也是先留着并不正式归还给 OS;
- 二进制安全:C 语言字符串只能保存
ascii
码,对于图片、音频等信息无法保存,SDS 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;
字典是如何实现的?Rehash 了解吗?
先总体聊一下 Redis 中的字典
字典是 Redis 服务器中出现最为频繁的复合型数据结构。除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key
和 value
也组成了一个 全局字典,还有带过期时间的 key
也是一个字典。*(存储在 RedisDb 数据结构中)*
说明字典内部结构和 rehash
Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的 链地址法 来解决部分 哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。
字典结构内部包含 两个 hashtable,通常情况下只有一个 hashtable
有值,但是在字典扩容缩容时,需要分配新的 hashtable
,然后进行 渐进式搬迁 *(rehash)*,这时候两个 hashtable
分别存储旧的和新的 hashtable
,待搬迁结束后,旧的将被删除,新的 hashtable
取而代之。
扩缩容的条件
正常情况下,当 hash 表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令)
,为了减少内存也得过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 **元素个数低于数组长度的 10%**,缩容不会考虑 Redis 是否在做 bgsave
。
渐进式rehash
为什么需要渐进式rehash呢?
在元素数量较少时,rehash会非常快的进行,但是当元素数量达到几百、甚至几个亿时进行rehash将会是一个非常耗时的操作。如果一次性将成万上亿的元素的键值对rehash到ht[1],庞大的计算量可能会导致服务器在一段时间内停止服务,这是非常危险的!所以,rehash这个动作不能一次性、集中式的完成,而是分多次、渐进式地完成。
渐进式rehash步骤:
- 为
ht[1]
分配空间,让字典同时持有ht[0]
和ht[1]
两个哈希表。 - 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
- 在rehash进行期间,每次对字典执行CRUD:添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将
ht[0]
哈希表在rehashidx索引上的所有键值对rehash到ht[1]
,当rehash工作完成之后,程序将rehashidx+1(表示下次将rehash下一个桶)。 - 随着字典操作的不断执行,最终在某个时间点上,
ht[0]
的所有键值对都会被rehash至ht[1]
,这时程序将rehashidx属性的值设为-1,表示rehash完成。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash 而带来的庞大计算量。
需要注意的是在渐进式rehash的过程,如果有增删改查操作时,如果index
大于rehashindex
,访问ht[0]
,否则访问ht[1]。
搬迁是埋伏在客户端请求指令当中,如果客户端闲置下来,没有后续指令触发这个搬迁,未搬迁完的的数据怎么办?
Redis 还会在定时任务中对字典进行主动搬迁。
1 | // 服务器定时任务 |
渐进式rehash的疑问
在迁移的过程中,会不会造成读少数据?
不会,因为在迁移时,首先会从ht[0]读取数据,如果ht[0]读不到,则会去ht[1]读。
在迁移过程中,新增加的数据会存放在哪个ht?
迁移过程中,新增的数据只会存在ht[1]中,而不会存放到ht[0],ht[0]只会减少不会新增。
跳跃表
Redis高可用
持久化
RDB
Redis 快照 是最简单的 Redis 持久性模式。当满足特定条件时,它将生成数据集的时间点快照,例如,如果先前的快照是在 2 分钟前创建的,并且现在已经至少有 100
次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制,也可以在运行时修改而无需重新启动服务器。快照作为包含整个数据集的单个 .rdb
文件生成。
AOF
快照不是很持久。如果运行 Redis 的计算机停止运行,电源线出现故障或者您 kill -9
的实例意外发生,则写入 Redis 的最新数据将丢失。尽管这对于某些应用程序可能不是什么大问题,但有些使用案例具有充分的耐用性,在这些情况下,快照并不是可行的选择。
AOF(Append Only File - 仅追加文件) 它的工作方式非常简单:每次执行 修改内存 中数据集的写操作时,都会 记录 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例 顺序执行所有的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
Redis 4.0 的混合持久化
重启 Redis 时,我们很少使用 rdb
来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb
来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb
文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小:
于是在 Redis 重启的时候,可以先加载 rdb
的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
RDB 和 AOF 各自有什么优缺点?
RDB | 优点
- 只有一个文件
dump.rdb
,方便持久化。 - 容灾性好,一个文件可以保存到安全的磁盘。
- 性能最大化,
fork
子进程来完成写操作,让主进程继续处理命令,所以使 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能 - 相对于数据集大时,比 AOF 的 启动效率 更高。
RDB | 缺点
- 数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;
AOF | 优点
- 数据安全,aof 持久化可以配置
appendfsync
属性,有always
,每进行一次命令操作就记录到 aof 文件中一次。 - 通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
- AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)
AOF | 缺点
- AOF 文件比 RDB 文件大,且 恢复速度慢。
- 数据集大 的时候,比 rdb 启动效率低。
两种方式如何选择?
- 一般来说, 如果想达到足以媲美 PostgreSQL 的 数据安全性,你应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- 如果你非常关心你的数据, 但仍然 可以承受数分钟以内的数据丢失,那么你可以 只使用 RDB 持久化。
- 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
- 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
Redis 的数据恢复
Redis 的数据恢复有着如下的优先级:
- 如果只配置 AOF ,重启时加载 AOF 文件恢复数据;
- 如果同时配置了 RDB 和 AOF ,启动只加载 AOF 文件恢复数据;
- 如果只配置 RDB,启动将加载 dump 文件恢复数据。
拷贝 AOF 文件到 Redis 的数据目录,启动 redis-server AOF 的数据恢复过程:Redis 虚拟一个客户端,读取 AOF 文件恢复 Redis 命令和参数,然后执行命令从而恢复数据,这些过程主要在 loadAppendOnlyFile()
中实现。
拷贝 RDB 文件到 Redis 的数据目录,启动 redis-server 即可,因为 RDB 文件和重启前保存的是真实数据而不是命令状态和参数。
主从复制
Redis主从复制核心原理
- salve node启动,会发送一个
PSYNC
命令给master node - 如果是slave node初次连接到master node,会触发一次
full resynchronization
全量复制。此时master回启动一个后台线程,生成RDB
快照文件,同时将从客户端新收到的所有写命令缓存到内存中。RDB
文件生成完毕后, master 会将这个RDB
发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。 - slave node如果跟master node有网络故障,断开了链接,会自动重连,连接之后master node仅会复制给salve部分缺少的数据。
主从复制是如何断点续传的?
从 Redis2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
master node会在内存中维护一个backlog,master和slave都会保存一个replica offset还有一个master run id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次replica offset开始复制,如果没有找到对应的offset,那么就会执行一次resynchronization
。
如果根据 host+ip 定位 master node,是不靠谱的,如果 master node 重启或者数据出现了变化,那么 slave node 应该根据不同的 run id 区分。