Redis 不仅是缓存,更是高性能数据结构服务。从单机到集群、从基础数据结构到企业级治理,本文深入 Redis 高级特性与生产实践。
1. Redis 数据结构原理
1.1 底层实现速查
| 用户类型 | 编码方式 | 场景 |
|---|---|---|
| String | SDS / int / embstr | 缓存、计数器 |
| List | quicklist(3.2+) | 队列、时间线 |
| Hash | ziplist / hashtable | 对象存储 |
| Set | intset / hashtable | 标签、去重 |
| ZSet | ziplist / skiplist+hashtable | 排行榜、延迟队列 |
| Stream | radix tree | 消息队列 |
| Bitmap / Bitfield | raw string | 签到、统计 |
| HyperLogLog | 稀疏/稠密编码 | UV 统计 |
| GEO | zset + geohash | 位置服务 |
| BloomFilter | RedisBloom 模块 | 去重判断 |
1.2 跳表(Skip List):ZSet 的秘密
ZSet 同时维护跳表 + 哈希表:
Hash: { member → score } O(1) 查找 score
Skip List: 按 score 排序的分层链表 O(log n) 范围查询
跳表示例(ZSet 的 skiplist):
Level 3: head ───────────────────────► 100
Level 2: head ───────────► 30 ───────► 100
Level 1: head ──► 10 ──► 30 ──► 50 ──► 100
Level 0: head ─► 10 ─► 20 ─► 30 ─► 50 ─► 100
▼
有序遍历
跳表相比平衡树:实现简单、范围查询高效、并发修改更容易。
2. Redis Cluster 架构
2.1 数据分片:16384 个 Slot
Redis Cluster 将 Key 空间划分为 16384(2^14)个哈希槽:
Slot = CRC16(key) mod 16384
┌─────────────────────────────────────────────────────┐
│ Node A (Master) Node B (Master) Node C │
│ Slots: 0-5460 Slots: 5461-10922 Slots:10923-16383│
│ │ │ │ │
│ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐│
│ │A1 Slave │ │B1 Slave │ │C1 Slave ││
│ └─────────┘ └─────────┘ └─────────┘│
└─────────────────────────────────────────────────────┘
每个节点负责一部分 Slot,副本通过主从复制同步。
2.2 Gossip 协议
Redis Cluster 节点间通过 Gossip 协议交换状态:
节点间消息类型:
- PING/PONG:心跳,携带节点信息(ID、地址、Slot、故障标识)
- MEET:新节点加入
- FAIL:标记节点失效
- PUBLISH:消息广播
- UPDATE:槽位变更通知
Gossip 传播:
Node A 知道 B 的信息。
A 向 C PING 时附带 B 的信息 → C 也知道了 B。
信息像病毒一样在集群中传播。
故障检测:
半数以上 Master 认为某节点 FAIL → 触发故障转移。
2.3 重定向(MOVED / ASK)
Client 缓存 Slot → Node 映射:
Client: GET mykey
│ Slot(CRC16("mykey") mod 16384) = 12003
│ 缓存中 12003 → Node C
▼
Node C: 12003 已迁移到 Node B!
│
└── MOVED 12003 NodeB:6379 → Client 更新缓存,重试
重新分片期间的临时重定向:
Node A: 12003 正在迁移到 Node C(未完成)
└── ASK 12003 NodeC:6379 → Client 先去 C ASKING,再执行命令
2.4 集群扩容与 Slot 迁移
# 1. 启动新节点
redis-cli --cluster add-node new_node:6379 existing_node:6379
# 2. 分配 Slot(从旧节点迁出)
redis-cli --cluster reshard old_node:6379 \
--cluster-from node-id \
--cluster-to new-node-id \
--cluster-slots 4096
# 迁移过程:
# - 对每个迁移的 key:原子性的 MIGRATE 命令
# - 旧节点先锁定 key → DUMP → 新节点 RESTORE → 删除旧 key
3. 缓存三大问题与防御
3.1 缓存穿透(Cache Penetration)
攻击者:GET user:id_99999(id 不存在)
│
├──► Redis 无缓存
├──► 查询数据库(也无记录)
└──► 没有写入缓存 → 下次继续穿透
影响:大量无效请求直达数据库,可能导致 DB 崩溃。
防御方案:
// 方案1:布隆过滤器预过滤(推荐)
@RedisBloom
public User getUser(Long id) {
if (!bloomFilter.mightContain("user:" + id)) {
return null; // 一定不存在
}
// 继续查缓存/DB
}
// 方案2:缓存空值(简单但占用内存)
public User getUser(Long id) {
String key = "user:" + id;
User user = redis.get(key);
if (user == null) {
user = db.query(id);
if (user == null) {
redis.setex(key, 60, "NULL"); // 空值缓存 60s
} else {
redis.setex(key, 3600, user);
}
}
return "NULL".equals(user) ? null : user;
}
3.2 缓存击穿(Cache Breakdown)
热点 Key 突然过期:
时间点: t0(Key过期) t1(并发重建)
│ │
请求1: GET hot_key ──► miss ──► 查 DB ──► 重建缓存
请求2: GET hot_key ──► miss ──► 查 DB ──► 重建缓存 ← 并发!
请求3: GET hot_key ──► miss ──► 查 DB ──► 重建缓存
... 大量请求同时打到数据库
防御方案:
// 方案1:互斥锁重建(排队等第一个)
public User getHotUser(Long id) {
String key = "user:" + id;
User user = redis.get(key);
if (user == null) {
String lockKey = "lock:user:" + id;
// 获取分布式锁(SET key value NX EX 10)
if (redis.set(lockKey, "1", "NX", "EX", 10)) {
try {
user = db.query(id); // 只有一个线程查 DB
redis.setex(key, 3600, user);
} finally {
redis.del(lockKey);
}
} else {
Thread.sleep(100); // 其他线程等待
return getHotUser(id); // 重试
}
}
return user;
}
// 方案2:逻辑过期(永不过期,后台异步刷新)
@Data
class CacheItem<T> {
T data;
long expireTime; // 逻辑过期时间
}
3.3 缓存雪崩(Cache Avalanche)
大量 Key 同时过期:
创建时间: 10:00 10:00 10:00
过期时间: 11:00 11:00 11:00
│ │ │
11:00: key1/key2/key3/... 同时过期!
│
└── 所有请求直达数据库 → DB 崩溃
防御方案:
// 方案1:过期时间加随机偏移
int expireSeconds = 3600 + ThreadLocalRandom.current().nextInt(300);
redis.setex(key, expireSeconds, value);
// 方案2:多级缓存(本地缓存 + Redis)
@CaffeineCache(maxSize = 10000, expireAfterWrite = 5m)
@RedisCache(expire = 3600)
public User getUser(Long id) {
return db.query(id);
}
// 本地缓存扛住 Redis 失效瞬间的流量洪峰
// 方案3:熔断降级
@CircuitBreaker(name = "userCache", fallbackMethod = "getUserFallback")
public User getUser(Long id) { ... }
public User getUserFallback(Long id, Exception ex) {
return User.defaultUser(); // 返回默认值,保护数据库
}
4. 企业级 Redis 治理
4.1 大 Key 治理
# 发现大 Key
redis-cli --bigkeys
redis-cli --mem-keys "*" --mem-keys-samples 1000
# 使用 rdb-tools 分析
rdb -c memory dump.rdb -l largest 100 > large_keys.csv
# 大 Key 拆分示例
# 原:hash "user:10000" 有 100 万个 field → 内存爆炸
# 拆:按用户 ID 后两位分桶
# user:10000:00 ~ user:10000:99,每个 1 万个 field
| 大 Key 类型 | 影响 | 治理方案 |
|---|---|---|
| 大 Hash | 单次操作阻塞、序列化慢 | 拆分为多个小 Hash |
| 大 List | 阻塞其他请求、网络带宽 | 分片到多个 List |
| 大 String(> 10MB) | 内存碎片、传输慢 | 压缩、拆分、放 OSS |
| 大 ZSet | ZRANGE 慢、持久化慢 | 按时间/分数分片 |
4.2 内存优化
# 1. 使用高效编码
hash-max-ziplist-entries 512 # Hash 元素 < 512 用 ziplist
hash-max-ziplist-value 64 # 每个元素值 < 64B 用 ziplist
list-max-ziplist-size -2 # quicklist 节点压缩
zset-max-ziplist-entries 128 # ZSet 小数据用 ziplist
# 2. 内存淘汰策略
maxmemory-policy allkeys-lru # 全局 LRU 淘汰(推荐)
# 备选:volatile-lru / allkeys-lfu / volatile-ttl
# 3. 内存碎片整理(Redis 4.0+)
active-defrag-yes yes
active-defrag-threshold-lower 10
4.3 持久化策略
| 策略 | 原理 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| RDB | 快照到二进制文件 | 恢复极快、文件紧凑 | 可能丢数据 | 容忍分钟级丢数据 |
| AOF | 追加写命令日志 | 数据安全(每秒 fsync) | 文件大、恢复慢 | 数据敏感 |
| 混合(4.0+) | RDB + AOF | 兼顾两者 | 稍复杂 | 生产推荐 |
# 推荐配置(混合持久化)
aof-use-rdb-preamble yes # 先 RDB 全量 + 后续 AOF 增量
appendonly yes
appendfsync everysec # 每秒刷盘(性能与安全的平衡)
5. 分布式锁与 RedLock
5.1 单节点分布式锁
# SET key value NX EX seconds(原子操作)
SET lock:order:1001 my_random_value NX EX 30
public boolean lock(String key, String value, int expireSeconds) {
String result = redis.set(key, value, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public void unlock(String key, String value) {
// Lua 原子脚本:只释放自己持有的锁
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
redis.eval(script, 1, key, value);
}
5.2 RedLock(多主节点)
// Redisson 实现
Config config = new Config();
config.useRedLock()
.addNodeAddress("redis://redis1:6379")
.addNodeAddress("redis://redis2:6379")
.addNodeAddress("redis://redis3:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getRedLock("myLock");
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
RedLock 在 N 个独立 Redis 节点上同时获取锁,当成功获取 **majority(N/2 + 1)**个锁且总耗时小于锁有效期时,认为获取成功。
6. Redis 7 新特性
| 特性 | 说明 |
|---|---|
| Function | Redis 端 Lua 函数持久化,可复用 |
| ACL LOG | 权限控制日志审计 |
| Sharded Pub/Sub | Cluster 模式下广播优化(按 Slot 路由) |
| Multi-part AOF | AOF 文件拆分,更高效的重写 |
| Client-eviction | 连接数过多时驱逐低优先级客户端 |
# Redis Function(7.0+)
FUNCTION LOAD "#!lua name=mylib\n\nredis.register_function('myfunc', function(keys, args)\n return redis.call('GET', keys[1])\nend)"
FCALL myfunc 1 mykey
7. 总结
Redis 从缓存走向数据结构服务、从单节点走向分布式集群:
Redis 架构演进:
单机 主从复制 Sentinel Cluster
│ │ │ │
▼ ▼ ▼ ▼
单实例 读写分离 自动故障转移 水平扩展
无高可用 手动切换 高可用 数据分片
数据备份 监控 自动迁移
生产环境 checklist:
- ✅ 使用 Redis Cluster 或多个 Sentinel 集群
- ✅ 混合持久化(RDB + AOF)
- ✅ 大 Key 监控与拆分
- ✅ 缓存三大问题防御
- ✅ 内存淘汰策略配置
- ✅ 慢查询监控(slowlog)
- ✅ 连接池合理配置(避免连接数耗尽)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。