前言

Redis作为当今最流行的开源内存数据库,以其卓越的性能、丰富的数据类型和灵活的功能,在互联网架构中扮演着至关重要的角色。本文将深入探讨Redis的核心原理,帮助读者全面理解Redis的内部工作机制。

基本概念

Redis(Remote Dictionary Server)是一个基于内存的高性能键值数据库,由意大利程序员Salvatore Sanfilippo(antirez)开发。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等,并具备以下特点:

  • 高性能:基于内存操作,读写性能极高
  • 持久化:支持数据持久化到磁盘
  • 丰富的数据类型:支持多种复杂的数据结构
  • 原子操作:所有操作都是原子性的,支持事务
  • 高可用:支持主从复制和集群模式

Redis的应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌───────────────────┐     ┌───────────────────┐     ┌───────────────────┐
│ │ │ │ │ │
│ 缓存系统 │ │ 计数器系统 │ │ 消息队列 │
│ │ │ │ │ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Redis │
│ │
└─────────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ │
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ │ │ │ │ │
│ 分布式锁系统 │ │ 排行榜系统 │ │ 实时分析系统 │
│ │ │ │ │ │
└───────────────────┘ └───────────────────┘ └───────────────────┘

核心数据结构

Redis的优势之一是支持多种数据结构,这些数据结构在内部由不同的实现方式支持。

简单动态字符串(SDS)

Redis没有直接使用C语言的字符串,而是自己构建了一种名为简单动态字符串(Simple Dynamic String, SDS)的抽象类型。

1
2
3
4
5
┌─────────────┬─────────────┬─────────────┐
│ len │ alloc │ buf │
├─────────────┼─────────────┼─────────────┤
│ 已使用长度 │ 总分配长度 │ 字节数组 │
└─────────────┴─────────────┴─────────────┘

相比C字符串,SDS具有以下优势:

  • 常数复杂度获取字符串长度:O(1)而非O(n)
  • 防止缓冲区溢出:SDS API会自动检查空间是否满足需求
  • 减少内存重分配次数:通过预分配和惰性释放策略
  • 二进制安全:可以存储任意二进制数据,不仅仅是文本

字典(Dict)

Redis的字典使用哈希表实现,采用链地址法解决冲突。字典是Redis实现数据库和哈希键的底层结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────┐
│ Dict │
└───────────────┬─────────────────────────────────────┘


┌───────────────────────────────────────────────────────────┐
│ dictht[2] │
└───────┬─────────────────────────────────────────┬─────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ dictht[0] │ │ dictht[1] │
│ (当前哈希表) │ │ (新哈希表) │
└─────────┬─────────┘ └───────────────────┘


┌──────┬──────┬──────┬──────┬──────┬──────┐
│ -- │ -- │ -- │ -- │ -- │ -- │
├──────┼──────┼──────┼──────┼──────┼──────┤
│ -- │ ▼ │ -- │ ▼ │ -- │ -- │
└──────┴──┬───┴──────┴──┬───┴──────┴──────┘
│ │
▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ 键值对│ ---> │ 键值对│ ---> │ 键值对│
└──────┘ └──────┘ └──────┘

Redis字典的特点:

  • 渐进式rehash:扩容时不是一次性完成,而是分多次完成,减少对服务器性能的影响
  • 负载因子触发扩容:当哈希表中元素数量达到表大小的一定比例时,触发扩容
  • 多次操作触发收缩:当元素减少到一定程度时,表大小会收缩以节约内存

跳跃表(SkipList)

跳跃表是一种有序数据结构,平均查找复杂度为O(logN),在Redis中用于实现有序集合(Sorted Set)。

1
2
3
4
5
6
7
8
9
10
Level 4  -∞ ───────────────────────────────────────────────────────────► +∞

Level 3 -∞ ───────────────────────────────► 30 ────────────────────────► +∞

Level 2 -∞ ───────────────────► 20 ────────► 30 ────────────────────────► +∞

