在计算机系统中,存储层次是一个金字塔结构:寄存器 > 缓存 > 内存 > SSD > 磁盘。越靠近 CPU 的存储越快,但容量越小、成本越高。缓存设计的核心思想就是利用局部性原理——程序倾向于访问最近访问过的数据(时间局部性)或邻近的数据(空间局部性)。Redis 作为内存数据库,将数据存储在内存中,并提供了持久化、集群、高可用等企业级特性,成为现代应用架构中的缓存标准组件。
常见缓存淘汰策略:
缓存问题:
# Redis 内存淘汰策略
# maxmemory-policy 可选值
# volatile-lru: 从设置了过期时间的数据中淘汰 LRU
# allkeys-lru: 从所有数据中淘汰 LRU
# volatile-lfu: 从设置了过期时间的数据中淘汰 LFU
# allkeys-lfu: 从所有数据中淘汰 LFU
# volatile-ttl: 淘汰过期时间最近的数据
# noeviction: 不淘汰,写满后返回错误
maxmemory 4gb
maxmemory-policy allkeys-lru
# 设置 TTL (过期时间)
SET user:123 "data" EX 3600 # 1 小时后过期
# 缓存穿透解决方案: 布隆过滤器
BF.ADD user_filter 12345
BF.EXISTS user_filter 12345
Redis 的高性能不仅来自于内存存储,还来自于精心设计的数据结构。Redis 没有直接使用 C 语言的标准字符串和链表,而是自己实现了SDS (Simple Dynamic String)、跳表 (Skip List)、字典 (Hash Table) 等结构。这些数据结构针对内存效率和操作性能进行了优化,使得 Redis 在读写操作上具有极高的效率。
SDS (Simple Dynamic String): 改进的字符串实现,记录了字符串长度和剩余空间,支持 O(1) 获取长度和自动扩容。
跳表 (Skip List): 一种多层链表结构,支持 O(log N) 的搜索、插入、删除,底层是双向链表,上层是多级索引。
字典 (Hash Table): Redis 的哈希表实现,使用链地址法解决冲突,支持渐进式 rehash 避免阻塞。
# Redis 对象结构 (redisObject)
typedef struct redisObject {
unsigned type:4; // 类型 (string, list, hash, set, zset)
unsigned encoding:4; // 编码方式
unsigned lru:24; // LRU 时间
int refcount; // 引用计数
void *ptr; // 指向实际数据结构的指针
} robj;
# 编码方式 (encoding) 决定底层结构
# OBJ_ENCODING_INT: 整数 (int)
# OBJ_ENCODING_EMBSTR: 短字符串 (SDS)
# OBJ_ENCODING_RAW: 长字符串 (SDS)
# OBJ_ENCODING_HT: 字典 (hash table)
# OBJ_ENCODING_SKIPLIST: 跳表
# OBJ_ENCODING_ZIPLIST: 压缩列表
# OBJ_ENCODING_QUICKLIST: 快速列表
# 查看对象编码
OBJECT ENCODING mykey
在大多数服务器软件中,多线程是提升并发能力的常用手段。但 Redis 选择了单线程 + 事件驱动的模型。这看似反直觉,但实际上 Redis 的设计目标是内存操作,主要瓶颈是网络 I/O,而不是 CPU。
Redis 事件循环由 文件事件处理器 和 时间事件处理器 组成:
// 主循环 (aeMain)
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
// 处理文件事件(网络 I/O)
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
// 文件事件处理
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
// 调用 epoll_wait 等待事件
int nfds = epoll_wait(eventLoop->epfd, events, maxEvents, timeout);
for (int i = 0; i < nfds; ++i) {
// 根据事件类型调用对应的处理函数
aeFileEvent *fe = &eventLoop->events[events[i].data.fd];
if (events[i].events & EPOLLIN) {
fe->rfileProc(eventLoop, fe->clientData); // 读事件
}
if (events[i].events & EPOLLOUT) {
fe->wfileProc(eventLoop, fe->clientData); // 写事件
}
}
return processed;
}
Redis 是内存数据库,数据存储在内存中,一旦进程退出或断电,数据就会丢失。为了数据持久化,Redis 提供了 RDB 和 AOF 两种机制:RDB 是将内存数据定期生成快照文件;AOF 是将每条写命令记录到日志文件中。
RDB (Redis Database): 通过 fork() 创建子进程,子进程将内存数据写入临时文件,完成后替换旧文件。RDB 文件是一个压缩的二进制文件,加载速度快。
AOF (Append Only File): 每条写命令追加到 AOF 文件中。AOF 通过重写 (rewrite) 机制压缩日志,去除冗余命令。
# RDB 配置
save 900 1 # 900 秒内至少 1 次修改 → 触发 RDB
save 300 10 # 300 秒内至少 10 次修改 → 触发 RDB
save 60 10000 # 60 秒内至少 10000 次修改 → 触发 RDB
dbfilename dump.rdb
dir /var/lib/redis
# AOF 配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # 每秒同步 (折中方案)
# appendfsync always # 每次写入都同步 (最安全)
# appendfsync no # 由操作系统决定 (最快)
# 混合持久化 (Redis 4.0+)
aof-use-rdb-preamble yes # AOF 文件头部使用 RDB 格式
Redis 单机存在单点故障风险。一旦主节点宕机,服务将不可用。主从复制通过将数据复制到多个从节点,实现了数据冗余。从节点可以实时同步主节点的数据,在主节点故障时可以提升为新的主节点,保证服务连续性。
全量同步: 从节点首次连接或断线重连时,主节点生成 RDB 快照发送给从节点,从节点加载后继续接收增量命令。
增量同步: 主节点将写命令发送到复制积压缓冲区(repl_backlog),从节点定期拉取。如果从节点断开时间较短,可以通过偏移量恢复增量同步。
# 主节点配置 (默认)
# 从节点配置
replicaof 192.168.1.10 6379 # 指定主节点
slave-read-only yes # 从节点只读
# 查看复制信息
INFO replication
# 输出: role:master 或 role:slave
# master_repl_offset: 12345
# slave_repl_offset: 12345
# 设置复制积压缓冲区大小
repl-backlog-size 10mb
# 手动触发重新同步
SLAVEOF NO ONE # 解除复制,从节点成为主节点
SLAVEOF 192.168.1.20 6379 # 指定新的主节点
主从复制虽然提供了数据冗余,但故障切换需要人工介入。如果主节点宕机,需要手动将从节点提升为主节点,并重新配置其他从节点。哨兵 (Sentinel) 是一个高可用解决方案,它自动监控 Redis 节点状态,在故障时自动执行故障转移,保证系统的高可用。
哨兵核心功能:
# sentinel.conf
port 26379
daemonize yes
# 监控主节点 (mymaster: 主节点名称)
# 2 表示需要至少 2 个哨兵同意才能判定故障
sentinel monitor mymaster 127.0.0.1 6379 2
# 故障转移超时设置
sentinel down-after-milliseconds mymaster 30000 # 30 秒无响应判定为主观下线
sentinel failover-timeout mymaster 180000 # 故障转移超时 180 秒
# 从节点同步配置
sentinel parallel-syncs mymaster 1 # 同时同步的从节点数
# 启动哨兵
redis-sentinel /etc/redis/sentinel.conf
# 查看哨兵状态
redis-cli -p 26379 sentinel masters
redis-cli -p 26379 sentinel slaves mymaster
即使使用最高配置的服务器,Redis 单机的内存也有上限(通常几十 GB)。当数据量超过单机内存时,需要数据分片(Sharding)——将数据分布到多个节点上。Redis Cluster 是官方提供的分布式解决方案,它通过哈希槽 (Hash Slot) 将数据均匀分布到多个节点,并提供了高可用和自动故障转移能力。
哈希槽 (Hash Slot): 16384 个槽位,每个键通过 CRC16(key) & 16383 映射到指定槽位。每个节点负责一个连续的槽位范围。
节点通信: 节点间使用 Gossip 协议 交换状态信息,每个节点定期发送 PING 消息给其他节点。
重定向: 客户端请求的键不在当前节点时,节点返回 MOVED 或 ASK 响应,客户端重新定位到正确的节点。
# 创建集群 (使用 redis-cli)
redis-cli --cluster create \
192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \
192.168.1.13:6379 192.168.1.14:6379 192.168.1.15:6379 \
--cluster-replicas 1
# 查看集群信息
redis-cli -h 192.168.1.10 -p 6379 cluster info
redis-cli -h 192.168.1.10 -p 6379 cluster nodes
# 手动迁移槽位
redis-cli --cluster rebalance 192.168.1.10:6379
# 添加节点
redis-cli --cluster add-node 192.168.1.16:6379 192.168.1.10:6379
# 迁移槽位到新节点
redis-cli --cluster reshard 192.168.1.10:6379
# 删除节点
redis-cli --cluster del-node 192.168.1.16:6379 node_id
在使用缓存(如 Redis)作为数据库查询层的加速器时,缓存和数据库之间的一致性是一个核心问题。当数据在数据库中更新时,缓存中的数据也需要更新或失效。缓存一致性策略的选择直接影响到系统的数据正确性和性能。
读策略 (Read Strategy):
写策略 (Write Strategy):
# Cache Aside 策略 (伪代码)
function get(key) {
# 1. 从缓存读取
value = redis.get(key);
if (value != null) {
return value;
}
# 2. 缓存未命中,从数据库读取
value = db.get(key);
if (value == null) {
return null;
}
# 3. 写入缓存,设置过期时间
redis.set(key, value, 'EX', 3600);
return value;
}
function update(key, new_value) {
# 1. 更新数据库
db.update(key, new_value);
# 2. 删除缓存 (写后删除策略)
redis.del(key);
# 3. 延迟双删 (可选)
sleep(500); # 等待 500ms
redis.del(key);
}