学习资料:小林 coding:https://www.xiaolincoding.com/
Redis 是什么
Redis 是一个键值数据库,将数据存储在内存中,一般用来做 MySQL 数据库的缓存,因为 MySQL 数据库存储在硬盘中。
Redis 与 Memcached 区别:Redis 支持的数据种类更多; Redis 支持持久化,可以将内存中的数据存储到硬盘中,这样在关机之后还可以恢复之前的数据 ;Redis 原生支持集群.
Redis 线程模型
IO 多路复用
举一个简单的例子:如果把服务器比作一个收银员 ,把每个客户端连接比作排队的顾客:
阻塞 IO:收银员盯着一个顾客,直到他掏出钱包点完餐。如果这个顾客动作慢,后面的人全得等着。
非阻塞 IO:收银员不停地问每个人:"你准备好了吗?"如果没准备好就问下一个。虽然不阻塞了,但收银员会累死(消耗大量 CPU)。
IO 多路复用 :收银员坐在柜台后,告诉大家:"谁准备好了谁就举手"。他只处理举手的人。这就是"多路"复用:一个收银员处理多个顾客的需求。
在 Linux 中,万物皆文件。每个网络连接都是一个文件描述符(File Descriptor, FD) 。 IO 多路复用的本质就是:系统内核负责监控这些 FD,当某个 FD 有数据可读或可写时,立刻通知应用程序。
① select (早期的平衡)做法:应用进程把要监控的 FD 塞进一个集合(BitsMap)传给内核。内核去轮询,有动静了就把进程唤醒。缺点:有上限:默认只能监听 1024 个连接。效率低:进程醒来后不知道谁动了,得把 1024 个 FD 全部遍历一遍 (O(n) 复杂度)。开销大:每次调用都要把集合从用户空间拷贝到内核空间。
② poll (select 的补丁)做法:改用链表存储 FD。进步:打破了 1024 的数量限制。缺点:依然需要线性遍历,连接数越多,性能下降越快。
③ epoll (现代高并发的神)这是 Redis、Nginx 高性能的核心秘密。做法:在内核中维护一棵红黑树,动态管理所有 FD,不用反复拷贝。利用回调机制。当某个 FD 就绪时,内核自动把它放进一个就绪链表。优点:极其高效:进程醒来后直接处理链表,时间复杂度 O(1)。无上限:支持的连接数仅受限于系统内存。
epoll 多路复用分为水平触发和边缘触发。水平触发就是只要文件描述符 FD 处于就绪状态就会一直不停地通知你。边缘触发是只在描述符的状态发生变化,例如从非就绪变成就绪的时候才回通知。
Redis 中的线程模型
Redis 前台有一个主线程,后台还有线程. Redis 的"单线程"是指网络请求的解析和键值对读写命令的执行是由一个线程完成的。除了处理请求的主回路,Redis 内部还有几个专门干"脏活累活"的后台线程,称为 BIO (Background I/O):
bio_close_file: 异步关闭大文件,防止关闭文件时的磁盘阻塞。
bio_aof_fsync: 异步将 AOF 日志刷入磁盘。
bio_lazy_free: 异步释放大块内存。比如你删除了一个拥有百万成员的 Hash Key,如果让主线程去释放内存,Redis 会卡住几秒,通过后台线程处理则完全无感。

Redis 为什么单线程还能这么快
1.Redis 是内存数据库,读写内存速度本身就很快.
2.Redis 单线程避免了多线程之间的上下文切换
3.Redis 采用了 IO 复用机制,一个 IO 应对许多任务,当有任务需要 IO 的时候就通知它。
Redis 持久化
Redis 是内存型数据库,存储数据都在内存中,但是如果关机的话就会导致数据丢失. Redis 持久化就是将内存中的数据写入到磁盘中,这样关机之后还可以恢复数据.
AOF 日志
AOF 日志是将数据库中执行的每一行指令都存储下来,将来恢复的时候只需要按照指令再重新执行一遍。


AOF 向硬盘中写回有三种策略:1.总是写回,也就是说在内存中写一次就向硬盘中更新一次 2.按秒写回,一秒钟更新一次 3.交由操作系统内核决定写回的时机。