Level 1 -∞ ────────► 10 ───────► 20 ────────► 30 ───────► 40 ──────────► +∞

Level 0 -∞ ──► 5 ───► 10 ────► 15 ──► 20 ───► 25 ──► 30 ──► 35 ──► 40 ──► +∞

跳跃表的特点:

  • 多层链表结构:通过概率方式维护层次高度
  • 快速查找:根据层次结构快速定位元素
  • 高效范围查询:适合区间操作,如ZRANGEBYSCORE命令
  • 空间换时间:相比平衡树,实现更简单但略占空间

整数集合(IntSet)

整数集合是Redis用于保存整数值的集合抽象数据类型,当一个集合只包含整数元素并且数量不多时使用。

1
2
3
4
5
┌───────────┬─────────┬───────┬───────┬───────┬───────┬───────┬───────┐
│ encoding │ length │ 1 │ 2 │ 3 │ 4 │ ... │ N │
├───────────┼─────────┼───────┼───────┼───────┼───────┼───────┼───────┤
│ 编码类型 │ 元素数量 │元素1 │元素2 │元素3 │元素4 │ ... │元素N │
└───────────┴─────────┴───────┴───────┴───────┴───────┴───────┴───────┘

整数集合的特点:

  • 自动升级编码:根据添加的整数大小自动升级内部编码
  • 节约内存:适用于整数集合时比哈希表节约内存
  • 有序存储:元素按值的大小有序排列

压缩列表(ZipList)

压缩列表是Redis为节约内存而开发的一种顺序型数据结构,用于实现列表和哈希表。

1
2
3
4
5
┌─────────┬───────┬───────────┬────────┬─────────┬────────┬─────────┐
│ zlbytes │ zltail │ zllen │ entry1 │ entry2 │ ... │ zlend │
├─────────┼───────┼───────────┼────────┼─────────┼────────┼─────────┤
│总字节数 │尾偏移量│ 元素数量 │ 元素1 │ 元素2 │ ... │ 结束符 │
└─────────┴───────┴───────────┴────────┴─────────┴────────┴─────────┘

压缩列表的特点:

  • 连续内存:所有元素连续存储在内存中
  • 节约空间:针对小数据量进行了空间优化
  • 性能取舍:插入和删除可能引起连锁更新,影响性能

持久化机制

Redis作为内存数据库,提供了两种持久化方案来保证数据不会因服务器宕机而丢失。

RDB(Redis Database)

RDB是Redis默认的持久化方式,它通过创建快照来保存数据库在某个时间点的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                  ┌─────────────┐
│ Redis服务器 │
└──────┬──────┘
│ 触发条件满足

┌─────────────────────────────────────────┐
│ 创建子进程 │
└───────────────────┬─────────────────────┘


┌─────────────────────────────────────────┐
│ 子进程生成RDB文件 │
└───────────────────┬─────────────────────┘


┌─────────────────────────────────────────┐
│ 替换旧的RDB文件 │
└─────────────────────────────────────────┘

RDB的优点:

  • 紧凑单一文件:方便备份和恢复
  • 性能影响小:通过子进程生成快照,主进程继续服务
  • 恢复速度快:适合大数据集的恢复

RDB的缺点:

  • 可能丢失数据:两次快照之间的数据可能丢失
  • fork操作昂贵:数据量大时可能阻塞服务

AOF(Append Only File)

AOF通过记录所有写操作命令来实现持久化,可以看作是Redis命令的日志。

1
2
3
4
5
6
7
8
9
┌───────────────┐     ┌───────────────┐     ┌───────────────┐
│ 命令写入缓冲区 │────▶│ 同步到AOF文件 │────▶│ 文件重写机制 │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 实时命令 │ │ 同步策略控制 │ │ 优化文件大小 │
│ always/每秒/不 │ │ fsync控制 │ │ 合并重复命令 │
└───────────────┘ └───────────────┘ └───────────────┘

