Redis 核心架构与原理

为什么 Redis 这么快?

核心数据类型及应用场景

数据类型底层实现典型场景
StringSDS (简单动态字符串)缓存、计数器、分布式锁
Hashziplist, hashtable存储对象(如用户信息)
Listquicklist消息队列、最新消息排行
Setintset, hashtable去重、交集/并集(共同好友)
ZSetziplist, skiplist排行榜、限流、带权重的消息队列

高效的数据结构

按前面“核心数据类型及应用场景”的顺序,这里依次介绍 String -> Hash -> List -> Set -> ZSet 对应的底层结构。

SDS (Simple Dynamic String)

Redis 的字符串不是 C 的 char*,而是 SDS。它把字符串长度等元信息放在头部,从而避免反复扫描。

D2 Diagram
qtopie.github.io
Dict / Hashtable (Hash)

Hash、Set 在元素变多后会转为哈希表实现。Redis 通过渐进式 rehash把扩容成本摊到多次请求中,避免一次性卡顿。

D2 Diagram
qtopie.github.io
ListPack (Hash 小对象编码)

当 Hash 元素较少且 field/value 较短时,Redis 会优先用 ListPack 做紧凑存储;规模变大后再转为 hashtable。

D2 Diagram
qtopie.github.io
QuickList (List)

Redis 的 List 并不是简单链表,而是 quicklist: 双向链表的每个节点里放一个紧凑的 listpack。

D2 Diagram
qtopie.github.io
IntSet (Set)

当 Set 里都是整数且元素较少时,Redis 会用 intset。它把整数放在一段连续内存里,极省空间。

D2 Diagram
qtopie.github.io
SkipList (ZSet)

Redis 的 Sorted Set (ZSet) 在数据量较大时采用跳表作为底层实现。它在普通有序链表的基础上增加了多层索引,通过“空间换时间”策略实现近似二分查找的效率。

D2 Diagram
qtopie.github.io
选型直觉(面试/实战高频)

持久化机制:AOF vs RDB


高可用与集群方案

主从复制 (Replication)

解决读写分离,主节点写,从节点读. 存在单点故障风险。

哨兵模式 (Sentinel)

在主从基础上增加监控和自动故障转移

Redis Cluster (分片集群)

解决单机内存瓶颈。

哈希标签 (Hash Tag) 与同槽保证

在 Redis Cluster 中,数据分片导致不同的 Key 可能散落在不同节点。如果尝试对跨节点的 Key 执行原子操作(如 MGET, MSET, Lua 脚本),会触发 CROSSSLOT 错误。

核心机制:Hash Tag 当一个 Key 包含 {} 符号时,Redis 只会对 {} 之间的内容进行哈希计算。

举例对比:

Key 名称实际参与哈希的部分说明
user:1001:profileuser:1001:profile对全名哈希,随机分配槽位
user:{1001}:profile1001只对 1001 哈希
user:{1001}:order1001只对 1001 哈希,必与上者同槽

Go 语言代码实现示例:

// 使用 {user100} 作为 Hash Tag 保证 profile 和 settings 在同一个 Slot
profileKey := "{user100}:profile"
settingsKey := "{user100}:settings"

// 此时可以使用 MSet 或在 Lua 脚本中同时操作它们,不会报 CROSSSLOT 错误
err := rdb.MSet(ctx, profileKey, "data1", settingsKey, "data2").Err()

注意事项(架构风险):


高频问题:穿透、击穿、雪崩

这是最考验实际项目经验的部分。

缓存穿透 (Cache Penetration)

  1. 参数校验。
  2. 缓存空对象(设置短 TTL)。
  3. 布隆过滤器 (Bloom Filter)

缓存击穿 (Cache Breakdown)

  1. 设置热点数据永不过期。
  2. 使用互斥锁 (Mutex Lock),只允许一个请求去加载 DB。

缓存雪崩 (Cache Avalanche)

  1. 过期时间加随机扰动值(防止同时失效)。
  2. 利用哨兵/集群实现高可用。
  3. 服务降级或限流。

Redis 常见问题

Q1:Redis 的过期删除策略 and 内存淘汰策略有什么区别?

Q2:如何实现分布式锁?需要注意什么?

  1. 必须设置过期时间(防止死锁)。
  2. Value 要具有唯一性(防止误删别人的锁)。
  3. 释放锁时要用 Lua 脚本保证原子性。

Q3:Redis 怎么保证和数据库的一致性?


进阶与调优

缓存的模式

模式工作方式优点缺点
旁路缓存 (Cache-Aside): 应用查缓存,无则查DB并入缓存。: 直接更新DB。简单,耦合度低。首次读取延迟;数据不一致风险(写后立即读到旧缓存)。
读穿 (Read-Through): 应用请求缓存,缓存未命中时自行从DB加载。应用代码简洁,无需处理DB逻辑。需缓存提供程序支持;缓存层责任重。
写穿 (Write-Through): 应用写入缓存,缓存立即同步写入DB。数据强一致性。写入延迟较高(等待双写完成)。
回写 (Write-Back / Write-Behind): 应用写入缓存后立即返回,缓存异步批量写入DB。写入性能极高。缓存崩溃时可能丢失数据;最终一致性。
环绕写 (Write-Around): 直接写入DB,绕过缓存。: 遵循旁路缓存模式。避免低频读数据污染缓存;适合写多读少。读取新写入数据时延迟高(首次必未命中);不适合写后立即读。
提前刷新 (Refresh-Ahead)刷新: 在缓存过期之前,系统自动异步从DB加载最新数据并刷新。消除读取时的 Cache Miss,提供极低延迟。预测不准会导致资源浪费;实现复杂度高。

Cache-Aside (旁路缓存)

D2 Diagram
qtopie.github.io

Read-Through (读穿)

D2 Diagram
qtopie.github.io

Write-Through (写穿)

D2 Diagram
qtopie.github.io

Write-Back (回写)

D2 Diagram
qtopie.github.io

Write-Around (环绕写)

D2 Diagram
qtopie.github.io

Refresh-Ahead (提前刷新)

D2 Diagram
qtopie.github.io

相关应用

Redis + Lua 脚本实现原子性扣减

-- KEYS[1]: 库存的 Key (例如 "item:1001:stock")
-- ARGV[1]: 想要扣减的数量 (例如 1)

local stockKey = KEYS[1]
local num = tonumber(ARGV[1])

-- 1. 获取当前库存
local currentStock = redis.call('GET', stockKey)

-- 2. 如果 Key 不存在,可以视业务情况返回特殊错误码(比如 -1)
if not currentStock then
    return -1
end

-- 3. 转换为数字进行比较
currentStock = tonumber(currentStock)
if currentStock < num then
    -- 库存不足,返回 -2
    return -2
else
    -- 4. 库存充足,进行扣减
    redis.call('DECRBY', stockKey, num)
    -- 返回扣减后的剩余库存
    return currentStock - num
end

参考