AOF 有重写机制,随着数据库的运行, AOF 会变得越来越大,在适当的时机需要将 AOF 更新。AOF 重写是读取数据库中的每一个键值对,每一个键值对都用一条指令记录到 AOF 文件中,然后用新的 AOF 替换掉旧的. 像下面这样。

AOF 重写是由后台子进程 bgrewriteaof完成的。使用后台子进程有两个好处:
1.主进程可以继续处理数据,不需要阻塞。
2.子进程和主进程享有相同的内存,如果用线程的话会需要锁,从而导致性能下降。子进程采用写时复制机制,当需要写时从主进程的内存中复制一份,主进程和子进程就有了不同的数据副本。
但是主进程在重写时仍然会有写的操作,如果这样的话,后台子进程的数据就和主进程不一样了。为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 AOF 缓冲区 和 AOF 重写缓冲区。


RDB 快照
AOF 记录的是日志,所以恢复的时候需要把所有日志里面的指令都执行一遍,所以会很慢。RDB 就是直接记录内容,比如给所有数据拍一张照片,直接按照照片恢复就可以,恢复速度快。RDB 会对所有数据都记录,因此会占用比较多的时间。

在后台子进程复制数据时,主进程仍然可以写数据,使用的技术是写时复制。

混合持久化
AOP 数据丢失少,但是恢复速度慢。RDB 数据丢失多,但是恢复速度快。
混合持久化的做法是: AOF 重写时,先将当前内存数据以 RDB 格式写入开头,剩下的增量命令再以 AOF 格式追加在后面。
效果: 结合了 RDB 的加载快和 AOF 的少丢数据,是目前生产环境的主流配置。
Redis 集群
主从复制
为了解决单机模式的可靠性问题,我们给"老板(Master)"配几个"员工(Slave)"。原理: Master 负责写操作,并将数据异步同步给多个 Slave。Slave 负责读操作。优点: 实现了读写分离,极大提高了读并发能力。缺点: 不具备自动容灾恢复能力。如果 Master 挂了,需要人工手动把一个 Slave 提升为新的 Master,并通知所有客户端修改配置。

主从服务器之间的命令复制是异步进行的,也就是说主服务器将数据同步给从服务器时并不会等待同步完成之后再返回数据,而是主服务器自己执行完之后就会返回结果了。所以无法保证强一致性。
哨兵模式
为了实现高可用性(High Availability),哨兵模式诞生了。它是主从模式的"自动升级版"。
架构: 在主从架构基础上,额外部署一组哨兵节点。它们不存数据,只负责监控。
核心功能:监控: 哨兵会不断"心跳检测" Master 和 Slave 是否活着。自动故障转移(Failover): 当哨兵发现 Master 挂了,会自动投票选出一个 Slave 升级为新的 Master。配置中心: 客户端连接哨兵,哨兵会告诉客户端当前谁才是 Master。

切片集群模式

