2. Redis 基础回顾:数据结构详解与单线程模型
在深入业务削峰与缓存一致性方案之前,必须夯实 Redis 核心基础。本章将系统回顾五种主要数据结构的底层实现与适用场景,并剖析 Redis 单线程模型与 IO 多路复用的工作原理,为后续章节的架构设计打下坚实基础。
2.1 五种核心数据结构详解
Redis 提供了丰富的数据结构,每种结构在底层都有对应的编码方式(encoding),以适应不同数据量和操作模式。下表总结了五种结构的核心特性与典型应用:
| 数据结构 | 底层编码(主要) | 时间复杂度(关键操作) | 典型业务场景 |
|---|---|---|---|
| String | int / embstr / raw | GET/SET: O(1) | 计数器、分布式锁、对象缓存 |
| Hash | ziplist / hashtable | HGET/HSET: O(1) | 对象属性存储、用户信息 |
| List | quicklist(ziplist + linkedlist) | LPUSH/RPOP: O(1) | 消息队列、时间线、最新消息 |
| Set | intset / hashtable | SADD/SISMEMBER: O(1) | 标签系统、去重、抽奖 |
| Sorted Set | ziplist / skiplist + dict | ZADD: O(log N);ZRANGE: O(log N + M) | 排行榜、延时队列、限流 |
2.1.1 String — 最基础的构建块
String 是 Redis 最基础的数据类型,最大存储 512 MB。其底层编码根据值类型自动切换:
- int:当值为整数且可被 long 表示时使用,支持原子自增/自减。
- embstr:当字符串长度 ≤ 44 字节时,使用连续内存块一次性分配,减少内存碎片。
- raw:当字符串长度 > 44 字节时,使用 SDS(Simple Dynamic String)结构,支持动态扩容。
2.1.2 Hash — 对象属性容器
Hash 适合存储结构化对象,如用户信息、商品详情。底层编码在字段数少且值较小时使用 ziplist(紧凑列表),当字段数超过 hash-max-ziplist-entries(默认 512)或字段值超过 hash-max-ziplist-value(默认 64 字节)时,自动升级为 hashtable。
# 用户信息存储示例
HSET user:1001 name "张三" age 28 city "北京"
HGETALL user:1001
2.1.3 List — 有序队列
List 底层使用 quicklist 结构,它是 ziplist 与双向链表的混合体。每个 quicklist 节点是一个 ziplist,既保证了内存紧凑,又支持两端的高效插入删除。List 是构建消息队列的天然选择,但需注意 BRPOP / BLPOP 的阻塞特性。
LLEN 监控队列长度,或使用 Redis Stream 替代。
2.1.4 Set — 无序去重集合
Set 支持集合运算(交集、并集、差集),底层在元素全为整数且数量较少时使用 intset(有序整数数组),否则使用 hashtable。intset 的查找通过二分法实现,效率极高。
# 标签系统示例
SADD article:101 tag:java tag:redis tag:architecture
SINTER article:101 article:102 # 获取共同标签
2.1.5 Sorted Set — 带权重的有序集合
Sorted Set 是 Redis 最强大的数据结构之一,底层结合 skiplist(跳跃表)与 dict(哈希表)。skiplist 保证按 score 排序和范围查询的高效性,dict 保证按 member 查找的 O(1) 性能。当元素数少于 zset-max-ziplist-entries(默认 128)且所有元素长度小于 zset-max-ziplist-value(默认 64 字节)时,使用 ziplist 以节省内存。
# 排行榜示例
ZADD leaderboard 100 "userA" 200 "userB" 150 "userC"
ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前三名
2.2 Redis 单线程模型与 IO 多路复用
Redis 的核心处理模型是 单线程 Reactor 模型,即所有命令的读取、解析、执行、回复都在一个主线程中串行完成。这一设计并非性能瓶颈,反而带来了原子性、无锁竞争和实现简单的优势。
2.2.1 为什么单线程还能这么快?
- 内存操作:所有数据存储在内存中,读写延迟通常在微秒级。
- IO 多路复用:使用 epoll(Linux)/ kqueue(macOS)实现单线程管理数千个客户端连接。
- 避免上下文切换:单线程消除了多线程的锁竞争、上下文切换开销。
- 非阻塞 IO:网络读写采用非阻塞模式,结合事件循环高效调度。
2.2.2 IO 多路复用工作流程
Redis 的事件循环(aeEventLoop)通过 IO 多路复用函数同时监听多个 socket 的读写事件。当某个 socket 可读或可写时,事件循环将对应的回调函数(如 readQueryFromClient、sendReplyToClient)放入待处理队列,由主线程依次执行。
2.2.3 单线程模型下的原子性保证
由于所有命令在单线程中串行执行,Redis 天然具备原子性。这意味着:
- 单个命令(如 INCR、HSET)的执行不会被其他命令中断。
- 多个命令可以通过 MULTI / EXEC 事务保证原子性,但事务中的命令不会立即执行,而是排队后一次性提交。
- Lua 脚本在 Redis 中也是原子执行的,适合实现复杂业务逻辑。
# Lua 脚本原子扣减库存示例
local stock = redis.call('GET', KEYS[1])
if stock and tonumber(stock) > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
2.3 数据结构选型与性能对比
在实际业务中,选择合适的数据结构直接影响系统性能与内存效率。下表从内存占用、读写速度、适用场景三个维度进行对比:
| 数据结构 | 内存效率 | 读写速度 | 推荐场景 |
|---|---|---|---|
| String | 高(直接存储值) | 极快(O(1)) | 简单缓存、计数器、分布式锁 |
| Hash | 中(ziplist 时高) | 快(O(1)) | 对象属性、用户信息、配置项 |
| List | 中(quicklist 平衡) | 两端快(O(1)) | 消息队列、时间线、日志 |
| Set | 中(intset 时高) | 快(O(1)) | 去重、标签、好友关系 |
| Sorted Set | 低(skiplist 开销) | 中等(O(log N)) | 排行榜、延时队列、限流 |