Appearance
Redis 基础入门
Redis 是什么?为什么这么快?单线程还是多线程?IO 多路复用是什么?本章从最基础的概念开始,建立对 Redis 的正确认知。
一、什么是 Redis?
Redis(Remote Dictionary Server)是 C 语言编写的开源、内存型、键值对存储数据库。它不仅是缓存,更是数据结构服务器,支持多种数据结构的原子操作。
Redis 的多重身份:
┌──────────────────────────────────────────────────────┐
│ Redis │
│ │
│ 缓存 → 热点数据放内存,降低数据库压力 │
│ 数据库 → 持久化存储,RDB/AOF 保证数据不丢 │
│ 消息中间件 → Pub/Sub + Stream 实现消息队列 │
│ 分布式锁 → SETNX + Lua 实现分布式互斥 │
│ 计数器 → INCR 原子自增,实现限流/统计 │
│ 会话存储 → 分布式 Session 共享 │
└──────────────────────────────────────────────────────┘二、Redis 可以用来干什么?
| 场景 | 使用方式 | 示例 |
|---|---|---|
| 缓存 | 热点数据放 Redis,减少 DB 查询 | 商品详情、用户信息 |
| 分布式锁 | SETNX + Lua 实现互斥 | 秒杀防超卖、重复提交 |
| 计数器 | INCR 原子自增 | 点赞数、浏览量、限流计数 |
| 排行榜 | ZSet 有序集合 | 游戏积分排行、热搜榜 |
| 消息队列 | List/Stream 实现 | 异步任务、延时队列 |
| Session 共享 | 分布式 Session 存储 | 多实例共享登录态 |
| 社交关系 | Set 交集/并集/差集 | 共同好友、关注列表 |
| 地理位置 | GEO 经纬度计算 | 附近的人、商家定位 |
三、Redis 有哪些数据结构?
3.1 五种基本类型
| 类型 | 结构 | 底层实现 | 典型场景 |
|---|---|---|---|
| String | key-value | SDS(简单动态字符串) | 缓存、计数器、分布式锁 |
| List | 有序列表 | quicklist(快速列表) | 消息队列、最新列表 |
| Set | 无序集合 | dict + intset | 标签、共同好友、抽奖 |
| ZSet | 有序集合 | skiplist + dict | 排行榜、延迟队列 |
| Hash | 键值对集合 | dict + ziplist | 对象存储、购物车 |
3.2 三种扩展类型
| 类型 | 用途 | 示例 |
|---|---|---|
| Bitmap | 位图,极省内存的布尔数组 | 签到统计、用户在线状态 |
| HyperLogLog | 基数统计(12KB 统计 2^64 个元素) | UV 统计、去重计数 |
| GEO | 地理位置计算 | 附近的人、商家距离 |
四、Redis 为什么快?
4.1 核心原因
Redis 快的 5 个原因:
1. 纯内存操作
所有数据在内存中,读写速度是纳秒级
内存访问 ≈ 100ns,磁盘访问 ≈ 10ms,差 10 万倍
2. 单线程模型(6.0 之前)
无锁竞争、无上下文切换开销
简单的命令处理流程,避免并发复杂性
3. IO 多路复用
一个线程同时监听多个 Socket 连接
非阻塞 IO + epoll 事件驱动
4. 高效的数据结构
专门设计的 SDS、ziplist、skiplist 等
时间/空间复杂度都经过优化
5. 简单的通信协议(RESP)
文本协议,解析开销小
批量命令支持(Pipeline)4.2 性能数据
单机 Redis QPS:
普通命令(GET/SET): 10W+ QPS
Pipeline 批量: 100W+ QPS
对比:
MySQL 单机 QPS: 5000 ~ 10000
Redis 比 MySQL 快 10 ~ 20 倍五、IO 多路复用
5.1 什么是 IO 多路复用?
一个线程同时监听多个网络连接,哪个连接有数据来了就处理哪个,没有数据就休眠,不浪费 CPU。
传统 BIO(阻塞 IO):
一个连接一个线程
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Thread 1 │ │ Thread 2 │ │ Thread 3 │
│ 连接 A │ │ 连接 B │ │ 连接 C │
│ 阻塞等待 │ │ 阻塞等待 │ │ 阻塞等待 │
└──────────┘ └──────────┘ └──────────┘
问题:1000 个连接需要 1000 个线程,大量资源浪费在等待上
IO 多路复用(非阻塞 IO):
一个线程管理所有连接
┌──────────────────────────────────┐
│ Thread │
│ │
│ epoll_wait(所有连接的 fd) │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ fd3 有数据 fd7 有数据 │
│ │ │ │
│ ▼ ▼ │
│ 处理 A 处理 B │
│ │
│ ★ 只处理有数据的连接,不空转 │
└──────────────────────────────────┘5.2 演进过程
select → poll → epoll
select:轮询所有 fd,O(n),最大 1024 个连接
poll: 轮询所有 fd,O(n),无连接数限制
epoll: 只返回就绪的 fd,O(1),Linux 专属
Redis 在 Linux 上使用 epoll5.3 epoll 三个核心函数
c
// 1. 创建 epoll 实例
int epfd = epoll_create(1024);
// 2. 注册要监听的 fd 和事件
struct epoll_event event;
event.events = EPOLLIN; // 可读事件
event.data.fd = socket_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);
// 3. 等待事件发生(阻塞直到有事件)
struct epoll_event events[1024];
int n = epoll_wait(epfd, events, 1024, -1);
// 只遍历就绪的 n 个 fd,而不是全部
for (int i = 0; i < n; i++) {
handle(events[i].data.fd);
}六、单线程 vs 多线程
6.1 Redis 为什么早期选择单线程?
单线程的优势:
1. 避免锁竞争
多线程操作共享数据需要加锁,锁竞争严重影响性能
Redis 的数据结构操作都是 O(1) 或 O(log N),单线程足够快
2. 避免上下文切换
线程切换需要保存/恢复 CPU 寄存器、栈、程序计数器
单线程没有切换开销
3. 代码简单
不需要考虑并发问题,代码可维护性高
所有命令都是原子执行的
4. 瓶颈不在 CPU
Redis 的瓶颈是内存和网络 IO,不是 CPU
单线程已足够将 CPU 利用率跑满6.2 Redis 6.0 多线程是怎么回事?
Redis 6.0 的多线程只用于网络 IO 的读写,命令执行仍然是单线程。
Redis 4.0(单线程):
┌──────────────────────────────────────────┐
│ 单线程做所有事 │
│ read → 解析 → 执行 → 写入 → write │
└──────────────────────────────────────────┘
Redis 6.0(IO 多线程):
┌──────────────────────────────────────────┐
│ IO Thread 1: read socket 1,2,3 │
│ IO Thread 2: read socket 4,5,6 │
│ IO Thread 3: read socket 7,8,9 │
│ │ │
│ ▼ │
│ Main Thread: 解析 → 执行 → 写入结果 │ ← 单线程
│ │ │
│ ▼ │
│ IO Thread 1: write socket 1,2,3 │
│ IO Thread 2: write socket 4,5,6 │
│ IO Thread 3: write socket 7,8,9 │
└──────────────────────────────────────────┘
★ 命令执行永远是单线程,不需要加锁
★ 只是网络读写多线程,提升 IO 吞吐量配置方式:
conf
# redis.conf
io-threads 4 # IO 线程数
io-threads-do-reads yes # 开启读多线程为什么只多线程化 IO?
因为 Redis 的性能瓶颈 90% 在网络 IO 上,而不是 CPU 计算。解决网络 IO 就能大幅提升性能,同时保持命令执行的简单性。
七、面试要点
Q1:Redis 为什么快?
五大原因:纯内存操作(纳秒级)、单线程无锁竞争、IO 多路复用(epoll)、高效的数据结构设计、简洁的 RESP 协议。
Q2:IO 多路复用是什么?
一个线程同时监听多个网络连接,只处理有数据的连接。Redis 使用 epoll(Linux),从 select/poll 的 O(n) 轮询优化为 O(1) 事件通知。
Q3:Redis 6.0 多线程是怎么回事?
只多线程化网络 IO 读写,命令执行仍然是单线程。因为瓶颈在网络 IO 而非 CPU,多线程 IO 可大幅提升吞吐量,同时保持命令执行的原子性。
