Redis 核心机制深度解析——从数据结构到持久化与高可用
摘要
Redis 作为业界最广泛使用的内存键值存储系统,以其卓越的性能和丰富的数据结构著称。本文从底层数据结构出发,深入剖析 Redis 六大核心机制的实现原理,涵盖 SDS 动态字符串、跳表、压缩列表等底层编码,RDB 与 AOF 持久化方案,主从复制与哨兵集群的高可用架构,缓存淘汰策略及穿透/击穿/雪崩解决方案,最后结合 Spring Boot 给出生产级实战代码。全文配有 Mermaid 图解与详细对比表格,帮助读者构建完整的 Redis 知识体系。
一、Redis 核心数据结构及其底层实现
Redis 之所以能提供远超传统数据库的性能,核心在于其精心设计的内存数据结构。本节逐一剖析五种基本类型(String、Hash、List、Set、ZSet)的底层编码实现。
1.1 String 类型与 SDS 动态字符串
String 是 Redis 最基础的键值类型,但它的底层并非 C 语言的 char*,而是自定义的 SDS(Simple Dynamic String,简单动态字符串)。
SDS 结构定义:
struct sdshdr {
int len; // 已用长度
int free; // 空闲字节数
char buf[]; // 字节数组
};
SDS 相比 C 字符串的优势:
| 特性 | C 字符串 | SDS |
|---|---|---|
| 获取长度 | O(n) 遍历 | O(1) 读 len 字段 |
| 缓冲区安全 | 不检查,易溢出 | 自动扩容,内存安全 |
| 二进制安全 | 以 \0 结尾,不能存二进制 |
二进制安全,通过 len 判断结尾 |
| 修改次数 n 次 | 必然 n 次内存重分配 | 预分配机制,最多 log₂(n) 次 |
空间预分配策略: 修改后若 len < 1MB,分配 len 同等的 free 空间;若 len >= 1MB,固定分配 1MB free。这一设计大幅减少了内存重分配次数。
1.2 Hash 类型的两种编码
graph TD
A[Hash 键] --> B{条件判断}
B -->|"元素数<512 且 所有key/value长度<64字节"| C[ziplist 编码]
B -->|"不满足条件"| D[hashtable 编码]
C --> E[连续内存块, 紧凑存储]
D --> F[字典结构, O(1) 操作]
E --> G[查询 O(n) 但数据量小时更快<br/>CPU缓存友好]
F --> H[查询 O(1) 适合大数据量]
ziplist(压缩列表) 将所有键值对顺序存储在一块连续内存中,每个 entry 包含前一个 entry 的长度(用于反向遍历)、当前 entry 编码和实际数据。当 Hash 元素较少时,ziplist 的 CPU 缓存命中率远高于 hashtable。
encoding 阈值可控: 通过 hash-max-ziplist-entries 和 hash-max-ziplist-value 配置。
1.3 List 的 quicklist 实现
Redis 3.2 之后,List 底层使用 quicklist——双向链表节点 + 每个节点内部是 ziplist 的混合结构。
graph LR
A[quicklist 节点1] --> B[quicklist 节点2] --> C[quicklist 节点3]
A -.->|"ziplist
[e1,e2,e3]"| D[连续内存块]
B -.->|"ziplist
[e4,e5]"| E[连续内存块]
C -.->|"ziplist
[e6,e7,e8]"| F[连续内存块]
- 链表维度提供两端 O(1) 的 push/pop
- ziplist 节点利用内存局部性提升遍历性能
- 每个 ziplist 的大小通过
list-max-ziplist-size控制(可取正数表示 entry 数,取负数表示大小级别)
1.4 Set 的整数集合与字典
Set 底层采用两种编码:
- intset(整数集合):当所有元素都是整型且数量不超过 512 时,采用有序无重复的整型数组,二分查找实现 O(log n) 查询
- hashtable:不满足上述条件时升级为字典
1.5 ZSet 的跳表(Skip List)
ZSet(有序集合)使用 跳表 + 字典 双结构:
graph TD
A[ZSet 键] --> B[zskiplist 跳表]
A --> C[dict 字典]
B --> D[按 score 排序<br/>范围查询 O(log n)]
C --> E[按 member 查询 score<br/>O(1)]
D --> F[多层索引<br/>概率平衡]
跳表的核心原理:
每层是一个有序链表,最底层包含全部元素,上层是 “快速通道”。查找时从最高层开始,遇到大于目标值的节点就向下层跳转,平均时间复杂度 O(log n),最坏 O(n)。
// 跳表节点结构
typedef struct zskiplistNode {
sds ele; // 成员对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度
} level[]; // 层级数组
} zskiplistNode;
为什么 Redis 用跳表而不用平衡树?
1. 跳表实现更简单,代码量约树的 1/3
2. 范围查询时,跳表只需在底层链表上遍历,而平衡树需要中序遍历
3. 插入/删除只需调整局部指针,无需复杂的旋转/染色操作
二、Redis 持久化机制——RDB 与 AOF
Redis 是内存数据库,数据在进程重启后会丢失。持久化机制确保数据能从磁盘恢复。
2.1 RDB(Redis DataBase)快照持久化
RDB 将某一时刻的内存快照保存到 .rdb 文件中,适合全量备份与灾难恢复。
触发方式:
# 手动触发
127.0.0.1:6379> SAVE # 同步阻塞
127.0.0.1:6379> BGSAVE # 异步后台 fork
# 自动触发配置 (save )
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
BGSAVE 执行流程:
sequenceDiagram
participant C as Client
participant R as Redis 主进程
participant F as Fork 子进程
C->>R: BGSAVE 命令
R->>F: fork() 创建子进程
Note over F: 子进程获得主进程<br/>内存快照副本
F->>F: 将数据写入临时 RDB 文件
F->>R: 通知父进程完成
R->>R: 覆盖旧 RDB 文件
R->>C: OK
Copy-On-Write(写时复制)优化: fork 后父子进程共享物理内存。当主进程收到写请求时,操作系统复制被修改的内存页,子进程读取的依然是被修改前的快照版本。这保证了 RDB 快照的一致性,同时最小化内存开销。
2.2 AOF(Append Only File)日志持久化
AOF 记录每次写操作命令,以 Redis 协议格式追加到 .aof 文件中。恢复时逐条重放命令。
配置示例:
appendonly yes
appendfilename "appendonly.aof"
# 同步策略:always / everysec / no
appendfsync everysec
| 同步策略 | 数据安全 | 性能 | 说明 |
|---|---|---|---|
| always | 最安全(最多丢1个命令) | 最慢 | 每次写操作后 fsync |
| everysec | 最多丢1秒数据 | 中等 | 每秒 fsync 一次(推荐) |
| no | 最不安全(丢几十秒) | 最快 | 由操作系统决定刷盘时机 |
AOF 重写(Rewrite)机制:
AOF 文件持续增长会变得庞大。AOF 重写通过读取当前内存状态,生成最小化命令集:
# 手动触发
BGREWRITEAOF
# 自动触发配置
auto-aof-rewrite-percentage 100 # 文件增长超过100%时触发
auto-aof-rewrite-min-size 64mb # 最小重写大小
2.3 RDB vs AOF 对比
| 对比维度 | RDB | AOF |
|---|---|---|
| 文件大小 | 紧凑,二进制小 | 较大,记录所有操作 |
| 恢复速度 | 快(直接加载快照) | 慢(逐条重放命令) |
| 数据安全性 | 可能丢失最后一次快照后的数据 | 最多丢 1 秒数据(everysec) |
| 对性能影响 | fork 子进程,写时复制 | 写操作追加日志 |
| 可读性 | 二进制,不可读 | 文本协议,可读可修改 |
2.4 Redis 4.0 混合持久化
Redis 4.0 引入混合持久化(aof-use-rdb-preamble yes),AOF 重写后文件前半部分是 RDB 格式的快照,后半部分追加增量命令。兼顾了 RDB 的恢复速度与 AOF 的数据安全。
三、Redis 主从复制与高可用架构
3.1 主从复制
sequenceDiagram
participant S as Slave
participant M as Master
S->>M: SLAVEOF master_ip master_port
M->>S: 返回 +FULLRESYNC <runid> <offset>
M->>S: 发送 RDB 快照
Note over S: 清空旧数据<br/>加载 RDB
M->>S: 发送复制缓冲区积压命令
Note over S,M: 基于长连接的<br/>增量命令传播
S->>M: REPLCONF ACK <offset>
Note over S,M: psync 断线重连<br/>仅同步断线期间数据
复制核心机制:
- 全量同步——从库首次或重连无法部分同步时,主库 fork 子进程生成 RDB 发送给从库
- 部分同步(psync)——从库断线重连后,只需同步主库复制积压缓冲区(repl-backlog)中未同步的命令
- 复制偏移量——主从各自维护
master_repl_offset和slave_repl_offset,用于判断数据同步进度
配置示例:
# 主库配置
replica-serve-stale-data yes # 从库断连时是否继续提供服务
# 从库配置
replicaof 192.168.1.100 6379
replica-read-only yes
3.2 哨兵(Sentinel)模式
哨兵模式解决了主库故障时的自动故障转移问题。
graph TD
A[Sentinel 集群<br/>至少3个节点] --> B[监控主库]
A --> C[监控从库]
A --> D[通知客户端<br/>发生切换]
B --> E{主观下线<br/>quorum 投票}
E -->|多数哨兵同意| F[客观下线]
F --> G[选举 Leader Sentinel]
G --> H[执行故障转移]
H --> I[选新主库]
H --> J[配置从库指向新主]
哨兵配置 (sentinel.conf):
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
quorum 2:至少 2 个哨兵认为主库下线才执行故障转移parallel-syncs 1:新主库选出后,每次只允许 1 个从库同步,防止高负载
3.3 Redis Cluster 集群模式
Redis Cluster 实现了数据分片与高可用的统一:
- 16384 个槽位——
CRC16(key) % 16384决定 key 所属 slot - 无中心架构——每个节点都保存集群拓扑信息
- 自动故障转移——主节点宕机后,从节点自动晋升
集群通信流程(Gossip 协议):
CLUSTER MEET 192.168.1.101 6379 # 节点握手
节点间通过 Gossip 协议 交换 ping/pong 消息(每秒约 10 次),传播槽位分配、节点状态等信息。集群节点数为 N 时,每个节点维护的元数据复杂度为 O(N),所以官方建议集群规模不超过 1000 节点。
槽位迁移示例:
# 将 slot 0~1000 从节点 A 迁移到节点 B
redis-cli --cluster reshard 192.168.1.100:6379
四、缓存淘汰策略与三大缓存问题
4.1 内存淘汰策略
# 设置最大内存
maxmemory 4gb
# 淘汰策略
maxmemory-policy allkeys-lru
| 策略 | 作用范围 | 淘汰算法 | 适用场景 |
|---|---|---|---|
| noeviction | — | 不淘汰,写入报错 | 强一致性场景 |
| volatile-lru | 已设 TTL 的 key | LRU | 有时效的缓存 |
| allkeys-lru | 所有 key | LRU | 通用缓存(最常用) |
| volatile-lfu | 已设 TTL 的 key | LFU | 访问频率差异大的数据 |
| allkeys-lfu | 所有 key | LFU | 热点数据集中场景 |
| volatile-random | 已设 TTL 的 key | 随机 | 均匀访问场景 |
| allkeys-random | 所有 key | 随机 | 均匀访问场景 |
| volatile-ttl | 已设 TTL 的 key | TTL 最短 | 时效敏感数据 |
LRU vs LFU: LRU 淘汰最近最少使用的 key,但可能被”批量扫描一次大量冷数据”干扰;LFU(Redis 4.0+)统计访问频率,能更好保留长期热点数据。
4.2 缓存穿透、击穿与雪崩
缓存穿透(Cache Penetration): 查询一个不存在的数据,缓存和数据库都没有,每次请求都穿透到数据库。
// 解决方案一:缓存空值
public String get(String key) {
String cache = redis.get(key);
if (cache != null) return cache;
String dbValue = queryDB(key);
// 即使为空也缓存,设置短 TTL 防止长期占用
redis.set(key, dbValue == null ? "NULL" : dbValue,
dbValue == null ? 60 : 3600);
return dbValue;
}
// 解决方案二:布隆过滤器
public boolean mightContain(String key) {
// bit 数组 + 多个 hash 函数
// 判断 key 是否可能存在
return bloomFilter.mightContain(key);
}
缓存击穿(Cache Breakdown): 热点 key 在过期瞬间,大量请求同时打到数据库。
// 解决方案:互斥锁
public String getWithLock(String key) {
String cache = redis.get(key);
if (cache != null) return cache;
String lockKey = "lock:" + key;
// SET NX 实现分布式锁,设置超时防止死锁
if (redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS)) {
try {
cache = queryDB(key);
redis.set(key, cache, 3600);
return cache;
} finally {
redis.del(lockKey);
}
} else {
// 没拿到锁的线程等待并读取
Thread.sleep(50);
return redis.get(key);
}
}
缓存雪崩(Cache Avalanche): 大量 key 在同一时间过期,或 Redis 实例宕机。
# 解决方案:
# 1. 过期时间分散
redis.set(key, value, baseTTL + random.nextInt(300));
# 2. 多级缓存(本地缓存 + Redis)
# 3. 熔断降级
# 4. Redis 高可用(主从 + 哨兵)
五、Spring Boot 集成 Redis 实战
5.1 依赖与配置
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 4
max-wait: -1ms
5.2 RedisTemplate 配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// JSON 序列化
Jackson2JsonRedisSerializer<Object> jsonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LazyValidatorPolyModule.INSTANCE,
ObjectMapper.DefaultTyping.NON_FINAL);
jsonSerializer.setObjectMapper(om);
// String 序列化 key
StringRedisSerializer stringSerializer =
new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
5.3 缓存注解实战
@Service
@CacheConfig(cacheNames = "user")
public class UserService {
@Cacheable(key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 方法返回结果自动缓存
return userMapper.selectById(id);
}
@CachePut(key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
@CacheEvict(key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
}
5.4 分布式锁
// 要求: set key value NX EX seconds 原子性
// RedisTemplate 封装
public boolean tryLock(String key, String value, long expireSec) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, value, expireSec, TimeUnit.SECONDS)
);
}
public void releaseLock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(script);
redisTemplate.execute(redisScript, Collections.singletonList(key), value);
}
5.5 Spring Boot + Redisson 最佳实践
Redisson 提供了开箱即用的分布式锁、信号量、队列等高级功能:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
// 可重入锁
@Autowired
private RedissonClient redisson;
public void businessMethod() {
RLock lock = redisson.getLock("business:lock");
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 执行业务逻辑
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
六、总结
本文从五个维度完整剖析了 Redis 的核心机制:
- 数据结构底层——SDS 解决了 C 字符串的性能和安全问题;ziplist 在数据量小时提供超紧凑存储;跳表简化了范围查询的实现
- 持久化机制——RDB 适合备份和快速恢复,AOF 数据更安全,混合持久化兼顾二者优势
- 高可用架构——主从复制解决读写分离,哨兵解决自动故障转移,Cluster 解决水平扩展
- 缓存策略——LRU/LFU 淘汰策略的选择取决于访问模式;穿透、击穿、雪崩各有对应解决方案
- 生产实践——Spring Boot 集成 Redis 时需关注序列化配置、连接池优化、分布式锁的原子性
Redis 的设计哲学可以总结为:在正确的场景用最合适的数据结构。理解这些底层机制,才能在生产中做出最优的架构决策。


暂无评论内容