网站建设插导航条,十三五关于网站建设,可信网站认证收费吗,做网站设计难吗Redis 为什么这么快#xff1f;
很多人只知道是 K/V NoSQl 内存数据库#xff0c;单线程……这都是没有全面理解 Redis 导致无法继续深问下去。
这个问题是基础摸底#xff0c;我们可以从 Redis 不同数据类型底层的数据结构实现、完全基于内存、IO 多路复用网络模型、线程…Redis 为什么这么快
很多人只知道是 K/V NoSQl 内存数据库单线程……这都是没有全面理解 Redis 导致无法继续深问下去。
这个问题是基础摸底我们可以从 Redis 不同数据类型底层的数据结构实现、完全基于内存、IO 多路复用网络模型、线程模型、渐进式 rehash…...
到底有多快
我们可以先说到底有多快根据官方数据Redis 的 QPS 可以达到约 100000每秒请求数有兴趣的可以参考官方的基准程序测试《How fast is Redis》地址https://redis.io/topics/benchmarks 基准测试
横轴是连接数纵轴是 QPS。 ❝这张图反映了一个数量级通过量化让面试官觉得你有看过官方文档很严谨。 基于内存实现
Redis 是基于内存的数据库跟磁盘数据库相比完全吊打磁盘的速度。
不论读写操作都是在内存上完成的我们分别对比下内存操作与磁盘操作的差异。
磁盘调用 内存操作
内存直接由 CPU 控制也就是 CPU 内部集成的内存控制器所以说内存是直接与 CPU 对接享受与 CPU 通信的最优带宽。
最后以一张图量化系统的各种延时时间部分数据引用 Brendan Gregg 高效的数据结构
学习 MySQL 的时候我知道为了提高检索速度使用了 B Tree 数据结构所以 Redis 速度快应该也跟数据结构有关。
Redis 一共有 5 种数据类型String、List、Hash、Set、SortedSet。
不同的数据类型底层使用了一种或者多种数据结构来支撑目的就是为了追求更快的速度。 ❝码哥寄语我们可以分别说明每种数据类型底层的数据结构优点很多人只知道数据类型而说出底层数据结构就能让人眼前一亮。 SDS 简单动态字符串优势 C 语言字符串与 SDS SDS 中 len 保存这字符串的长度O(1) 时间复杂度查询字符串长度信息。 空间预分配SDS 被修改后程序不仅会为 SDS 分配所需要的必须空间还会分配额外的未使用空间。 惰性空间释放当对 SDS 进行缩短操作时程序并不会回收多余的内存空间而是使用 free 字段将这些字节数量记录下来不释放后面如果需要 append 操作则直接使用 free 中未使用的空间减少了内存的分配。
zipList 压缩列表
压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一。
当一个列表只有少量数据的时候并且每个列表项要么就是小整数值要么就是长度比较短的字符串那么 Redis 就会使用压缩列表来做列表键的底层实现。 ziplist
这样内存紧凑节约内存。
相关视频推荐
从 4 方面看 redis 的高效设计
基于redis集群实现一个分布式延时队列
redis 单线程为什么这么快通过源码调试告诉你答案
免费学习地址c/c linux服务器开发/后台架构师
需要C/C Linux服务器架构师学习资料加qun812855908获取资料包括C/CLinuxgolang技术NginxZeroMQMySQLRedisfastdfsMongoDBZK流媒体CDNP2PK8SDockerTCP/IP协程DPDKffmpeg等免费分享 quicklist
后续版本对列表数据结构进行了改造使用 quicklist 代替了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合体它将 linkedlist 按段切分每一段使用 ziplist 来紧凑存储多个 ziplist 之间使用双向指针串接起来。 skipList 跳跃表
sorted set 类型的排序功能便是通过「跳跃列表」数据结构来实现。
跳跃表skiplist是一种有序数据结构它通过在每个节点中维持多个指向其他节点的指针从而达到快速访问节点的目的。
跳表在链表的基础上增加了多层级索引通过索引位置的几个跳转实现数据的快速定位如下图所示 跳跃表
整数数组intset
当一个集合只包含整数值元素并且这个集合的元素数量不多时Redis 就会使用整数集合作为集合键的底层实现节省内存。
单线程模型 ❝码哥寄语我们需要注意的是Redis 的单线程指的是 Redis 的网络 IO 6.x 版本后网络 IO 使用多线程以及键值对指令读写是由一个线程来执行的。 对于 Redis 的持久化、集群数据同步、异步删除等都是其他线程执行。 千万别说 Redis 就只有一个线程。
单线程指的是 Redis 键值对读写指令的执行是单线程。
先说官方答案让人觉得足够严谨而不是人云亦云去背诵一些博客。
官方答案因为 Redis 是基于内存的操作CPU 不是 Redis 的瓶颈Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现而且 CPU 不会成为瓶颈那就顺理成章地采用单线程的方案了。原文地址https://redis.io/topics/faq。 ❝为啥不用多线程执行充分利用 CPU 呢 在运行每个任务之前CPU 需要知道任务在何处加载并开始运行。也就是说系统需要帮助它预先设置 CPU 寄存器和程序计数器这称为 CPU 上下文。
切换上下文时我们需要完成一系列工作这是非常消耗资源的操作。
引入多线程开发就需要使用同步原语来保护共享资源的并发读写增加代码复杂度和调试难度。 ❝单线程有什么好处 不会因为线程创建导致的性能消耗 避免上下文切换引起的 CPU 消耗没有多线程切换的开销 避免了线程之间的竞争问题比如添加锁、释放锁、死锁等不需要考虑各种锁问题。 代码更清晰处理逻辑简单。
I/O 多路复用模型
Redis 采用 I/O 多路复用技术并发处理连接。采用了 epoll 自己实现的简单的事件框架。
epoll 中的读、写、关闭、连接都转化成了事件然后利用 epoll 的多路复用特性绝不在 IO 上浪费一点时间。 高性能 IO 多路复用
Redis 线程不会阻塞在某一个特定的监听或已连接套接字上也就是说不会阻塞在某一个特定的客户端请求处理上。正因为此Redis 可以同时和多个客户端连接并处理请求从而提升并发性。
Redis 全局 hash 字典
Redis 整体就是一个 哈希表来保存所有的键值对无论数据类型是 5 种的任意一种。哈希表本质就是一个数组每个元素被叫做哈希桶不管什么数据类型每个桶里面的 entry 保存着实际具体值的指针。 Redis 全局哈希表
而哈希表的时间复杂度是 O(1)只需要计算每个键的哈希值便知道对应的哈希桶位置定位桶里面的 entry 找到对应数据这个也是 Redis 快的原因之一。
Redis 使用对象redisObject来表示数据库中的键值当我们在 Redis 中创建一个键值对时至少创建两个对象一个对象是用做键值对的键对象另一个是键值对的值对象。
也就是每个 entry 保存着 「键值对」的 redisObject 对象通过 redisObject 的指针找到对应数据。
typedef struct redisObject{//类型unsigned type:4;//编码unsigned encoding:4;//指向底层数据结构的指针void *ptr;//...}robj;
Hash 冲突怎么办
Redis 通过链式哈希解决冲突也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能所以 Redis 为了追求快使用了两个全局哈希表。用于 rehash 操作增加现有的哈希桶数量减少哈希冲突。
开始默认使用 「hash 表 1 」保存键值对数据「hash 表 2」 此刻没有分配空间。当数据越来越多触发 rehash 操作则执行以下操作 给 「hash 表 2 」分配更大的空间 将 「hash 表 1 」的数据重新映射拷贝到 「hash 表 2」 中 释放 「hash 表 1」 的空间。
值得注意的是将 hash 表 1 的数据重新映射到 hash 表 2 的过程中并不是一次性的这样会造成 Redis 阻塞无法提供服务。
而是采用了渐进式 rehash每次处理客户端请求的时候先从「 hash 表 1」 中第一个索引开始将这个位置的 所有数据拷贝到 「hash 表 2」 中就这样将 rehash 分散到多次请求过程中避免耗时阻塞。
Redis 如何实现持久化宕机后如何恢复数据
Redis 的数据持久化使用了「RDB 数据快照」的方式来实现宕机快速恢复。但是 过于频繁的执行全量数据快照有两个严重性能开销 频繁生成 RDB 文件写入磁盘磁盘压力过大。会出现上一个 RDB 还未执行完下一个又开始生成陷入死循环。 fork 出 bgsave 子进程会阻塞主线程主线程的内存越大阻塞时间越长。
所以 Redis 还设计了 AOF 写后日志记录对内存进行修改的指令记录。 ❝面试官什么是 RDB 内存快照 在 Redis 执行「写」指令过程中内存数据会一直变化。所谓的内存快照指的就是 Redis 内存中的数据在某一刻的状态数据。
好比时间定格在某一刻当我们拍照的通过照片就能把某一刻的瞬间画面完全记录下来。
Redis 跟这个类似就是把某一刻的数据以文件的形式拍下来写到磁盘上。这个快照文件叫做 RDB 文件RDB 就是 Redis DataBase 的缩写。 RDB内存快照
在做数据恢复时直接将 RDB 文件读入内存完成恢复。 ❝面试官在生成 RDB 期间Redis 可以同时处理写请求么 可以的Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化保证数据一致性。
Redis 在持久化时会调用 glibc 的函数fork产生一个子进程快照持久化完全交给子进程来处理父进程继续处理客户端请求。
当主线程执行写指令修改数据的时候这个数据就会复制一份副本 bgsave 子进程读取这个副本数据写到 RDB 文件。
这既保证了快照的完整性也允许主线程同时对数据进行修改避免了对正常业务的影响。 写时复制技术保证快照期间数据可修改 ❝面试官那 AOF 又是什么 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列那么就可以通过对一个空的 Redis 实例顺序执行所有的指令也就是「重放」来恢复 Redis 当前实例的内存数据结构的状态。
Redis 提供的 AOF 配置项appendfsync写回策略直接决定 AOF 持久化功能的效率和安全性。 always同步写回写指令执行完毕立马将 aof_buf缓冲区中的内容刷写到 AOF 文件。 everysec每秒写回写指令执行完日志只会写到 AOF 文件缓冲区每隔一秒就把缓冲区内容同步到磁盘。 no 操作系统控制写执行执行完毕把日志写到 AOF 文件内存缓冲区由操作系统决定何时刷写到磁盘。
没有两全其美的策略我们需要在性能和可靠性上做一个取舍。 ❝面试官既然 RDB 有两个性能问题那为何不用 AOF 即可。 AOF 写前日志记录的是每个「写」指令操作。不会像 RDB 全量快照导致性能损耗但是执行速度没有 RDB 快同时日志文件过大也会造成性能问题。
所以Redis 设计了一个杀手锏「AOF 重写机制」Redis 提供了 bgrewriteaof指令用于对 AOF 日志进行瘦身。
其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中追加完毕后就立即替代旧的 AOF 日志文件了瘦身工作就完成了。 AOF重写机制(3条变一条) ❝面试官如何实现数据尽可能少丢失又能兼顾性能呢 重启 Redis 时我们很少使用 rdb 来恢复内存状态因为会丢失大量数据。我们通常使用 AOF 日志重放但是重放 AOF 日志性能相对 rdb 来说要慢很多这样在 Redis 实例很大的情况下启动需要花费很长的时间。
Redis 4.0 为了解决这个问题带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志通常这部分 AOF 日志很小。
于是在 Redis 重启的时候可以先加载 rdb 的内容然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放重启效率因此大幅得到提升。
Redis 主从架构数据同步
Redis 提供了主从模式通过主从复制将数据冗余一份复制到其他 Redis 服务器。 ❝面试官主从之间数据如何保证一致性 为了保证副本数据的一致性主从架构采用了读写分离的方式。 读操作主、从库都可以执行 写操作主库先执行之后将写操作同步到从库 Redis 读写分离 ❝面试官主从复制还有其他作用么 故障恢复当主节点宕机其他节点依然可以提供服务 负载均衡Master 节点提供写服务Slave 节点提供读服务分担压力 高可用基石是哨兵和 cluster 实施的基础是高可用的基石。 ❝面试官主从复制如何实现的 同步分为三种情况 第一次主从库全量复制 主从正常运行期间的同步 主从库间网络断开重连同步。 ❝面试官第一次同步怎么实现 主从库第一次复制过程大体可以分为 3 个阶段连接建立阶段即准备阶段、主库同步数据到从库阶段、发送同步期间新写命令到从库阶段 Redis全量同步 建立连接从库会和主库建立连接从库执行 replicaof 并发送 psync 命令并告诉主库即将进行同步主库确认回复后主从库间就开始同步了。 主库同步数据给从库master 执行 bgsave命令生成 RDB 文件并将文件发送给从库同时主库为每一个 slave 开辟一块 replication buffer 缓冲区记录从生成 RDB 文件开始收到的所有写命令。从库保存 RDB 并清空数据库再加载 RDB 数据到内存中。 发送 RDB 之后接收到的新写命令到从库在生成 RDB 文件之后的写操作并没有记录到刚刚的 RDB 文件中为了保证主从库数据的一致性所以主库会在内存中使用一个叫 replication buffer 记录 RDB 文件生成后的所有写操作。并将里面的数据发送到 slave。 ❝面试官主从库间的网络断了咋办断开后要重新全量复制么 在 Redis 2.8 之前如果主从库在命令传播时出现了网络闪断那么从库就会和主库重新进行一次全量复制开销非常大。
从 Redis 2.8 开始网络断了之后主从库会采用增量复制的方式继续同步。
增量复制用于网络中断等情况后的复制只将中断期间主节点执行的写命令发送给从节点与全量复制相比更加高效。
断开重连增量复制的实现奥秘就是 repl_backlog_buffer 缓冲区不管在什么时候 master 都会将写指令操作记录在 repl_backlog_buffer 中因为内存有限 repl_backlog_buffer 是一个定长的环形数组如果数组内容满了就会从头开始覆盖前面的内容。
master 使用 master_repl_offset记录自己写到的位置偏移量slave 则使用 slave_repl_offset记录已经读取到的偏移量。 repl_backlog_buffer
当主从断开重连后slave 会先发送 psync 命令给 master同时将自己的 runIDslave_repl_offset发送给 master。
master 只需要把 master_repl_offset与 slave_repl_offset之间的命令同步给从库即可。
增量复制执行流程如下图 Redis增量复制 ❝面试官那完成全量同步后正常运行过程中如何同步数据呢 当主从库完成了全量复制它们之间就会一直维护一个网络连接主库会通过这个连接将后续陆续收到的命令操作再同步给从库这个过程也称为基于长连接的命令传播使用长连接的目的就是避免频繁建立连接导致的开销。
哨兵原理连环问 ❝面试官可以呀知道这么多你知道 哨兵集群原理么 哨兵是 Redis 的一种运行模式它专注于对 Redis 实例主节点、从节点运行状态的监控并能够在主节点发生故障时通过一系列的机制实现选主及主从切换实现故障转移确保整个 Redis 系统的可用性。
他的架构图如下 Redis哨兵集群
Redis 哨兵具备的能力有如下几个 监控持续监控 master 、slave 是否处于预期工作状态。 自动切换主库当 Master 运行故障哨兵启动自动故障恢复流程从 slave 中选择一台作为新 master。 通知让 slave 执行 replicaof 与新的 master 同步并且通知客户端与新 master 建立连接。 ❝面试官哨兵之间是如何知道彼此的 哨兵与 master 建立通信利用 master 提供发布/订阅机制发布自己的信息比如身高体重、是否单身、IP、端口……
master 有一个 __sentinel__:hello 的专用通道用于哨兵之间发布和订阅消息。这就好比是 __sentinel__:hello 微信群哨兵利用 master 建立的微信群发布自己的消息同时关注其他哨兵发布的消息。 ❝面试官哨兵之间虽然建立连接了但是还需要和 slave 建立连接不然没法监控他们呀如何知道 slave 并监控他们的 关键还是利用 master 来实现哨兵向 master 发送 INFO 命令 master 掌门自然是知道自己门下所有的 salve 小弟的。所以 master 接收到命令后便将 slave 列表告诉哨兵。
哨兵根据 master 响应的 slave 名单信息与每一个 salve 建立连接并且根据这个连接持续监控哨兵。 INFO命令获取slave信息
Cluster 集群连环炮 ❝面试官除了哨兵以外还有其他的高可用手段么 有 Cluster 集群实现高可用哨兵集群监控的 Redis 集群是主从架构无法横向拓展。使用 Redis Cluster 集群主要解决了大数据量存储导致的各种慢问题同时也便于横向拓展。
在面向百万、千万级别的用户规模时横向扩展的 Redis 切片集群会是一个非常好的选择。 ❝面试官什么是 Cluster 集群 Redis 集群是一种分布式数据库方案集群通过分片sharding来进行数据管理「分治思想」的一种实践并提供复制和故障转移功能。
将数据划分为 16384 的 slots每个节点负责一部分槽位。槽位的信息存储于每个节点中。
它是去中心化的如图所示该集群由三个 Redis 节点组成每个节点负责整个集群的一部分数据每个节点负责的数据多少可能不一样。 Redis 集群架构
三个节点相互连接组成一个对等的集群它们之间通过 Gossip协议相互交互集群信息最后每个节点都保存着其他节点的 slots 分配情况。 ❝面试官哈希槽又是如何映射到 Redis 实例上呢 根据键值对的 key使用 CRC16 算法计算出一个 16 bit 的值 将 16 bit 的值对 16384 执行取模得到 0 16383 的数表示 key 对应的哈希槽。 根据该槽信息定位到对应的实例。
键值对数据、哈希槽、Redis 实例之间的映射关系如下 数据、Slot与实例的映射 ❝面试官Cluster 如何实现故障转移 Redis 集群节点采用 Gossip 协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail)它会将这条信息向整个集群广播其它节点也就可以收到这点失联信息。
如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数就可以标记该节点为确定下线状态 (Fail)然后向整个集群广播强迫其它节点也接收该节点已经下线的事实并立即对该失联节点进行主从切换。 ❝面试官客户端又怎么确定访问的数据分布在哪个实例上呢 Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例实现了哈希槽分配信息的扩散。
这样集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。
当客户端连接任何一个实例实例就将哈希槽与实例的映射关系响应给客户端客户端就会将哈希槽与实例映射信息缓存在本地。
当客户端请求时会计算出键所对应的哈希槽再通过本地缓存的哈希槽实例映射信息定位到数据所在实例上再将请求发送给对应的实例。 Redis 客户端定位数据所在节点 ❝面试官什么是 Redis 重定向机制 哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了客户端将请求发送到实例上这个实例没有相应的数据该 Redis 实例会告诉客户端将请求发送到其他的实例上。
Redis 通过 MOVED 错误和 ASK 错误告诉客户端。
MOVED
MOVED 错误负载均衡数据已经迁移到其他实例上当客户端将一个键值对操作请求发送给某个实例而这个键所在的槽并非由自己负责的时候该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。
同时客户端还会更新本地缓存将该 slot 与 Redis 实例对应关系更新正确。 MOVED 指令
ASK
如果某个 slot 的数据比较多部分迁移到新实例还有一部分没有迁移。
如果请求的 key 在当前节点找到就直接执行命令否则时候就需要 ASK 错误响应了。
槽部分迁移未完成的情况下如果需要访问的 key 所在 Slot 正在从 实例 1 迁移到 实例 2如果 key 已经不在实例 1实例 1 会返回客户端一条 ASK 报错信息客户端请求的 key 所在的哈希槽正在迁移到实例 2 上你先给实例 2 发送一个 ASKING 命令接着发发送操作命令。
比如客户端请求定位到 key 「公众号:码哥字节」的槽 16330 在实例 172.17.18.1 上节点 1 如果找得到就直接执行命令否则响应 ASK 错误信息并指引客户端转向正在迁移的目标节点 172.17.18.2。 ASK 错误
注意ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。
未完待续
本篇主要将 Redis 核心内容过了一遍涉及到数据结构、内存模型、 IO 模型、持久化 RDB 和AOF 、主从复制原理、哨兵原理、cluster 原理。