Skip to content

Redis — 集群架构与最佳实践

数据结构与应用场景

Redis 提供丰富的数据结构,每种结构对应特定的业务场景:

数据结构命令典型场景
StringSET/GET/INCR缓存、计数器、分布式锁
HashHSET/HGET/HMGET对象存储(用户信息)
ListLPUSH/RPOP/LRANGE消息队列、最新列表
SetSADD/SMEMBERS/SINTER标签、共同好友
ZSetZADD/ZRANGE/ZRANGEBYSCORE排行榜、延迟队列
BitmapSETBIT/GETBIT/BITCOUNT签到、布隆过滤器
HyperLogLogPFADD/PFCOUNTUV 统计(近似)
StreamXADD/XREAD/XGROUP消息流、事件溯源
GeoGEOADD/GEODIST/GEORADIUS附近的人、地理围栏

持久化机制

RDB(快照)

bash
# redis.conf
# 自动触发:900s内有1次写操作,或300s内有10次,或60s内有10000次
save 900 1
save 300 10
save 60 10000

# 手动触发
BGSAVE  # 后台异步保存(推荐)
SAVE    # 同步保存(阻塞,不推荐生产使用)

优点:文件紧凑,恢复速度快
缺点:两次快照之间的数据可能丢失

AOF(追加日志)

bash
# redis.conf
appendonly yes
appendfilename "appendonly.aof"

# 同步策略
appendfsync always    # 每次写操作都 fsync(最安全,最慢)
appendfsync everysec  # 每秒 fsync(推荐,最多丢失1s数据)
appendfsync no        # 由 OS 决定(最快,可能丢失较多数据)

# AOF 重写(压缩 AOF 文件)
auto-aof-rewrite-percentage 100  # AOF 文件增长100%时触发
auto-aof-rewrite-min-size 64mb   # 最小触发大小

混合持久化(Redis 4.0+)

bash
# 推荐生产配置
aof-use-rdb-preamble yes  # AOF 文件头部使用 RDB 格式,加速恢复

高可用架构

Sentinel(哨兵模式)

Master ──[复制]──► Slave1
       ──[复制]──► Slave2

Sentinel1 ─┐
Sentinel2 ─┼──► 监控 Master/Slave,自动故障转移
Sentinel3 ─┘
bash
# sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2  # 2个哨兵同意才故障转移
sentinel down-after-milliseconds mymaster 5000  # 5s无响应认为宕机
sentinel failover-timeout mymaster 60000        # 故障转移超时60s
sentinel parallel-syncs mymaster 1              # 同时同步的Slave数
java
// Spring Boot 配置
spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - sentinel1:26379
        - sentinel2:26379
        - sentinel3:26379
    password: redis-password
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5

Cluster(集群模式)

Redis Cluster 使用哈希槽(Hash Slot)分片,共 16384 个槽:

Cluster(3主3从):
  Master1(槽 0-5460)    ──► Slave1
  Master2(槽 5461-10922)──► Slave2
  Master3(槽 10923-16383)──► Slave3

Key 路由:CRC16(key) % 16384 → 对应 Master
bash
# 创建集群
redis-cli --cluster create \
  192.168.1.10:7001 192.168.1.11:7002 192.168.1.12:7003 \
  192.168.1.10:7004 192.168.1.11:7005 192.168.1.12:7006 \
  --cluster-replicas 1

# 查看集群状态
redis-cli -c -h 192.168.1.10 -p 7001 cluster info
redis-cli -c -h 192.168.1.10 -p 7001 cluster nodes
yaml
# Spring Boot Cluster 配置
spring:
  redis:
    cluster:
      nodes:
        - redis1:7001
        - redis2:7002
        - redis3:7003
      max-redirects: 3
    lettuce:
      cluster:
        refresh:
          adaptive: true          # 自适应拓扑刷新
          period: 30s

分布式锁

Redisson 实现

java
@Autowired
private RedissonClient redisson;