AOF的优点:

  • 数据安全:可配置为每条命令或每秒同步,数据安全性高
  • 可读性:文件以纯文本方式记录,便于分析
  • 自动重写:文件过大时会自动重写以减小体积

AOF的缺点:

  • 文件体积:通常比RDB文件大
  • 性能略低:相比RDB,AOF的持久化对性能影响更大
  • 恢复较慢:需要重新执行所有命令

混合持久化

Redis 4.0引入了混合持久化,结合了RDB和AOF的优点。

1
2
3
4
5
6
┌────────────────────────────────────────────────────────────────┐
│ 混合AOF文件结构 │
├────────────────────────────┬───────────────────────────────────┤
│ RDB数据部分 │ AOF命令部分 │
│ (base snapshot in RDB) │ (commands after snapshot) │
└────────────────────────────┴───────────────────────────────────┘

混合持久化的优点:

  • 快速恢复:前半部分RDB格式快速恢复大部分数据
  • 数据安全:后半部分AOF保证最新操作不丢失
  • 文件体积适中:比单纯的AOF文件小

主从复制

Redis提供了主从复制功能,可以实现数据备份和读写分离。

1
2
3
4
5
6
7
8
9
10
11
         ┌───────────────┐  
│ 主节点 │
│ (Master) │
└───────┬───────┘

┌────────┼────────┐
│ │ │
┌───────▼──┐ ┌───▼────┐ ┌─▼───────┐
│ 从节点1 │ │ 从节点2 │ │ 从节点3 │
│ (Slave1) │ │(Slave2) │ │(Slave3) │
└──────────┘ └─────────┘ └─────────┘

主从复制的特点:

  • 数据备份:从节点自动同步主节点的数据变化
  • 读写分离:主节点处理写请求,从节点处理读请求
  • 容灾恢复:主节点故障时可以从从节点恢复
  • 扩展性:可以添加更多从节点以扩展读性能

复制原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌────────┐                            ┌────────┐
│ 主节点 │ │ 从节点 │
└────┬───┘ └────┬───┘
│ │
│ 1. PSYNC/SYNC请求 │
│◀────────────────────────────────────┤
│ │
│ 2. 执行BGSAVE生成RDB文件 │
├────────────────────────────────────▶│
│ │
│ 3. 传送RDB文件 │
├────────────────────────────────────▶│
│ │
│ 4. 传送复制缓冲区数据 │
├────────────────────────────────────▶│
│ │
│ 5. 命令传播 │
├────────────────────────────────────▶│
│ │

复制的三个阶段:

  1. 同步阶段:从节点发送SYNC命令,主节点返回RDB文件和缓冲区数据
  2. 命令传播阶段:主节点将写命令发送给从节点
  3. 部分重同步:Redis 2.8引入PSYNC命令,支持断线后的部分重同步

Redis集群

Redis提供了集群模式,可以横向扩展Redis的存储能力和性能。

集群架构

1
2
3
4
5
6
7
8
9
10
11
12
13
┌───────────┐     ┌───────────┐     ┌───────────┐
│ 主节点1 │ │ 主节点2 │ │ 主节点3 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ 从节点1 │ │ 从节点2 │ │ 从节点3 │
└───────────┘ └───────────┘ └───────────┘
│ │ │
└─────────────────┼─────────────────┘

┌─────▼─────┐
│ 客户端 │
└───────────┘

Redis集群的特点:

  • 分片存储:数据自动分片到不同节点
  • 去中心化:所有节点互相连接,无中心节点
  • 高可用性:主节点故障时自动选举从节点接管
  • 线性扩展:可以方便地添加新节点扩展集群

数据分片

Redis集群使用哈希槽(hash slot)来分配数据,共有16384个槽。

1
2
3
4
5
6
┌───────────────────────────────────────────────────────────────┐
│ 16384个哈希槽 │
├───────────────────┬───────────────────┬───────────────────────┤
│ Node A │ Node B │ Node C │
│ 0-5460 │ 5461-10922 │ 10923-16383 │
└───────────────────┴───────────────────┴───────────────────────┘

