Redis 高级实战

Redis Cluster 集群架构、Gossip 协议、Slot 迁移机制深度解析,以及缓存穿透/击穿/雪崩的防御方案、大 Key 治理、内存优化与 Redis 7 新特性实战。

Redis 不仅是缓存,更是高性能数据结构服务。从单机到集群、从基础数据结构到企业级治理,本文深入 Redis 高级特性与生产实践。


1. Redis 数据结构原理

1.1 底层实现速查

用户类型编码方式场景
StringSDS / int / embstr缓存、计数器
Listquicklist(3.2+)队列、时间线
Hashziplist / hashtable对象存储
Setintset / hashtable标签、去重
ZSetziplist / skiplist+hashtable排行榜、延迟队列
Streamradix tree消息队列
Bitmap / Bitfieldraw string签到、统计
HyperLogLog稀疏/稠密编码UV 统计
GEOzset + geohash位置服务
BloomFilterRedisBloom 模块去重判断

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
大 ZSetZRANGE 慢、持久化慢按时间/分数分片

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 新特性

特性说明
FunctionRedis 端 Lua 函数持久化,可复用
ACL LOG权限控制日志审计
Sharded Pub/SubCluster 模式下广播优化(按 Slot 路由)
Multi-part AOFAOF 文件拆分,更高效的重写
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)
  • ✅ 连接池合理配置(避免连接数耗尽)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「数据库」更多文章

  1. 备份恢复与高可用方案
  2. 数据库性能监控与诊断
  3. NewSQL 选型对比