本文写于 2018 年 6 月——Redis 4.0 主线、Redis 5.0(Stream 类型)2018-10 发布前夜。
一、8 大数据类型应用场景
| 类型 | 场景 | 底层实现 | 说明 |
|---|---|---|---|
| String | 缓存 / 计数器 / 限流 / 分布式锁 / 共享 Session | int + SDS(简单动态字符串) | 最基础类型 |
| Hash | 缓存对象 / 购物车 | 压缩列表 / 哈希表 | 7.0 中,压缩列表已经废弃了,交由 listpack 来实现了 |
| List | 消息队列 | 双向链表 / 压缩列表 | 简单的字符串列表,最大长度为 2^32 - 1,40 亿。3.2 版本之后,底层数据结构就只由 quicklist 实现了 |
| Set | 点赞 / 共同关注 / 抽奖活动 | 哈希表 / 整数集合 | 无序并唯一的键值集合 |
| ZSet | 排行榜 / 电话排序 / 姓名排序 | 压缩列表 / 跳表 | 7.0 中,压缩列表已经废弃了,交由 listpack 来实现了 |
| BitMap(2.2 版新增) | 签到 / 用户登陆状态 / 布隆过滤器 | String | 统计二值状态 |
| HyperLogLog(2.8 版新增) | 百万级以上的网页 UV 计数 | 自定义 | 用于「统计基数」 |
| GEO(3.2 版新增) | 滴滴叫车 | Sorted Set | 地理坐标 |
| Stream(5.0 版新增) | 消息队列 | 自定义 | 完美地实现消息队列 |
1.1 选型决策
二、Redis 线程模型
2.1 单线程 vs 多线程
Redis 单线程指的是「接收客户端请求 → 解析请求 → 进行数据读写等操作 → 发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候是会启动后台线程(BIO)的:
- Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务
- Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程
2.2 Redis 6.0 引入多线程 I/O
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是 在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理。 所以大家不要误解 Redis 有多线程同时执行命令。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
2.3 6.0 线程数配置
| |
官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
2.4 Redis 启动后的线程组成
因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会 额外创建 6 个线程 (这里的线程数不包括主线程 ):
- Redis-server:Redis 的主线程,主要负责执行命令
- bio_close_file、bio_aof_fsync、bio_lazy_free:三个后台线程,分别异步处理关闭文件任务、AOF 刷盘任务、释放内存任务
- io_thd_1、io_thd_2、io_thd_3:三个 I/O 线程,io-threads 默认是 4,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力
三、Redis 为什么这么快
3.1 4 大核心原因
Redis 快的核心原因,可以用一句话概括:它干的活极其简单,而且它把单线程的优势发挥到了极致,同时通过操作系统底层的"高级外挂"解决了高并发的排队问题。
原因 1:绝大多数请求是纯内存操作
这是最根本的基础。Redis 的所有数据都存储在内存中,CPU 读写内存的速度(纳秒级别)比起读写机械硬盘或 SSD(微秒甚至毫秒级别)快了数万倍。
原因 2:避免了多线程的"内耗"
多线程虽然能并发,但也带来了高昂的代价。Redis 采用单线程,反而完美避开了这些性能杀手:
- 没有线程切换的开销:操作系统在切换线程时,需要保存当前线程的上下文并加载新线程的数据。单线程一个人干到底,零切换成本
- 没有锁的竞争:多线程访问共享资源必须加锁,加锁、解锁以及等待锁的操作非常重。Redis 单线程天然保证了所有操作都是原子性的,根本不需要锁
原因 3:I/O 多路复用技术(底层外挂)
“单线程"指的是 Redis 核心的网络事件处理器和键值对读写命令 是由一个线程串行执行的。但面对海量的客户端连接,单线程怎么可能忙得过来?
这就是 I/O 多路复用(I/O Multiplexing) 大显身手的地方。Redis 利用了操作系统底层的非阻塞 I/O 机制(在 Linux 上通常是 epoll)。
epoll 工作原理:
| |
epoll 优势:
- select:O(n) 遍历所有 fd
- poll:O(n) 遍历所有 fd
- epoll:O(1) 事件通知(内核维护就绪链表)
原因 4:精心优化的数据结构
| 数据结构 | 优化 |
|---|---|
| SDS | O(1) 长度获取、避免缓冲区溢出、二进制安全 |
| 跳跃表 | O(log N) 范围查询,替代红黑树 |
| 压缩列表 | 连续内存 + 紧凑编码 |
| dict(哈希表) | 渐进式 rehash,不阻塞 |
四、内存淘汰策略
4.1 8 大策略
| 策略 | 描述 |
|---|---|
| noeviction(默认) | 不淘汰,写入失败返回错误 |
| allkeys-lru | 所有 key 中淘汰最近最少使用 |
| allkeys-lfu(4.0+) | 所有 key 中淘汰最不经常使用 |
| allkeys-random | 所有 key 中随机淘汰 |
| volatile-lru | 设置过期的 key 中淘汰 LRU |
| volatile-lfu(4.0+) | 设置过期的 key 中淘汰 LFU |
| volatile-random | 设置过期的 key 中随机淘汰 |
| volatile-ttl | 设置过期的 key 中淘汰最近过期 |
4.2 选型建议
- 缓存场景(容忍丢失):
allkeys-lru/allkeys-lfu - 数据不能丢:
noeviction+ 监控告警 - 混合场景:
volatile-lru(仅淘汰过期 key)
五、持久化机制
5.1 三大方式
| 方式 | 文件 | 频率 | 性能 | 数据安全 |
|---|---|---|---|---|
| RDB | dump.rdb | 定时 / 手动 | 高 | 较低(可能丢失最后一次快照后的数据) |
| AOF | appendonly.aof | 每秒 | 中 | 较高(仅丢失 1s 数据) |
| 混合(4.0+) | dump + aof | - | 中高 | 高 |
5.2 RDB 触发
| |
5.3 AOF 三种策略
| |
六、Redis 集群方案
| 方案 | 数据分片 | 高可用 | 适用 |
|---|---|---|---|
| 主从复制 | ✗ | 手动故障切换 | 简单备份 |
| Sentinel | ✗ | 自动故障切换 | 中小规模 |
| Codis | ✓ | 手动 + 代理 | 国产分片方案 |
| Cluster | ✓(16384 slot) | 自动故障切换 | 大规模生产 |
七、Redis 经典问题
7.1 缓存雪崩
现象:大量 key 同时过期,请求打到数据库。
解决方案:
- 过期时间加随机值:
expire = base + random(0, 300) - 熔断降级:数据库压力过大时直接拒绝部分请求
- 缓存预热:系统启动时把热点数据加载到缓存
7.2 缓存穿透
现象:查询不存在的 key,每次都打到数据库。
解决方案:
- 空值缓存:
key -> null(短 TTL) - 布隆过滤器:先过布隆过滤器
- 接口校验:参数合法性检查
7.3 缓存击穿
现象:热点 key 突然过期,瞬间大量请求打到数据库。
解决方案:
- 分布式锁:第一个请求查 DB,后续请求等
- 永不过期:逻辑过期(不设 TTL,值带过期时间字段)
- 熔断降级:直接返回旧值
7.4 数据不一致
现象:数据库和缓存数据不一致。
解决方案:
- 先更新 DB,再删除缓存(Cache-Aside Pattern)
- 延迟双删:更新 DB → 删除缓存 → 异步延迟再删
- binlog 订阅:监听 binlog 异步更新缓存
- 设置较短 TTL:最终一致性兜底
八、写在最后
Redis 面试要点:
- 基础:8 大数据类型 + 选型
- 线程模型:单线程 vs 多线程 I/O、I/O 多路复用
- 快的原因:内存操作 + 无锁 + epoll + 优化数据结构
- 持久化:RDB / AOF / 混合
- 集群:主从 / Sentinel / Cluster
- 经典问题:雪崩 / 穿透 / 击穿 / 一致性
