文章目录
- 下载链接
- Redis
- 缓存
- 缓存雪崩
- 缓存击穿
- 缓存穿透
- 数据库和缓存的数据一致性问题
- 持久化
- AOF日志
- RDB快照
- AOF和RDB合体
- 大key问题
- 功能
- 过期删除策略
- 内存淘汰策略
- 高可用
- 主从复制
- 哨兵机制
- 数据类型
- 数据类型
下载链接
链接: https://pan.baidu.com/s/1hRTh7rSesikisgRUO2GBpA?pwd=utgp 提取码: utgp
Redis
缓存
缓存雪崩
-
原因
- 为了保证数据一致性,Redis的数据有过期时间,当数据过期后会重新访问访问数据库生成缓存
-
现象
- 大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机,用户数据请求全部堆积到数据库
-
解决
-
大量数据同时过期
-
均匀设置过期时间
- 给数据的过期时间设置随机值,确保不会在同一时间过期
-
互斥锁(要设置超时时间避免故障)
- 确保同一时间内只有一个请求在从数据库中获取缓存,其他请求等待或者返回null
-
后台更新缓存
-
业务线程不设置缓存过期,由后台线程定时更新缓存
-
更新策略
-
后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存
- 由于有间隔时间,体验一般
-
在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存
- 这个比较及时,用户体验不错
-
-
-
在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是缓存预热,后台更新缓存的机制适合干这个事情
-
-
Redis故障宕机
-
服务熔断或请求限流机制
-
服务熔断
-
直接暂停业务应用对缓存服务的访问,直接返回错误,等恢复正常了再允许业务访问缓存
-
保护数据库的正常访问,但是暂停了业务应用访问缓存服务,全部业务都无法正常工作
-
-
请求限流
-
只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等Redis恢复了再说
-
这个比直接熔断要强一点,熔断直接所有服务都不能用了,这个能用一点,大规模使用还是要等Redis恢复后先预热
-
-
-
构建Redis缓存高可靠集群
- 最好通过主从节点的方式构建 Redis 缓存高可靠集群,搭配哨兵机制更加智能化
-
-
缓存击穿
-
现象
- 缓存中会有一些热点数据访问频率很高,如果这些数据突然过期了,大量请求同时访问数据库就会造成缓存击穿
-
解决
- 其实缓存击穿可以理解成是缓存雪崩的一个子集,所以直接使用缓存雪崩的互斥锁和后台更新缓存策略即可
缓存穿透
-
原因
- 用户要访问的数据,既不在Redis缓存,也不在数据库,所以此次寻找既使用了数据库,还没有缓存到Redis
-
现象
- 如果有大量的恶意请求请求的是不存在的数据,那么就会绕过Redis,直接重复访问数据库
-
解决
-
非法请求的限制
- 对于很明显的恶意请求,直接拒绝访问,避免访问数据库
-
缓存空值或者默认值
- 对于找不到的请求,可以缓存一个null值,这样下次进行询问返回null即可
-
使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
- 写入数据库数据时,使用布隆过滤器做个标记,这样请求也不会访问到数据库,最多是Redis压力大
-
数据库和缓存的数据一致性问题
-
并发问题
-
先更新数据库,再更新缓存
-
请求a更新数据库,请求b更新数据库和缓存,请求a的缓存最后更新,此时数据不一致
- 有解决方式,在总结部分
-
-
先更新缓存,再更新数据库
- 请求a更新缓存,请求b更新缓存和数据库,请求a的数据库最后更新,此时数据不一致
-
先删除缓存,再更新数据库
- 请求a要更新数据,请求b获取缓存为数据库值,请求a更新值,数据不一致
-
先更新数据库,再删除缓存
-
请求a获取数据库值,请求b更新数据库并删除缓存,请求a写回缓存,数据不一致
- 不过概率不高,所以正常来说可以保证
-
-
-
旁路缓存(Cache Aside)策略
-
概念
-
先删除缓存,再更新数据库
-
先更新数据库,再删除缓存
-
-
问题
-
先删除缓存,再更新数据库不能保证数据一致,需要使用延迟双删
-
先更新数据库,再删除缓存可以保证数据一致,但是前提是两个操作都要完成,否则也会不一致
-
-
解决
-
先删除缓存,再更新数据库
-
延迟双删
-
先删缓存,更新,一段时间再删
- 总的来说不可靠,不如另外一个
-
-
-
先更新数据库,再删除缓存
- 这个基本可以保证,但是缓存删除会对命中率造成影响
-
-
-
总结
-
追求缓存命中率,使用更新数据库+更新缓存,二选一
-
在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存
-
更新完缓存,给缓存加上较短的过期时间,就算缓存不一致,也会很快过期
-
-
总的来说,建议先更新数据库,再删除缓存,不更新缓存
-
缓存可能和多个数据表有关,与其全部更新,不如直接删掉
- Lazy Loading:适用于那些加载代价大的操作
-
-
-
不管是什么情况,如何保证两个操作都能顺利完成
-
核心
- 采用异步操作缓存
-
重试机制
- 把要更新的缓存数据放到消息队列中,多试几次,尽可能把缓存的数据删了
-
订阅 MySQL binlog,再操作缓存
- 拿到数据库中的binlog操作,再删除缓存
-
持久化
AOF日志
-
先进行写操作,再把写操作写到日志中保存
-
顺序的意义
-
优点
-
避免语法错误带来的检查开销
- 只记录有效的操作
-
不会阻塞当前命令的执行
- Redis是单线程
-
-
缺点
-
可能有丢失风险
- 执行后立即掉电
-
可能给下一个命令带来阻塞
- 硬盘IO时间很长
-
-
-
写回策略
-
总体概述
-
执行写操作,命令进入server.aof_buf缓冲区
-
通过write()调用写到AOF文件
- page cache
-
内核把缓冲区刷新到磁盘
-
-
三种策略
-
Always
- 每次操作都同步到磁盘
-
Everysec
- 先写到AOF缓冲区,每隔一秒写入一次磁盘
-
No
- 先写到AOF缓冲区,由操作系统决定什么时候写
-
-
但是三种方法都不能解决顺序带来的缺点,因为这是对立的概念
-
-
源码实现
-
三种策略不同,体现在fsync()执行的不同时机
- 应用程序向文件写数据,内核先把数据写到缓冲区,再排入队列,内核最终决定何时刷新到磁盘
-
Always就是每次写入AOF文件后就立刻fsync()
-
Everysec就是创建一个异步任务定期执行fsync()
-
No就是永远不会执行fsync()
-
-
重写机制
-
原因
-
AOF是日志文件,内容会不断增大,文件过大会导致恢复过慢
-
当文件内容超过一定限度后,就会触发重写机制,压缩文件
-
-
做法
-
重写时读取数据库的所有键值对,读取到新的AOF文件,最后替换
-
比如写了两次键值对,删了一次,这三个操作可以重写为插入一个键值对
-
-
-
AOF后台重写
-
AOF重写是一个很耗时间的工作,因此需要后台子进程来完成
- bgrewriteaof
-
优点
-
AOF重写期间不影响主进程处理请求
-
子进程拥有数据副本,不用加锁保证安全,不用负担加锁开销
- 父子进程之间写时拷贝
-
-
缺点
-
阻塞主进程
-
创建子进程的途中,由于要复制父进程的页表等数据结构
-
创建子进程后,如果发生写时复制,也可能阻塞
-
-
数据不一致
-
重写后,如果主进程执行修改操作,会导致数据不一致
- AOF 重写缓冲区
-
-
-
AOF 重写缓冲区
-
为了解决父子进程可能的读写不一致,在创建子进程后会有此缓冲区
-
在重写期间,主进程需要
-
执行命令
-
写入AOF缓冲区
- 写入AOF重写缓冲区
-
-
-
-
子进程完成重写,会向主进程发送一个信号
-
主进程执行信号处理函数(阻塞)
-
将AOF重写缓冲区追加到新AOF中
-
修改名字,改为新的并覆盖AOF文件
-
-
-
RDB快照
-
介绍
-
RDB保存的是二进制数据
-
就是记录某一刻的数据情况
-
RDB 文件的加载工作是在服务器启动时自动执行
- 恢复数据快
-
-
-
-
使用
-
save
- 在主线程生成RDB文件,可能会阻塞主进程
-
bgsave
- 创建一个子进程生成文件,避免阻塞主进程
-
-
配置
-
Redis可以设置,每隔一定的时间自动执行bgsave
-
save time count
-
time秒内,对数据库进行至少count次操作
- 自动执行
-
-
-
-
数据修改
-
基于写时拷贝技术,Redis可以在快照过程中继续修改数据
- 但是会导致数据不一致,只能交给下一次快照
-
AOF和RDB合体
-
混合使用AOF日志和内存快照,混合持久化
- Redis 4.0提出的
-
原因
-
RDB频率太低
- 数据丢失严重
-
RDB频率太高
- 性能开销大
-
AOF数据丢失不严重
- 回放的速度太慢
-
-
原理
-
AOF重写日志时,子进程先把主进程的内容RDB快照一份
-
主进程在这个阶段进行的操作写到重写缓冲区中
- 以AOF的方式写到AOF文件中
-
混合持久化:前半部分是RDB格式的全量数据,后半部分是AOF的增量数据
-
-
优点
- 重启Redis数据时,前半部分是RDB内容,加载速度快,后半部分是AOF内容,使得数据更少丢失
大key问题
-
大key对持久化的影响
-
AOF日志
-
Always策略
- 如果是大key,进程执行fsync()阻塞时间会更久
-
Everysec策略
- 异步执行,不影响
-
No策略
- 不影响
-
-
AOF重写和RDB快照
-
由于有大key,因此会很快触发AOF重写机制
- RDB快照触发不影响
-
这两种方式都会涉及fork页表的复制,如果页表大,那么复制过程会阻塞
-
-
解决fork耗时太长
-
控制单个实例在10GB以内可以迅速返回
-
如果Redis当做缓存,则直接关闭AOF和AOF重写,直接避免调用fork
-
在主从架构中,避免全量同步,即避免RDB快照,避免调用fork
-
-
写时拷贝
-
主进程复制页表到子进程,大key会导致页表大,导致阻塞
-
触发写时拷贝,大key导致拷贝物理内存带来的阻塞
-
-
-
大key的其他影响
-
客户端超时阻塞
-
引发网络阻塞
-
阻塞工作现场
-
内存分布不均匀
- 集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大
-
-
解决措施
-
最好在设计阶段,就把大 key 拆分成一个一个小 key
-
定时检查 Redis 是否存在大 key
-
不要用del删除
- unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程
-
功能
过期删除策略
-
概念
- Redis可以设置对key进行过期时间,时间到自动删除
-
用法
-
设置过期时间
-
expire
- key 在 n 秒后过期
-
pexpire
- key 在 n 毫秒后过期
-
expireat
- key 在某个时间戳(秒)之后过期
-
pexpireat
- key 在某个时间戳(毫秒)之后过期
-
-
查看存活时间
- TTL
-
取消过期时间
- PERSIST
-
-
原理
-
如何判定key过期
-
设置了过期后
- Redis 会把该 key 带上过期时间存储到一个过期字典
-
过期字典存储在 redisDb
- typedef struct redisDb {
dict dict; / 数据库键空间,存放着所有的键值对 */
dict expires; / 键的过期时间 */
…
} redisDb;
- typedef struct redisDb {
-
过期字典数据结构
-
字典实际上是哈希表
-
查询一个key值,先看在不在过期字典中
-
不在
- 正常读值
-
在
-
获取过期时间
-
没过期
- 正常读取
-
过期
- 判定过期了
-
-
-
-
-
-
过期删除策略
-
定时删除
-
在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作
-
优点
- 可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放,对内存友好
-
缺点
- 删除过期 key 可能会占用相当一部分 CPU 时间,对CPU不友好
-
-
惰性删除
-
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key
-
优点
- 策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好
-
缺点
- 只要这个过期 key 一直没有被访问,它所占用的内存就不会释放。对内存不友好
-
-
定期删除
-
每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期key
-
优点
- 对CPU和内存都稍微友好一点
-
缺点
-
不如定时删除和惰性删除的优点
-
删除操作执行的时长和频率不好控制,太频繁和太少都不行
-
-
-
-
Redis的过期删除策略
-
惰性删除+定期删除
- 合理使用 CPU 时间和避免内存浪费之间取得平衡
-
实现惰性删除
-
在访问或者修改 key 之前,调用函数对其进行检查,检查 key 是否过期
-
过期
-
同步/异步删除
- 返回null
-
-
没过期
-
不处理
- 正常返回
-
-
-
-
实现定期删除
-
间隔时长
-
默认每秒进行 10 次过期检查一次数据库
- 随机抽取一定数量的 key
-
-
抽查数量
- 写死的数量:20
-
流程
- 随机抽取20个,如果已过期的数量超过了25%,就再次抽取20个
-
-
-
内存淘汰策略
-
概念
- 当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行
-
分类
-
不进行数据淘汰
-
noeviction(3.0之后默认)
- 不淘汰数据,但是也不能新增,可以查询删除
-
-
进行数据淘汰
-
在设置了过期时间的数据中进行淘汰
-
volatile-random
- 随机淘汰设置了过期时间的任意键值
-
volatile-ttl
- 优先淘汰更早过期的
-
volatile-lru(3.0之前默认)
- 淘汰所有设置了过期时间的键值中,最久未使用的键值
-
volatile-lfu(4.0之后新增)
- 淘汰所有设置了过期时间的键值中,最少次使用的键值
-
-
在所有数据范围内进行淘汰
-
allkeys-random
- 随机淘汰任意值
-
allkeys-lru
- 淘汰最久没使用过的
-
allkeys-lfu(4.0后新增)
- 淘汰使用次数最少的
-
-
-
-
LRU和LFU的区别
-
LRU
- Least Recently Used:最近最少使用
-
LFU
- Least Frequently Used:最近最不常用
-
-
Redis如何实现LRU
-
传统的LRU会有链表空间开辟和节点移动带来的时间成本的问题
-
Redis实现
-
在对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间
-
进行淘汰会随机选择5个值,淘汰这5个值里面最久没有使用的那个
-
-
优点
-
不用为所有的数据维护一个大链表,节省了空间占用
-
不用在每次数据访问时都移动链表项,提升了缓存的性能
-
-
缺点
-
无法解决缓存污染问题
- 大量数据只被使用一次,以后就不用了
-
-
-
Redis如何实现LFU
-
在对象结构体中,包含最后访问时间和访问频率
-
访问频率默认是5,随着时间推移会逐渐降低
- 判断依据是根据当前访问时间和最后访问时间来判定频率
-
每次访问都会增加访问频率,但是增加量越来越少
-
高可用
主从复制
-
原因
-
单台服务器宕机就无法访问,最好有数据备份
- 和MySQL的架构模式差不多
-
-
方式
- 借助replicaof来形成主服务器和从服务器的关系
-
第一次同步
-
第一阶段是建立链接、协商同步(为了全量复制做准备)
-
执行replicaof后,从服务器给主服务器传递psync命令,表示我想和你进行同步了
-
主服务器的 runID
- 初始化为?
-
复制进度 offset
- 初始化-1
-
-
主服务器收到psync命令后,会用FULLRESYNC作为响应传递给从服务器
-
主服务器的 runID
-
主服务器目前的复制进度 offset
-
-
-
第二阶段是主服务器同步数据给从服务器
-
主服务器会执行 bgsave 命令来生成 RDB 文件,传递给从服务器
-
从服务器,会先清空当前的数据,然后载入 RDB 文件
-
-
第三阶段是主服务器发送新写操作命令给从服务器
-
从服务器把RDB装载完成后,回复一个确认消息
-
主服务器把replication buffer的写操作同步到从服务器
-
-
-
命令传播
-
主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接
-
基于长连接的命令传播
- 主服务器的写操作会传播给从服务器
-
-
分摊主服务器压力
-
从服务器可以有从服务器,因此生成RDB和传输RDB可以分摊
-
replicaof <目标从服务器的IP> 6379
-
-
增量复制
-
如果从服务器连接断开,重新恢复连接后如何保证数据一致
-
Redis2.8之前
- 从服务器和主服务器进行全量复制
-
Redis2.8之后
- 主从服务器采用增量复制的方法
-
-
主服务器会有一个环形缓冲区,记录的是主从读写位置
-
如果已经套圈了,那么就得全量复制了
-
如果没有套圈,那么直接进行增量复制
-
把增量命令写到缓冲区
- 传播给从服务器即可
-
-
-
-
面试题
-
Redis主从节点时长连接还是短连接?
-
怎么判断 Redis 某个节点是否正常工作?
-
Redis 主节点默认每隔 10 秒对从节点发送 ping 命令
-
Redis 从节点每隔 1 秒给主节点上报自身当前的复制偏移量
-
实时监测主从节点网络状态
-
上报自身复制偏移量, 检查复制数据是否丢失
-
-
-
主从复制架构中,过期key如何处理?
- 如果主节点删除了一个过期的key,那么主节点模拟一条del命令发送给从节点
-
Redis 是同步复制还是异步复制?
- 先写到内部缓冲区,再异步发送给从节点
-
主从复制中两个 Buffer有什么区别?
-
replication buffer
-
增量复制阶段
- 一个主节点只分配一个
-
如果满了就直接进行覆盖(环状)
-
-
repl backlog buffer
-
全量复制阶段和增量复制阶段
- 一个从节点一个
-
如果满了直接断开连接,全量复制
-
-
-
如何应对主从数据不一致?
-
原因
-
主从节点间的命令复制是异步进行的,不能保证时时刻刻一致
-
主节点给从节点的写命令,从节点还没来得及执行就被访问
-
-
解决
-
保证主从节点间的网络连接状况良好
-
开发一个外部程序来监控主从节点间的复制进度
-
查到主、从节点的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从节点和主节点间的复制进度差值了
-
进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,这样就可以减少读到不一致数据的情况
-
-
-
-
主从切换如何减少数据丢失?
-
异步复制同步丢失
-
原因
- 命令传播的时候掉电丢失数据
-
解决
- Redis有一个参数,表示一旦所有的从节点数据复制和同步的延迟都超过了这个参数定义的值,那么主节点就会拒绝接收任何请求
-
-
集群产生脑裂数据丢失
-
原因
-
主节点和从节点的网络出现了问题,和从节点全部断开连接
-
客户端依旧给主节点写数据,但是数据不能同步到从节点,全部都存储在主从之间的缓冲区中
-
哨兵发现此时主节点失联了,哨兵就会从 从节点 中选出一个作为新的主节点,但是实际上主节点还在
-
脑裂就出现了,此时网络又正常了,主从之间恢复通信
- 哨兵会把旧的主节点变为从节点
-
此时这个旧的主节点缓冲区内的信息就会被清空,然后装入新的主节点的信息,全量同步
-
-
解决
-
当主节点发现从节点下线的数量太多,或者网络延迟太大的时候,那么主节点会禁止写操作,直接把错误返回给客户端
-
所以即使主从之间连接断了,哨兵选出来一个新的主节点,也没事,因为连接断了以后就不能写数据了
-
-
-
-
主从如何做到故障自动切换?
-
主节点挂了 ,从节点是无法自动升级为主节点的,这个过程需要人工处理,在此期间 Redis 无法对外提供写操作
-
此时,Redis 哨兵机制就登场了,哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性
-
-
哨兵机制
-
为什么要有哨兵机制
- 主节点挂了就全挂了,所以找个哨兵来实现主从节点故障转移
-
哨兵机制如何工作
-
哨兵节点本质上就是一个特殊的Redis进程,也是一个节点
-
哨兵节点主要负责三件事情:监控、选主、通知
-
-
如何判断主节点真的故障
-
哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行
-
误判
-
为了防止单个哨兵因为网络问题导致无回应,所以会有3+机器来组成哨兵集群,减少误判
- 3个和投票有关系
-
-
主观下线( 适用于主从节点)
- 没有在规定时间响应,就标记为主观下线
-
客观下线(只适用于主节点)
-
当一个主节点被判定主观下线后
-
此时会发起投票,如果其他哨兵也认为可以下线,就会标记为客观下线
- 在多个从节点中选一个为主节点
-
-
-
-
由哪个哨兵进行主从故障转移
-
机制
-
进行主从故障转移需要选举出leader
- 哪个哨兵节点判断主节点为客观下线,这个哨兵节点就是候选者
-
-
候选者如何选举成为 Leader
-
拿到半数以上的赞成票
-
拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值
-
-
为什么哨兵节点至少要有 3 个
- quorum 的值建议设置为哨兵个数的二分之一加 1
-
-
主从故障转移的过程
-
选出新主节点
-
首先得选个网络状态好的
-
其次根据优先级、复制进度、ID 号
- 相同情况比下一个
-
-
将从节点指向新主节点
-
向「从节点」发送 SLAVEOF 命令
- 让从节点变为新主节点
-
-
通知客户的主节点已更换
- 哨兵会向+switch-master频道发布新主节点的IP端口
-
将旧主节点变为从节点
-
旧主节点上线后,哨兵会发SLAVEOF
- 让旧主节点变为从节点
-
-
-
哨兵集群是如何组成
-
哨兵之间如何建立联系?
-
哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的
- 名为__sentinel__:hello的频道
-
-
哨兵集群如何知道从节点的信息
- 主节点知道,所以哨兵每10s发INFO命令获取从节点信息
-
数据类型
数据类型
-
String
-
介绍
- value可以是字符串,数字等
-
内部实现
-
数据结构
-
int
-
SDS(简单动态字符串)
-
和C语言的字符串差不多,但是比它强
-
不仅可以保存文本数据,还可以保存二进制数据
-
获取字符串长度的时间复杂度是 O(1)
-
API 是安全的,拼接字符串不会造成缓冲区溢出
-
-
-
编码方式
-
int
- 如果是整数并且可以用long表示,调整编码方式,把数据保存在ptr中
-
embstr
-
如果是字符串,长度较短,就使用embstr进行优化
-
只读的
- 修改需要将embstr修改为raw
-
-
raw
- 如果是字符串,长度较长,就使用raw
-
-
-
常用命令
-
set
-
get
-
exists
-
strlen
-
del
-
mset
-
mget
-
-
-
-
-
-
-
-