分片原理:

  1. 使用CRC16算法计算键的哈希值
  2. 对16384取模得到槽位
  3. 根据槽位映射到对应的节点

故障检测和恢复

Redis集群使用Gossip协议进行节点间通信和故障检测。

1
2
3
4
5
6
7
8
9
10
┌─────────┐     心跳     ┌─────────┐
│ 节点A │◀───────────▶│ 节点B │
└─────────┘ └─────────┘
▲ ▲
│ │
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ 节点D │◀───────────▶│ 节点C │
└─────────┘ 心跳 └─────────┘

故障处理流程:

  1. 主观下线:某个节点认为另一个节点不可达
  2. 客观下线:半数以上主节点认为该节点不可达
  3. 故障转移:从节点被选举为新的主节点
  4. 拓扑更新:集群拓扑结构更新,对外提供服务

内存管理

作为内存数据库,Redis的内存管理至关重要。

内存分配器

Redis可以使用不同的内存分配器:

  • jemalloc:默认分配器,分类管理内存,减少碎片
  • tcmalloc:Google开发的高性能分配器
  • libc malloc:系统自带的分配器

内存回收策略

当内存达到上限时,Redis提供多种策略:

1
2
3
4
5
6
7
8
9
10
11
12
┌───────────────────────────────────┐
│ 内存回收策略 │
├───────────────┬───────────────────┤
│ 过期删除 │ 内存淘汰 │
├───────────────┼───────────────────┤
│ - 惰性删除 │ - volatile-lru │
│ - 定期删除 │ - allkeys-lru │
│ │ - volatile-ttl │
│ │ - volatile-random│
│ │ - allkeys-random │
│ │ - noeviction │
└───────────────┴───────────────────┘

常用淘汰策略:

  • noeviction:不淘汰任何数据,写入操作报错
  • allkeys-lru:使用LRU算法淘汰任意键
  • volatile-lru:只淘汰设置了过期时间的键
  • allkeys-random:随机淘汰任意键
  • volatile-ttl:淘汰即将过期的键

Redis事务

Redis提供了简单的事务功能,可以一次执行多个命令。

事务执行过程

1
2
3
4
5
6
7
8
┌─────────┐      ┌─────────┐      ┌─────────┐      ┌─────────┐
│ MULTI │─────▶│ COMMAND │─────▶│ EXEC │─────▶│ 结果返回 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│开始事务 │ │入队命令 │ │执行事务 │ │返回结果 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

事务的特点:

  • 原子性:EXEC命令之前,客户端可以发送多个命令到事务队列中
  • 隔离性:事务队列中的命令不会被其它客户端发送的命令插入
  • 非ACID:Redis事务不支持回滚操作,执行失败不会撤销已执行的命令
  • 乐观锁:通过WATCH命令实现乐观锁机制

Redis性能优化

内存优化

  • 合理使用数据结构:选择合适的数据结构减少内存占用
  • 启用压缩:对较大的值启用压缩
  • 设置合理的过期时间:防止无用数据占用内存
  • 共享对象池:对于整数值可以使用共享对象减少内存

性能优化

  • 避免大键值对:分拆大键值对,避免阻塞
  • 使用管道(Pipeline):批量执行命令,减少网络往返时间
  • 合理使用Lua脚本:减少网络通信,原子执行多个操作
  • 优化持久化配置:根据需求平衡持久化和性能

总结

Redis凭借其高性能、灵活的数据结构和丰富的功能,成为现代互联网架构中不可或缺的组件。理解Redis的内部原理和机制,有助于我们更好地使用Redis,并在实际应用中针对性地进行优化。

在实际应用中,应根据业务需求合理选择数据结构、持久化策略和集群方案,并通过监控和优化不断提升Redis的性能和可靠性。

通过本文的介绍,希望读者能对Redis的核心原理有更深入的理解,为后续的深度学习和应用打下坚实的基础。

参考资料