public void processOrder(String orderId) {
    RLock lock = redisson.getLock("order:lock:" + orderId);
    
    try {
        // 尝试加锁,等待10s,锁超时30s
        boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
        if (!acquired) {
            throw new RuntimeException("获取锁失败");
        }
        
        // 执行业务逻辑
        doProcessOrder(orderId);
        
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

// 红锁(RedLock):多个独立 Redis 实例,防止单点故障
RLock lock1 = redisson1.getLock("order:lock:" + orderId);
RLock lock2 = redisson2.getLock("order:lock:" + orderId);
RLock lock3 = redisson3.getLock("order:lock:" + orderId);
RLock redLock = redisson.getRedLock(lock1, lock2, lock3);

SET NX 实现(简单场景)

java
// 加锁
String lockKey = "order:lock:" + orderId;
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);

// 释放锁(Lua 脚本保证原子性)
String script = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """;
redisTemplate.execute(
    new DefaultRedisScript<>(script, Long.class),
    List.of(lockKey),
    lockValue
);

缓存问题与解决方案

缓存穿透

问题:查询不存在的 Key,每次都打到数据库。

java
// 方案1:缓存空值
public User getUser(Long userId) {
    String key = "user:" + userId;
    String cached = redis.get(key);
    
    if (cached != null) {
        return "NULL".equals(cached) ? null : JSON.parse(cached, User.class);
    }
    
    User user = db.findById(userId);
    if (user == null) {
        redis.setex(key, 300, "NULL");  // 缓存空值5分钟
    } else {
        redis.setex(key, 3600, JSON.toJSON(user));
    }
    return user;
}

// 方案2:布隆过滤器(推荐,内存效率高)
@Autowired
private RBloomFilter<Long> bloomFilter;

public User getUser(Long userId) {
    if (!bloomFilter.contains(userId)) {
        return null;  // 一定不存在
    }
    // 可能存在,查缓存和数据库
    ...
}

缓存击穿

问题:热点 Key 过期瞬间,大量请求同时打到数据库。

java
// 方案:互斥锁(只有一个请求重建缓存)
public User getUser(Long userId) {
    String key = "user:" + userId;
    User user = redis.get(key);
    
    if (user != null) return user;
    
    // 加锁,防止并发重建
    String lockKey = "lock:user:" + userId;
    if (redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
        try {
            user = db.findById(userId);
            redis.setex(key, 3600, user);
        } finally {
            redis.delete(lockKey);
        }
    } else {
        // 等待后重试
        Thread.sleep(50);
        return getUser(userId);
    }
    return user;
}

// 方案2:逻辑过期(不设置 TTL,在 Value 中存过期时间)

缓存雪崩

问题:大量 Key 同时过期,或 Redis 宕机,导致数据库压力骤增。

java
// 方案1:过期时间加随机值
int ttl = 3600 + new Random().nextInt(600);  // 3600~4200s
redis.setex(key, ttl, value);

// 方案2:Redis 高可用(Sentinel/Cluster)

// 方案3:本地缓存兜底(Caffeine)
@Cacheable(value = "users", key = "#userId", cacheManager = "caffeineCacheManager")
public User getUser(Long userId) { ... }

性能优化

Pipeline(批量操作)

java
// 普通方式:N次网络往返
for (String key : keys) {
    redis.get(key);
}

// Pipeline:1次网络往返
List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    keys.forEach(key -> connection.get(key.getBytes()));
    return null;
});

Lua 脚本(原子操作)

java
// 原子性地检查并更新库存
String script = """
    local stock = tonumber(redis.call('get', KEYS[1]))
    if stock == nil or stock < tonumber(ARGV[1]) then
        return -1
    end
    return redis.call('decrby', KEYS[1], ARGV[1])
    """;

Long remaining = redisTemplate.execute(
    new DefaultRedisScript<>(script, Long.class),
    List.of("stock:product:123"),
    "1"
);

故障处理案例

案例一:内存使用率 100%

现象:Redis 内存达到 maxmemory,写入失败(取决于淘汰策略)。

排查

bash
redis-cli info memory
redis-cli info keyspace

# 查找大 Key
redis-cli --bigkeys

# 查找热 Key
redis-cli --hotkeys  # 需要 maxmemory-policy 为 LFU

解决

bash
# 配置合理的淘汰策略
# redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lru  # 淘汰最近最少使用的 Key

案例二:主从复制中断

现象:Slave 日志出现 MASTER aborted replication with an error

排查

bash
redis-cli info replication
# 查看 master_link_status: down
# 查看 master_last_io_seconds_ago

常见原因

  • 网络抖动导致复制中断
  • Master 的 repl-backlog-size 太小,Slave 落后太多需要全量同步

解决

bash
# 增大复制缓冲区
repl-backlog-size 256mb
repl-backlog-ttl 3600

案例三:慢查询

bash
# 查看慢查询日志
redis-cli slowlog get 10
redis-cli slowlog len

# 配置慢查询阈值(微秒)
slowlog-log-slower-than 10000  # 10ms
slowlog-max-len 128

常见慢命令

  • KEYS *(全量扫描,禁止生产使用,改用 SCAN
  • SMEMBERS(大 Set)
  • LRANGE 0 -1(大 List)
  • HGETALL(大 Hash)

监控指标

指标说明告警阈值
used_memory内存使用量> maxmemory 的 80%
connected_clients连接数> maxclients 的 80%
instantaneous_ops_per_sec每秒操作数接近设计上限
keyspace_hits/misses缓存命中率命中率 < 90%
rejected_connections拒绝连接数> 0
rdb_last_bgsave_status最近 RDB 状态err
master_link_status主从连接状态down

PaaS 中间件生态系统深度学习文档