Redis基础知识扫盲
Redis基础知识
一.基础篇
1.什么是redis?有哪些基础数据结构?
redis是是一个使用 C 语言 编写的,开源的 (BSD许可) 高性能 非关系型 (NoSQL) 的 键值对数据库。
Redis 可以存储 键 和 不同类型数据结构值 之间的映射关系。键的类型只能是字符串,而值除了支持最 基础的五种数据类型 外,还支持一些 高级数据类型:
2.Redis的优点和缺点?
优点:
- 读写性能优异, Redis能读的速度是
110000
次/s,写的速度是81000
次/s。 - 支持数据持久化,支持 AOF 和 RDB 两种持久化方式。
- 支持事务,Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
- 数据结构丰富,除了支持 string 类型的 value 外还支持 hash、set、zset、list 等数据结构。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点:
- 数据库 容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
- Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了 系统的可用性。
- Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
3.为什么要用缓存?为什么使用 Redis?
提一下现在 Web 应用的现状
在日常的 Web 应用对数据库的访问中,读操作的次数远超写操作,比例大概在 1:9 到 3:7,所以需要读的可能性是比写的可能大得多的。当我们使用 SQL 语句去数据库进行读写操作时,数据库就会 去磁盘把对应的数据索引取回来,这是一个相对较慢的过程。
使用 Redis or 使用缓存带来的优势
如果我们把数据放在 Redis 中,也就是直接放在内存之中,让服务端直接去读取内存中的数据,那么这样 速度 明显就会快上不少 (高性能),并且会 极大减小数据库的压力 (特别是在高并发情况下)。
也要提一下使用缓存的考虑
但是使用内存进行数据存储开销也是比较大的,限于成本 的原因,一般我们只是使用 Redis 存储一些 常用和主要的数据,比如用户登录的信息等。
一般而言在使用 Redis 进行存储的时候,我们需要从以下几个方面来考虑:
- 业务数据常用吗?命中率如何? 如果命中率很低,就没有必要写入缓存;
- 该业务数据是读操作多,还是写操作多? 如果写操作多,频繁需要写入数据库,也没有必要使用缓存;
- 业务数据大小如何? 如果要存储几百兆字节的文件,会给缓存带来很大的压力,这样也没有必要;
在考虑了这些问题之后,如果觉得有必要使用缓存,那么就使用它!
4.使用缓存会出现什么问题?
一般来说有如下几个问题,回答思路遵照 是什么 → 为什么 → 怎么解决:
- 缓存雪崩问题;
- 缓存穿透问题;
- 缓存击穿;
- 缓存和数据库双写一致性问题;
1.缓存雪崩
- 含义:大面积的缓存失效,大量的请求直接打到了db。
- 后果:大数量级的请求直接打到db,后果几乎是灾难性的,比如说打挂的是一个用户服务的数据库,那么依赖这个库的所有接口都会报错,如果没有做熔断等策略,基本上就是瞬间挂一片的节奏。
- 避免方法:
- 往redis中批量存数据时,把每个key的失效时间都加一个随机值,保证数据不会在同一时间大量失效。
- 如果redis是集群部署,将热点数据均匀分布在不同redis库中也可。
- 或者直接设置热点数据永远不过期。
另外对于 "Redis 挂掉了,请求全部走数据库" 这样的情况,我们还可以有如下的思路:
事发前
:实现 Redis 的高可用(主从架构 + Sentinel 或者 Redis Cluster),尽量避免 Redis 挂掉这种情况发生。
事发中
:万一 Redis 真的挂了,我们可以设置本地缓存(ehcache) + 限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)
事发后
:Redis 持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。
2.缓存穿透
- 含义:指缓存和db中都没有此数据,但用户不断发起请求。
- 后果:这种请求类似攻击,会造成数据库压力过大,严重可能会击垮数据库。
- 避免方法:
- 首先要追究产生缓存穿透的原因,比如请求用户ID为负数的请求,没有为负数的ID,服务端没有做任何限制,导致直接打到了db,我们要做的就是防备不合法请求打到db,比如在接口层增加校验,比如用户鉴权,参数做校验,不合法参数直接return等。
- 我们开发时,都要有一颗不信任的心,不信任接口调用者对传的任何参数。
- 对单秒内发起很多次请求的恶意用户,可以让运维对单个IP访问次数超过阈值的IP进行拉黑。
- 布隆过滤器,判断一个元素是否在合集中
3.缓存击穿
-
含义:一个key非常热点,在不停的扛着大并发,在这个key失效瞬间,持续的大并发就击破缓存直接打到DB,就像是在完好的桶上开了一个洞。
-
后果:后果类似缓存雪崩
-
避免:
-
热点数据设置成永不过期或长一点
-
加互斥锁,并发的多个请求中,只有一个请求线程可以拿到锁去数据库执行查询操作,执行完后,将查询的值设置到redis
-
4.缓存和数据库双写一致性问题
5.Redis 为什么早期版本选择单线程?
官方解释
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是 机器内存的大小 或者 网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
简单总结一下
- 使用单线程模型能带来更好的 可维护性,方便开发和调试;
- 使用单线程模型也能 并发 的处理客户端的请求;(I/O 多路复用机制)
- Redis 服务中运行的绝大多数操作的 性能瓶颈都不是 CPU;
6.redis为什么这么快?
简单总结:
- 纯内存操作:读取不需要进行磁盘 I/O,所以比传统数据库要快上不少;(但不要有误区说磁盘就一定慢,例如 Kafka 就是使用磁盘顺序读取但仍然较快)
- 单线程,无锁竞争:这保证了没有线程的上下文切换,不会因为多线程的一些操作而降低性能;
- 多路 I/O 复用模型,非阻塞 I/O:采用多路 I/O 复用技术可以让单个线程高效的处理多个网络连接请求(尽量减少网络 IO 的时间消耗);
- 高效的数据结构,加上底层做了大量优化:Redis 对于底层的数据结构和内存占用做了大量的优化,例如不同长度的字符串使用不同的结构体表示,HyperLogLog 的密集型存储结构等等..
7.redis单机会有瓶颈,这个问题如何解决?
采用集群部署的方式,也就是redis cluster
- redis cluster是主从同步读写分离,rediscluster 支撑N个 reids master node,每个master node都可以挂载多个slave node.
- 读写分离的架构,对于每个master,写就写到master,读就从master对应的slave去读
- 每个master都有自己的slave节点,如果master挂掉,会自动将master的某个slave切换成master
- 整个redis可以进行横向扩容,如果需要支持更大数据量的缓存,就横向扩容多个master节点。
8.redis和memcached的区别?
分别从数据操作,内存管理机制,性能,集群管理四个方向进行分析
- 数据操作
- memcached仅支持简单kv存储,不支持其他数据类型
- redis支持更多的数据结构,有更加丰富的数据操作
- 内存管理机制
- memcached所有数据都是一直存储在内存中,不支持持久化,采用默认的slab allocation机制管理内存,其主要思想就是按照预先规定的大小,将分配的内存切割成特定长度的块以存储相应长度的key-value数据,以完全解决内存碎片问题
- redis支持RDB和AOF持久化
- 当物理内存用完时,reids可以将一些很久没有用的value写到磁盘,这种特性可以保证reids可以保存超过机器物理内存大小的数据。
- 性能
- redis只使用单核,memcached可以使用多核
- 所以平均每个核上redis在存储小数据时比memcached性能高,而在100k以上的大数据时,memcached性能较高
- 集群管理
- memcached本身不支持分布式,因此只能在客户端通过一致性hash这样的分布式算法来实现memcached的分布式存储
- redis偏向于在服务器端构建分布式存储
二.数据结构篇
1.简述一下 Redis 常用数据结构及实现?
首先在 Redis 内部会使用一个 RedisObject 对象来表示所有的 key
和 value
:
其次 Redis 为了 平衡空间和时间效率,针对 value
的具体类型在底层会采用不同的数据结构来实现,下图展示了他们之间的映射关系:(好像乱糟糟的,但至少能看清楚..)
基础数据结构:
-
String
- 字符串对象
- 底层实现
- int : 整数值实现
- 短字符串:embstr编码,sds实现
- 长字符串:raw编码,sds实现
-
List
- 列表对象
- 底层实现
-
ziplist 压缩列表
- 图例
- 图例
-
linkedlist 双端列表
- 图例
- 图例
-
满足特定条件的对象才使用ziplist实现,否则使用linkedlist实现
-
-
Hash
- 哈希对象
- 底层实现
- ziplist 压缩列表
- 图例
- 图例
- hashtable 哈希表,其底层采用字典实现
- 图例
- 图例
- 满足特定条件的对象才使用ziplist实现,否则使用hashtable实现
- ziplist 压缩列表
-
Set
- 集合对象
- 底层实现
- 整数集合 intset实现
- 图例
- 图例
- 非整数集合 hashtable实现,底层同样是dict
- 图例
- 图例
- 满足特定条件的对象使用intset实现,否则使用hashtable实现
- 整数集合 intset实现
-
SortedSet
- 有序集合对象
- 底层实现
- ziplist
- 样例
- 样例
- skiplist编码,底层包含一个skiplist和dict
- 样例
- 样例
- ziplist
大致总结一下:
高级数据结构
:
-
HyperLogLog
- 作用: 以极小的内存占用提供稍微不精准的去重基数统计。
- 在redis中,只需要12kb就可以统计2^64个数据,
- 计数存在一定的误差,误差率整体较低。标准误差为 0.81%
-
Geo
- 用于计算地理位置信息相关的一些功能(比如附近的人),其底层依然采用sortedSet实现
- redis使用业界通用的地理位置距离排序算法GeoHash算法
- GeoHash算法将二维经纬度数据映射到一维整数,这样所有的元素都将挂载到一条线上距离相近的二维点映射到一维也会相近,然后再用有序set排序,就可实现附近的人功能
-
Pub/Sub
- 发布与订阅
- 在redis中,你可以对某一个key值进行消息发布和订阅,当一个key值进行了消息发布之后,所有订阅它的客户端都会收到相应的消息。
Redis Moudle:
-
RedisBloom:分布式环境下的布隆过滤器
- 布隆过滤器
- 作用:用于检索一个元素是否在一个合集中,它的优点是空间效率高,查询时间少,缺点是有一定的误识别率和无法删除
- 原理:当一个元素被加入合集时,通过K个哈希函数将元素映射成一个位数组中的K个点,将他们置为1,检索时,只需要看这些点是不是1就知道它是否在集合中,如果这些点,有任何一个为0,则被检元素一定不在,如果都是1,则被检元素很有可能存在。
- 应用:
- 主要用于防止缓存击穿,避免每次请求都不走缓存直接打到数据库。
- 布隆过滤器可以明确某个数据在数据库是否存在,所以可以过滤掉无效的查询到数据库,减小数据库的压力。
- 布隆过滤器
-
RedisSearch:全文检索组件,适合数据量适中,内存和存储有限的缓存。
2.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 是二进制安全的,写入什么读取就是什么,不做任何过滤和限制;
3.redis的字典是怎么实现的?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
。
4.跳跃表是如何实现的?原理?
跳跃表(skiplist)是一种随机化的数据结构,由 William Pugh 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出,是一种可以与平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成,
首先,因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?
- 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说);
- 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;
基于以上的一些考虑,Redis 基于 William Pugh 的论文做出一些改进后采用了 跳跃表 这样的结构。
跳跃表 skiplist 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)。
skiplist 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。为了表达清楚,下图展示了如何通过一步步的插入操作从而形成一个 skiplist 的过程:
从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。
现在我们假设从我们刚才创建的这个结构中查找 23 这个不存在的数,那么查找路径会如下图:
具体请参考:redis跳跃表
5.压缩列表如何实现的?
这是 Redis 为了节约内存 而使用的一种数据结构
zset 和 hash 容器对象会在元素个数较少的时候,采用压缩列表(ziplist)进行存储。
压缩列表是 一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。
当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。
当一个哈希只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希的底层实现。
三.持久化篇
1.什么是持久化?持久化时发生了什么?分析如何保证持久化安全?
先简单谈一谈是什么持久化
Redis 的数据 全部存储 在 内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。
解释一下持久化发生了什么
我们来稍微考虑一下 Redis 作为一个 "内存数据库" 要做的关于持久化的事情。通常来说,从客户端发起请求开始,到服务器真实地写入磁盘,需要发生如下几件事情:
- 客户端向数据库 发送写命令 (数据在客户端的内存中)
- 数据库 接收 到客户端的 写请求 (数据在服务器的内存中)
- 数据库 调用系统 API 将数据写入磁盘 (数据在内核缓冲区中)
- 操作系统将 写缓冲区 传输到 磁盘控控制器 (数据在磁盘缓存中)
- 操作系统的磁盘控制器将数据 写入实际的物理媒介 中 (数据在磁盘中)
分析如何保证持久化安全
如果我们故障仅仅涉及到 软件层面 (该进程被管理员终止或程序崩溃) 并且没有接触到内核,那么在 上述步骤 3 成功返回之后,我们就认为成功了。即使进程崩溃,操作系统仍然会帮助我们把数据正确地写入磁盘。
如果我们考虑 停电/ 火灾 等 更具灾难性 的事情,那么只有在完成了第 5 步之后,才是安全的。
所以我们可以总结得出数据安全最重要的阶段是:步骤三、四、五,即:
- 数据库软件调用写操作将用户空间的缓冲区转移到内核缓冲区的频率是多少?
- 内核多久从缓冲区取数据刷新到磁盘控制器?
- 磁盘控制器多久把数据写入物理媒介一次?
- 注意: 如果真的发生灾难性的事件,我们可以从上图的过程中看到,任何一步都可能被意外打断丢失,所以只能 尽可能地保证 数据的安全,这对于所有数据库来说都是一样的。
我们从 第三步 开始。Linux 系统提供了清晰、易用的用于操作文件的 POSIX file API
,20
多年过去,仍然还有很多人对于这一套 API
的设计津津乐道,我想其中一个原因就是因为你光从 API
的命名就能够很清晰地知道这一套 API 的用途:
int open(const char *path, int oflag, .../*,mode_t mode */);
int close (int filedes);int remove( const char *fname );
ssize_t write(int fildes, constvoid *buf, size_t nbyte);
ssize_t read(int fildes, void *buf, size_t nbyte);
所以,我们有很好的可用的 API
来完成 第三步,但是对于成功返回之前,我们对系统调用花费的时间没有太多的控制权。
然后我们来说说 第四步。我们知道,除了早期对电脑特别了解那帮人 (操作系统就这帮人搞的),实际的物理硬件都不是我们能够 直接操作 的,都是通过 操作系统调用 来达到目的的。为了防止过慢的 I/O 操作拖慢整个系统的运行,操作系统层面做了很多的努力,譬如说 上述第四步 提到的 写缓冲区,并不是所有的写操作都会被立即写入磁盘,而是要先经过一个缓冲区,默认情况下,Linux 将在 30 秒 后实际提交写入。
但是很明显,30 秒 并不是 Redis 能够承受的,这意味着,如果发生故障,那么最近 30 秒内写入的所有数据都可能会丢失。幸好 PROSIX API
提供了另一个解决方案:fsync
,该命令会 强制 内核将 缓冲区 写入 磁盘,但这是一个非常消耗性能的操作,每次调用都会 阻塞等待 直到设备报告 IO 完成,所以一般在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync
操作。
到目前为止,我们了解到了如何控制 第三步
和 第四步
,但是对于 第五步,我们 完全无法控制。也许一些内核实现将试图告诉驱动实际提交物理介质上的数据,或者控制器可能会为了提高速度而重新排序写操作,不会尽快将数据真正写到磁盘上,而是会等待几个多毫秒。这完全是我们无法控制的。
2.redis是如何实现持久化的?
- RDB做镜像全量持久化,AOF做增量持久化
- 因为RDB耗时长,不够实时,在停机的时候回导致大量丢失数据,所以需要AOF配合使用,在redis重启实例时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
- 可以将RDB理解为一整个表的全量数据,AOF理解为每次操作的日志,服务器重启的时候先将表的数据全部搞进去,但可能不完整,再回放一下日志,数据就完整了。
1.RDB
- 优点
- 它会生成多个数据文件,每个数据文件都代表了某一时刻redis里面的数据,这种方式,适合做冷备份,比如你想要多少分钟前的redis数据。
- RDB对redis性能的影响也非常小,因为在同步数据时它只是fork了一个子进程去做持久化,而且在恢复数据的时候速度比AOF更快。
- 缺点
- RDB都是快照文件,都是默认五分钟或者更久才生成一次,这意味着两次同步时间之间的这五分钟的数据都会全部丢掉。而AOF最多丢失一秒数据
- 在生成RDB快照文件时,如果文件很大,客户端可能会暂停几毫秒或者几秒,这不能满足高性能要求场景,比如秒杀活动。
2.AOF
-
优点
- AOF通过一个后台线程进行sync操作,最多丢失一秒的数据
- AOF的日志通过append-only的方式去写,追加的方式写数据,会少很多磁盘寻址的开销,写入性能很不错,文件也不容易破损
- AOF的日志是以一种非常可读的方式进行记录的,这种特性适合做灾难性数据误删除的紧急恢复操作。
-
缺点
- AOF数据文件体积比RDB大
- AOF开启后,redis支持写的QPS会比RDB支持写的要低,因为AOF每次都要去异步刷新一下日志fsync
-
可以采用RDB做冷备份,AOF做热备份
-
掉电会有可能导致数据丢失,这个取决于AOF日志的sync属性,不要求性能时,在每条写指令时都sync磁盘,就不会丢失数据,但高性能要求下要求每次写指令都sync是不现实的,一般使用定时sync,比如1s一次,这个时候就最多丢失1s的数据。
这两种持久化方式应该如何选择?
- 一般来说, 如果想达到足以媲美 PostgreSQL 的 数据安全性,你应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- 如果你非常关心你的数据, 但仍然 可以承受数分钟以内的数据丢失,那么你可以 只使用 RDB 持久化。
- 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
- 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
3.redis的数据恢复?
Redis 的数据恢复有着如下的优先级:
- 如果只配置 AOF ,重启时加载 AOF 文件恢复数据;
- 如果同时配置了 RDB 和 AOF ,启动只加载 AOF 文件恢复数据;
- 如果只配置 RDB,启动将加载 dump 文件恢复数据。
拷贝 AOF 文件到 Redis 的数据目录,启动 redis-server AOF 的数据恢复过程:Redis 虚拟一个客户端,读取 AOF 文件恢复 Redis 命令和参数,然后执行命令从而恢复数据,这些过程主要在 loadAppendOnlyFile()
中实现。
拷贝 RDB 文件到 Redis 的数据目录,启动 redis-server 即可,因为 RDB 文件和重启前保存的是真实数据而不是命令状态和参数。
四.集群篇
1.redis主从同步了解吗
主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。
主从复制主要的作用
- 数据冗余: 主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复: 当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 (实际上是一种服务的冗余)。
- 负载均衡: 在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
- 高可用基石: 除了上述作用以外,主从复制还是哨兵和集群能够实施的 基础,因此说主从复制是 Redis 高可用的基础。
实现原理
2.redis哨兵模式了解吗?
上图 展示了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:
- 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
- 数据节点: 主节点和从节点都是数据节点;
在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:
- 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
- 自动故障转移(Automatic failover): 当 主节点 不能正常工作时,哨兵会开始 自动故障转移操作,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
- 配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
- 通知(Notification): 哨兵可以将故障转移的结果发送给客户端。
其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。而配置提供者和通知功能,则需要在与客户端的交互中才能体现。
新的主服务器是怎样被挑选出来的?
故障转移操作的第一步 要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送 slaveof no one
命令,将这个从服务器转换为主服务器。但是这个从服务器是怎么样被挑选出来的呢?
简单来说 Sentinel 使用以下规则来选择新的主服务器:
- 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰。
- 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。
- 在 经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset)最大 的那个 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。
3.redis集群使用过吗?实现原理是什么?
上图 展示了 Redis Cluster 典型的架构图,集群中的每一个 Redis 节点都 互相两两相连,客户端任意 直连 到集群中的 任意一台,就可以对其他 Redis 节点进行 读写 的操作。
redis集群实现的基本原理
Redis 集群中内置了 16384
个哈希槽。当客户端连接到 Redis 集群之后,会同时得到一份关于这个 集群的配置信息,当客户端具体对某一个 key
值进行操作时,会计算出它的一个 Hash 值,然后把结果对 16384
求余数,这样每个 key
都会对应一个编号在 0-16383
之间的哈希槽,Redis 会根据节点数量 大致均等 的将哈希槽映射到不同的节点。
再结合集群的配置信息就能够知道这个 key
值应该存储在哪一个具体的 Redis 节点中,如果不属于自己管,那么就会使用一个特殊的 MOVED
命令来进行一个跳转,告诉客户端去连接这个节点以获取数据:
MOVED
指令第一个参数 3999
是 key
对应的槽位编号,后面是目标节点地址,MOVED
命令前面有一个减号,表示这是一个错误的消息。客户端在收到 MOVED
指令后,就立即纠正本地的 槽位映射表,那么下一次再访问 key
时就能够到正确的地方去获取了。
集群的主要作用?
- 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点,一方面 突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及,例如,如果单机内存太大,
bgsave
和bgrewriteaof
的fork
操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出…… - 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。
4.redis集群中的数据如何分区?
带有虚拟节点的一致性哈希分区
该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦 了 数据和实际节点 之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有 4
个实际节点,假设为其分配 16
个槽(0-15);
- 槽 0-3 位于 node1;4-7 位于 node2;以此类推....
如果此时删除 node2
,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1
,槽 6 分配给 node3
,槽 7 分配给 node4
;可以看出删除 node2
后,数据在其他节点的分布仍然较为均衡。
5.redis节点间的通信机制了解吗?
集群的建立离不开节点之间的通信,例如我们在 快速体验 中刚启动六个集群节点之后通过 redis-cli
命令帮助我们搭建起来了集群,实际上背后每个集群之间的两两连接是通过了 CLUSTER MEET <ip> <port>
命令发送 MEET
消息完成的,下面我们展开详细说说。
两个端口
在 哨兵系统 中,节点分为 数据节点 和 哨兵节点:前者存储数据,后者实现额外的控制功能。在 集群 中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个 TCP 端口:
- 普通端口: 即我们在前面指定的端口 (7000等)。普通端口主要用于为客户端提供服务 (与单机节点类似);但在节点间数据迁移时也会使用。
- 集群端口: 端口号是普通端口 + 10000 (10000是固定值,无法改变),如
7000
节点的集群端口为17000
。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。
Gossip 协议
节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。
- 广播是指向集群内所有节点发送消息。优点 是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点 是每条消息都要发送给所有节点,CPU、带宽等消耗较大。
- Gossip 协议的特点是:在节点数量有限的网络中,每个节点都 “随机” 的与部分节点通信 (并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip 协议的 优点 有负载 (比广播) 低、去中心化、容错性高 (因为通信有冗余) 等;缺点 主要是集群的收敛速度慢。
消息类型
集群中的节点采用 固定频率(每秒10次) 的 定时任务 进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。
节点间发送的消息主要分为 5
种:meet 消息
、ping 消息
、pong 消息
、fail 消息
、publish 消息
。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的:
- MEET 消息: 在节点握手阶段,当节点收到客户端的
CLUSTER MEET
命令时,会向新加入的节点发送MEET
消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个PONG
消息。 - PING 消息: 集群里每个节点每秒钟会选择部分节点发送
PING
消息,接收者收到消息后会回复一个PONG
消息。PING 消息的内容是自身节点和部分其他节点的状态信息,作用是彼此交换信息,以及检测节点是否在线。PING
消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:(1)随机找 5 个节点,在其中选择最久没有通信的 1 个节点;(2)扫描节点列表,选择最近一次收到PONG
消息时间大于cluster_node_timeout / 2
的所有节点,防止这些节点长时间未更新。 - PONG消息:
PONG
消息封装了自身状态数据。可以分为两种:第一种 是在接到MEET/PING
消息后回复的PONG
消息;第二种 是指节点向集群广播PONG
消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播PONG
消息。 - FAIL 消息: 当一个主节点判断另一个主节点进入
FAIL
状态时,会向集群广播这一FAIL
消息;接收节点会将这一FAIL
消息保存起来,便于后续的判断。 - PUBLISH 消息: 节点收到
PUBLISH
命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH
命令。
6.redis集群自身的数据是如何存储的?
节点为了存储集群状态而提供的数据结构中,最关键的是 clusterNode
和 clusterState
结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。
clusterNode 结构
clusterNode
结构保存了 一个节点的当前状态,包括创建时间、节点 id、ip 和端口号等。每个节点都会用一个 clusterNode
结构记录自己的状态,并为集群内所有其他节点都创建一个 clusterNode
结构来记录节点状态。
下面列举了 clusterNode
的部分字段,并说明了字段的含义和作用:
typedefstruct clusterNode {
//节点创建时间
mstime_t ctime;
//节点id
char name[REDIS_CLUSTER_NAMELEN];
//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;
//节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等
int flags;
//配置纪元:故障转移时起作用,类似于哨兵的配置纪元
uint64_t configEpoch;
//槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsignedchar slots[16384/8];
//节点中槽的数量
int numslots;
…………
} clusterNode;
除了上述字段,clusterNode
还包含节点连接、主从复制、故障发现和转移需要的信息等。
clusterState 结构
clusterState
结构保存了在当前节点视角下,集群所处的状态。主要字段包括:
typedefstruct clusterState {
//自身节点
clusterNode *myself;
//配置纪元
uint64_t currentEpoch;
//集群状态:在线还是下线
int state;
//集群中至少包含一个槽的节点数量
int size;
//哈希表,节点名称->clusterNode节点指针
dict *nodes;
//槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL
clusterNode *slots[16384];
…………
} clusterState;
除此之外,clusterState
还包括故障转移、槽迁移等需要的信息。
五.其他问题?
1.redis的过期键删除策略?
简单描述
先抛开 Redis 想一下几种可能的删除策略:
- 定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在上述的三种策略中定时删除和定期删除属于不同时间粒度的 主动删除,惰性删除属于 被动删除。
三种策略都有各自的优缺点
- 定时删除对内存使用率有优势,但是对 CPU 不友好;
- 惰性删除对内存不友好,如果某些键值对一直不被使用,那么会造成一定量的内存浪费;
- 定期删除是定时删除和惰性删除的折中。
Redis 中的实现
Reids 采用的是 惰性删除和定时删除 的结合,一般来说可以借助最小堆来实现定时器,不过 Redis 的设计考虑到时间事件的有限种类和数量,使用了无序链表存储时间事件,这样如果在此基础上实现定时删除,就意味着 O(N)
遍历获取最近需要删除的数据。
2.redis的键淘汰策略有哪些?
Redis 有六种淘汰策略
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的 KV 集中优先对最近最少使用(less recently used)的数据淘汰 |
volitile-ttl | 从已设置过期时间的 KV 集中优先对剩余时间短(time to live)的数据淘汰 |
volitile-random | 从已设置过期时间的 KV 集中随机选择数据淘汰 |
allkeys-lru | 从所有 KV 集中优先对最近最少使用(less recently used)的数据淘汰 |
allKeys-random | 从所有 KV 集中随机选择数据淘汰 |
noeviction | 不淘汰策略,若超过最大内存,返回错误信息 |
4.0 版本后增加以下两种
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
3.Redis常见性能问题和解决方案?
- Master 最好不要做任何持久化工作,包括内存快照和 AOF 日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个 Slave 开启 AOF 备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave 和 Master 最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库。
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
4.假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用 keys
指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan
指令,scan
指令可以无阻塞的提取出指定模式的 key
列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys
指令长。
如果redis正在给线上服务提供服务,使用keys指令会有什么问题?
- reids是单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到keys指令执行完毕,服务才能恢复
- 这个时候可以采用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但有一定概率重复(scan是增量式的迭代命令),在客户端做一次去重就ok了,但比单keys命令耗时长。
5.redis如何实现分布式锁?
- 先用setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘了释放。
- 并且set有非常复杂的参数,可以将setnx和expire合成一条指令来使用,不用担心setnx之后expire之前,进程意外crash或者重启维护了
setnx: 如果不存在,则set
6.如何使用redis实现异步队列?
- 方式1:生产者消费者模式
- 使用list结构做队列,rpush生产消息,lpop消费消息,当lpop没有消息时,适当的sleep一会再重试,避免过高qps,或者直接使用blpop指令,在没有消息的时候,他会阻塞住,直到消息到来。
- 方式2:发布订阅模式
- 使用pub/sub主题订阅者模式,可以实现1:N的消息队列,即生产一次,消费多次,缺点就是在消费者下线的时候,生产的消息会丢失,此场景,建议MQ。
7.如何使用redis实现延时队列?
- 使用sortedset,用时间戳作为score,消息内容作为key,使用zadd命令生产消息,使用zrangebyscore来消费最早的一条消息。
- 之所以可以用redis实现延时队列,
- 最主要的原因就是redis支持高性能的socre排序。
- 同时redis的持久化支持bgsave特性,保证了消息的消费和存储问题。
- bgsave的原理是fork和cow。
- fork是指redis通过创建子进程来进行bgsave操作。
- cow是指copy on write ,子进程创建后,父进程通过共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
8.pipeline是什么?有什么好处?
- pipeline又叫做管道,是个队列。
- pipeline可以一次性发送多条命令,服务端依次处理完后,通过一条响应一次性将结果返回
- pipeline通过减少客户端与redis的通信次数来实现降低往返延时时间,总结其好处就是可以将多次IO往返时间缩减为一次。
- 同时pipeline具有事务隔离特性。
9.redis的线程模型?不是说单线程吗?为什么又采用多线程了?
很多人说Redis是单线程的,就认为Redis中所有模块的操作都是单线程的,其实这是不对的。
我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
所以说,Redis中并不是没有多线程模型的,早在Redis 4.0的时候就已经针对部分命令做了多线程化。
Redis并没有在网络请求模块和数据操作模块中使用多线程模型,主要是基于以下四个原因:
- 1、Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
- 2、使用单线程模型,可维护性更高,开发,调试和维护的成本更低
- 3、单线程模型,避免了线程间切换带来的性能开销
- 4、在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
还是要记住:Redis并不是完全单线程的,只是有关键的网络IO和键值对读写是由一个线程完成的。
为什么Redis 6.0 引入多线程?
主要是因为我们对Redis有着更高的要求。
根据测算,Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,对于小数据包,Redis 服务器可以处理 80,000 到 100,000 QPS,这么高的对于 80% 的公司来说,单线程的 Redis 已经足够使用了。
但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的 QPS。
为了提升QPS,很多公司的做法是部署Redis集群,并且尽可能提升Redis机器数。但是这种做法的资源消耗是巨大的。
而经过分析,限制Redis的性能的主要瓶颈出现在网络IO的处理上,虽然之前采用了多路复用技术。但是我们前面也提到过,多路复用的IO模型本质上仍然是同步阻塞型IO模型。
从上图我们可以看到,在多路复用的IO模型中,在处理网络请求时,调用 select (其他函数同理)的过程是阻塞的,也就是说这个过程会阻塞线程,如果并发量很高,此处可能会成为瓶颈。
虽然现在很多服务器都是多个CPU核的,但是对于Redis来说,因为使用了单线程,在一次数据操作的过程中,有大量的CPU时间片是耗费在了网络IO的同步处理上的,并没有充分的发挥出多核的优势。
如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。
所以,Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。
但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。
那么,在引入多线程之后,如何解决并发带来的线程安全问题呢?
这就是为什么我们前面多次提到的"Redis 6.0的多线程只用来处理网络请求,而数据的读写还是单线程"的原因。
Redis 6.0 只有在网络请求的接收和解析,以及请求后的数据通过网络返回给时,使用了多线程。而数据读写操作还是由单线程来完成的,所以,这样就不会出现并发问题了。
10.如何排除redis的性能问题?
参考:redis性能问题排除
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性