1.Redis架构
- 访问框架
- 网络访问框架
- 索引模块
- 基于不同value类型的操作模块
- 存储模块
- 持久化(AOF/RDB)
- 高可用集群支撑模块
- 主从复制
- 哨兵机制
- 高可扩展集群支撑模块
- 数据分片
2. Redis数据结构底层实现
为了实现从key到value的快速访问,Redis使用了一个<u>哈希桶</u>来保存,哈希桶里的元素是指向具体指的指针。计算key的hash值就可以知道哈希桶位置从而访问到相应的entry
潜在问题
-
哈希表的冲突问题
- 负载因子:哈希表中的K-V对数量/哈希表长度
- 解决方式:链式哈希
-
rehash可能带来的操作阻塞
rehash过程:增加现有的哈希桶数量,让逐渐多的entry能在更多的桶之间分散保存,减少哈希冲突
-
渐进式hash
把一次性的大量拷贝开销分摊到多次处理请求中避免耗时操作
-
扩展/收缩策略(尽可能避免在子进程做持久化的时候进行rehash避免不必要的内存写入操作)
- 服务器目前没在执行BGSAVE或者BGREWRITEAOF命令(持久化)并且哈希因子>=1
- 服务器正在执行BGSAVE或者BGREWRITEAOF命令并且哈希因子>=5
-
<img src="https://cdn.meishakeji.com/study/Redis%E5%85%A8%E5%B1%80%E5%93%88%E5%B8%8C%E8%A1%A8.png" style="zoom:50%;" />
底层数据结构
整数数组
双向链表
哈希表
压缩列表
-
跳表
在链表的基础上增加了多级索引通过索引位置的跳转快速定位数据
<img src="https://cdn.meishakeji.com/study/Redis%E8%B7%B3%E8%A1%A8.png" style="zoom:50%;" />
3.Redis快的原因
-
单线程
redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,redis的其他功能如持久化、异步删除、集群同步等是由额外的线程执行的
高效的数据结构
-
基于多路复用的高性能I/O模型 epoll机制(基于事件的回调机制)
- 一个Redis线程处理多个IO流 一旦检测到FD上有请求到达时就会触发相应的事件
- 事件被放进一个事件队列,Redis单线程对事件队列不断进行处理,调用相应的处理函数实现基于事件的回调
- 这就像病人去医院瞧病。在医生实际诊断前,每个病人(等同于请求)都需要先分诊、测体温、登记等。如果这些工作都由医生来完成,医生的工作效率就会很低。所以,医院都设置了分诊台,分诊台会一直处理这些诊断前的工作(类似于 Linux 内核监听请求),然后再转交给医生做实际诊断。这样即使一个医生(相当于 Redis 单线程),效率也能提升。
<img src="https://cdn.meishakeji.com/study/%E5%9F%BA%E4%BA%8E%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8%E7%9A%84redis%E9%AB%98%E6%80%A7%E8%83%BDIO%E6%A8%A1%E5%9E%8B.png" style="zoom:50%;" />
4.Redis如何避免数据丢失
-
错误方案:从数据库恢复
频繁访问数据 从慢速数据库中读取出来性能上比不上redis读
正确方案:redis要实现数据持久化
-
AOF持久化方案
-
实现:先执行命令把数据写入内存后记录日志(DB写日志是实际写数据前把修改的数据记到日志文件中)
- 日志内容:redis收到的每一条命令
- 实现的好处:避免出现记录错误命令的情况避免额外的检查开销;不会阻塞当前的写操作
- 实现的潜在风险:执行完还没来得及日志就挂了;给下一个操作带来阻塞风险
-
写回策略
配置项 写回时机 优点 缺点 always 同步写回 可靠性高 数据基本不丢失 性能影响较大 everysec 每秒写回 性能适中 宕机时丢失1s的数据 no 操作系统控制写回 性能好 宕机时丢失数据较多
-
-
AOF文件瘦身
-
一个拷贝
每次执行重写时,主线程fork出后台的bgrewriteaof子进程,此时fork会把子进程的内存拷贝一份给bgrewriteaof子进程(copy on write)这里面就包含了redis数据库的最新数据。bgrewriteaof子进程就在可以不影响主进程的情况下逐一把拷贝的数据写成操作计入重写日志
-
两处日志
主线程如果有写操作,第一处日志就是指正在使用的AOF日志,redis会把这个操作写到缓冲区。第二处日志是指新的AOF重写日志。这样重写日志也不会丢失最新的操作。
<img src="https://cdn.meishakeji.com/study/AOF%E9%9D%9E%E9%98%BB%E5%A1%9E%E7%9A%84%E9%87%8D%E5%86%99%E8%BF%87%E7%A8%8B.png" style="zoom:50%;" />
-
5.Redis如何实现快速恢复
RDB持久化
-
命令:bgsave 创建一个子进程专门用于写入RDB文件避免主进程阻塞。redis借助操作系统的写时复制技术(copy-on-write)在执行快照时主线程正常进行写操作
增量快照:做了一次全量快照后后续的快照只对修改的数据进行快照记录避免每次全量快照的开销,为了记住修改引入的额外空间开销较大
缺点:快照的频率不好把握,频率太低两次快照间挂掉的话有较多数据丢失,频率太高又会产生较大额外开销
-
终极方案:AOF混合RDB
内存快照以一定频率执行,在两次快照之间使用AOF日志记录期间命令执行
-
注意:子进程创建后不会阻塞主进程,但是<u>fork操作本身会阻塞主进程</u>
<img src="https://cdn.meishakeji.com/study/COW%E6%9C%BA%E5%88%B6%E4%BF%9D%E8%AF%81RDB%E5%BF%AB%E7%85%A7%E6%9C%9F%E9%97%B4%E6%95%B0%E6%8D%AE%E5%8F%AF%E4%BF%AE%E6%94%B9.png" style="zoom:50%;" />
6.Redis主从如何实现数据一致
-
redis的高可靠性
- AOF/RDB保证数据尽量少丢失
- 增加副本冗余量保证服务尽量少中断
-
Redis主从模式
读操作:主库、从库都可以接收
写操作:首先到主库执行,主库将写操作同步给从库
-
主从库第一次同步的流程
-
第一阶段
从库和主库建立起连接并告诉主库即将进行同步,发送psync命令(包含主库的runId和复制进度 第一次不知道主库ID 和进度)。主库收到psync命令后用FULLRESYNC命令响应
-
第二阶段
主库执行bgsave命令生成RDB文件,将文件发给从库。从库通过replicaof命令和主库同步,收到RDB文件后先清空现有数据然后加载RDB文件。主库同步数据给从库的过程中主库不会被阻塞,主库会在内存中用专门的replication buffer记录RDB文件生成后收到的所有写操作
用RDB不用AOF的原因:RDB文件是经过压缩的二进制文件(不同数据类型做了针对性优化)文件小,传输时对主库网络带宽消耗小。RDB存的是二进制文件从库按照RDB协议解析还原数据非常快而AOF需要依次重放每个命令。
-
第三阶段
主库完成RDB文件发送后把此时replication buffer中的修改操作发送给从库,从库重新执行这些操作,最终实现主从同步。
-
-
主从级联模式(主-从-从)分担全量复制时的主库压力
- 主库耗时操作:生成RDB文件(fork操作阻塞)、传输RDB文件(占用主库网络带宽)
<img src="https://cdn.meishakeji.com/study/%E4%B8%BB%E4%BB%8E%E5%BA%93%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%90%8C%E6%AD%A5.png" style="zoom:50%;" />
-
非首次的同步
基于长连接的命令传播(避免频繁建立连接的成本)
网络断了之后增量同步,把主从库网络断连期间主库收到的命令同步给从库
-
增量复制时主从保持同步
主库记录写到的位置,从库记录自己已经读到的位置。主从库的连接恢复之后,从库会给主库发送psync命令,把自己的偏移量发给主库主库判断偏移量的差距
<img src="https://cdn.meishakeji.com/study/redis%E7%9A%84repl_backlog_buffer%E7%8E%AF%E5%BD%A2%E7%BC%93%E5%86%B2%E5%8C%BA.png" style="zoom:50%;" />
-
潜在风险
因为repl_backlog_buffer是一个环形缓冲区所以在缓冲区写满之后主库会继续写入此时会覆盖之前写的操作,如果从库读取速度比较慢就有可能导致从库还未读取的操作被主库新写的操作覆盖了导致主从不一致
7.Redis哨兵机制
哨兵其实就是一个运行在特殊模式下的redis进程。哨兵主要负责的三个任务:监控、选择主库、通知
-
监控
哨兵在进程运行时周期性给所有的主从库发送PING命令,检测它们是否仍然在线运行。如果主库或从库没有在规定时间内响应哨兵的PING命令,哨兵就把它标记为下线状态,主库下线的话开始自动切换主库的流程
-
选主
- 引入多个哨兵实例一起判断主机是否下线,避免单个哨兵因为自身网络状况不好而误判主库下线
- 选主策略
- 按照在线状态、网络状态筛选过滤一部分不符合要求的从库
- 按照优先级、复制进度、ID号大小对剩余从库大分
-
通知
- 让从库执行replicaof与新主库同步
- 通知客户端与新主库连接
-
哨兵集群的运行机制
- redis提供了pub/sub(发布/订阅)机制,使得哨兵之间可以相互发现。哨兵只要和主库建立了连接,就可以在主库上发布消息(比如发布自己的连接信息IP和端口)
- 哨兵知道从库的连接信息靠的是哨兵向主库发送INFO命令,之后和从库建立连接并进行监控
- 选取哨兵执行主从切换
- raft算法选举
8. Redis切片集群
-
redis如何保存更多数据
-
纵向扩展:升级单个Redis实例的资源配置,包括内存、硬盘、CPU。
- 优点:实施简单直接
- 缺点:受硬件成本限制大;数据量增大fork阻塞时间久
-
横向扩展:增加当前Redis实例的个数
-
数据切片后在多个实例之间如何分布
- Redis Cluster方案采用哈希槽来处理数据和实例之间的映射关系,映射过程:一个集群有16384个哈希槽,根据key按照CRC16算法计算一个16bit的值 ,对16384取模,对应一个编号的哈希槽。哈希槽映射成具体的Redis实例的过程:均分或者手动分配(按照具体实例的配置)
- 哈希槽和实例的对应关系不是一成不变的,最常见的变化有:集群中有实例新增或删除Redis需要重新分配哈希槽,为了负载均衡Redis需要把哈希槽在所有实例重新分布一遍
- 实例之间可以通过相互传递信息获取最新的哈希槽分配信息
-
客户端怎么确定想要访问的数据在哪个实例上
-
重定向机制
客户端会缓存分配信息,缓存结果和分配结果不一致的时候,目标实例会给客户端发送MOVED命令告诉客户端新实例的访问地址且更新本地缓存(前提是数据已经全部迁移到新的实例了)。还有一种情况是数据迁移没全部完成,此时会返回ASK命令表明数据还在迁移中且把客户端请求数据的最新实例地址返回给客户端但不会更新缓存
-
-
-
9.Redis阻塞点分析
-
交互时产生操作
- 客户端:网络I/O、键值对增删改查操作、数据库操作
- 磁盘:生成RDB快照、记录AOF、AOF日志重写
- 主从节点:主库生成、传输RDB文件、从库接受RDB文件、清空数据库、加载RDB文件
- 切片集群实例:向其他实例传输哈希槽信息、数据迁移
判断操作复杂度是否算高:操作复杂度是否为O(N)
-
阻塞点
集合全量查询和聚合操作(读操作是关键路径操作无法异步操作)
-
bigkey删除
删除本质是要释放键值对占用的内存空间,为了更加高效管理内存空间操作系统需要把释放掉的内存块插入一个空闲内存块的链表,阻塞当前释放内存的应用程序,增加空闲内存块链表操作时间相应造成redis主线程阻塞(可异步执行)
清空数据库(可异步执行)
AOF日志同步写(可异步执行)
加载RDB文件(关键路径操作)
10.Redis变慢
- 从慢查询命令开始排查,根据业务需求替换慢查询命令
- 排查过期key的时间设置,根据实际使用需求设置不同的过期时间
11.Redis秒杀
- 秒杀前
- 使用CDN把页面静态化元素缓存起来
- 秒杀活动开始
12.Redis 事务机制
特性 | 情况 | |
---|---|---|
原子性(Atomicity) | 1. 执行EXEC命令前客户端发送的命令本身有错误被判断出来了,事务中的所有命令都不会被执行,保证了原子性 2.事务操作入队时命令和操作的数据类型不匹配没有检查出来错误,执行的时候Redis会对错误命令报错但还是会把正确的命令执行完此时原子性得不到保证 3.执行事务的Exec命令时Redis实例故障,如果开启了AOF日志可以保证原子性 | |
一致性(Consistency) | 有保证 | |
隔离性(Isolation) | 1.并发操作在EXEC命令前执行此时隔离性的保证要用WATCH机制来实现 2.并发操作在EXEC命令后执行的话可以保证隔离性 | |
持久性(Durability) | 得不到保证 |
13.Redis内存碎片
-
内因:内存分配器的分配策略
Redis默认使用jemalloc,按照一系列固定大小划分内存空间,如2KB、4KB、8KB等,当程序申请的内存最接近某个固定值时给它分配相应大小的空间(这样是为了减少分配次数)
-
外因:键值对不小不一样和删改操作
键值对的删改会导致空间的扩容和释放,占用额外的空间或者释放不用的空间形成空闲空间
14.Redis并发访问问题
- 加锁
- 系统并发性能降低
- 基于单个Redis节点实现分布式锁
- lock_key unique_value NX PX 10000
- unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示
- NX选项表示not exist即键值对不存在时才设置
- PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁。
- 释放锁时匹配unique_value避免误释放
- lock_key unique_value NX PX 10000
- 基于多个Redis节点实现高可靠的分布式锁
- redlock算法
- 原子操作
- 单命令操作:把多个操作在Redis中实现成一个操作
- RMW操作(读取-修改-写回 read-modify-write)不符合原子性
- INCR/DECR
- Lua脚本:把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本
- 单命令操作:把多个操作在Redis中实现成一个操作
15.Redis主从机制的坑
坑 | 原因 | 解决方案 |
---|---|---|
主从数据不一致 | 主从数据异步复制 | 使用外部监控程序对比主从库复制进度,不让客户端从落后的从库中读取数据 |
读到过期数据 | 过期数据删除策略 | 1. 使用Redis3.2及以上版本 2. 使用EXPIREAT/PEXPIREAT命令给数据设置过期时间点 |
不合理配置项导致服务挂掉 | protected-mode、cluster-node-timeout配置不合理 | 1. 设置protected-mode为no 2. 调大cluster-node-timeout |
16.Redis数据倾斜问题
倾斜类型 | 原因 | 解决方案 |
---|---|---|
数据量倾斜 | 存在bigkey | 1. 业务层避免创建bigkey 2. 把集合类型的bigkey拆分成多个小集合 |
slot手工分配不均 | 运维规范 | |
使用hash tag 导致大量数据集中到一个slot | 不使用hash tag | |
数据访问倾斜 | 存在热点数据 | 采用带有不同key前缀的多副本方法 |