Slot 是哈希槽,一共有 16384 个哈希槽,平均分配到节点上,图里面的 Redis 实例 1 和 Redis 实例 2 就是节点. 首先先通过 CRC 算法将 key 映射到一个整数,然后将这个整数对 16384 取余,得到应该放到哪个哈希槽里面。这些哈希槽会自动映射到 Redis 节点上,也就是 Redis 实例 1 和 Redis 实例 2.
脑裂问题
Redis 集群一般是一个主节点搭配很多从节点, 客户端向主节点中写,主节点再同步到从节点中. 脑裂现象是指当主节点和从节点失去联系时,客户端还在向主节点中写入数据,此时这些数据没有同步到从节点中. 哨兵节点会发现主节点丢失,在剩余的从节点中选举出一个新的主节点,此时就有了两个主节点. 当之前的主节点和从节点恢复联系时,它已经被降级成从节点,之前客户端写入的数据会丢失。脑裂现象会导致客户端写入的数据丢失。
Redis 过期淘汰策略
过期删除策略
分为惰性删除和定期删除。惰性删除:只在访问 key 的时候检查,如果超过规定时间了更新,对 CPU 友好,但是对内存不友好,容易在内存中留有大量 key。定期删除:每隔一段时间随机抽选一批 key,如果有过期的就删除。
持久化时过期键如何处理
RDB
在生成 RDB 快照时会检验 key 是否过期,过期的 key 不会被保存到 RDB 文件中。在载入 RDB 快照时要看是主服务器还是从服务器,如果是主服务器会检验 key 是否过期,过期的 key 不会载入内存,如果是从服务器,所有的 key 都会被载入内存。
AOF
AOF 分为两个阶段:AOF 写入和 AOF 重写。AOF 写入时会将原来的旧记录写入日志,直到删除时向 AOF 文件末尾写入 DEL,告诉后面恢复的进程,这个数据已经死了。AOF 重写时会写入最新的键,过期的键不会被写入。
Redis 缓存设计
缓存雪崩
定义: 大量的缓存 Key 在同一时间集中失效,或者 Redis 直接宕机。
场景: 程序员为了图省事,给所有缓存设置了同样的 EXPIRE 3600s。
后果: 1 小时后,几万个 Key 同时消失。数据库就像在暴雨中失去所有排水系统的城市,瞬间被淹没。这和击穿的区别在于:击穿是一个点,雪崩是一大片。
解决方案:随机过期时间: 给过期时间加上一个随机抖动值(如 3600s + rand(0, 300)s),让 Key 错峰失效。双层缓存: 使用本地缓存(Guava/Caffeine)+ 远程 Redis。高可用架构: 开启 Redis 哨兵或集群,防止单点宕机导致的雪崩
缓存击穿
定义: 一个热点 Key(比如微博热搜、秒杀商品)在过期的一瞬间,同时有海量请求涌入。
场景: 某个明星突然官宣,该 Key 恰好到期了。
后果: 就像堤坝上唯一的排水口突然关了,洪水(千万级并发)在这一秒钟内全部绕过缓存,直冲数据库。数据库因为瞬间负荷过重而宕机。
解决方案:设置热点数据永不过期: 物理上不设置过期时间,后台异步刷新。互斥锁 (Mutex Lock): 只允许一个线程去查数据库并回写缓存,其他请求原地等待(或者重试)。在 Redis 中通常用 SETNX 实现。
设置热点数据永不过期: 物理上不设置过期时间,后台异步刷新这里举一个例子:
假设"热搜榜"每 10 分钟过期一次。
10:00:00:缓存过期。
10:00:01:恰好有 10 万个用户刷新微博。
结果:这 10 万个请求发现缓存没了,全都冲向数据库去计算新的榜单。数据库瞬间由于 CPU 100% 而宕机。
"逻辑过期"的做法(永不过期 + 异步刷新):在这种设计下,我们存入 Redis 的数据不再只是简单的榜单,而是一个包含**"逻辑过期时间"**的 JSON 对象。
第一步:存入数据
我们在 Redis 里设置这个 Key 时,不调用 EXPIRE 命令(物理上永不过期),但在 Value 里面带上一个时间戳:
第二步:读取与异步更新
当用户在 22:11:00 请求数据时:读取缓存:Redis 迅速返回了上面的 JSON。
判断逻辑过期:程序发现当前时间(22:11:00)已经超过了逻辑过期时间(22:10:00)。
开启异步任务:程序立刻把这份"旧数据"先返回给用户(用户体验好,不卡顿)。
后台启动一个独立的线程,去数据库计算最新的榜单。
更新缓存:后台线程算完后,把新榜单和新的逻辑过期时间(比如 22:20:00)写回 Redis
缓存穿透
定义: 查询一个根本不存在的数据(缓存里没有,数据库里也没有)。
场景: 黑客通过脚本恶意发送大量不存在的 userId(如 -1 或随机长字符串)。
后果: 缓存没命中,请求每次都直接打到数据库。数据库查无此果,也没法写回缓存。瞬间高并发请求可能直接把数据库"点点点"点死。
解决方案:缓存空对象: 即使数据库返回空,也把这个 null 存入 Redis,并设一个简短的过期时间(如 5 分钟)。布隆过滤器 (Bloom Filter): 在缓存前加一道门。布隆过滤器能快速判断一个 Key "一定不存在"或"可能存在"。如果判定不存在,直接拦截。