Redis — 集群架构与最佳实践
数据结构与应用场景
Redis 提供丰富的数据结构,每种结构对应特定的业务场景:
| 数据结构 | 命令 | 典型场景 |
|---|---|---|
| String | SET/GET/INCR | 缓存、计数器、分布式锁 |
| Hash | HSET/HGET/HMGET | 对象存储(用户信息) |
| List | LPUSH/RPOP/LRANGE | 消息队列、最新列表 |
| Set | SADD/SMEMBERS/SINTER | 标签、共同好友 |
| ZSet | ZADD/ZRANGE/ZRANGEBYSCORE | 排行榜、延迟队列 |
| Bitmap | SETBIT/GETBIT/BITCOUNT | 签到、布隆过滤器 |
| HyperLogLog | PFADD/PFCOUNT | UV 统计(近似) |
| Stream | XADD/XREAD/XGROUP | 消息流、事件溯源 |
| Geo | GEOADD/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: 5Cluster(集群模式)
Redis Cluster 使用哈希槽(Hash Slot)分片,共 16384 个槽:
Cluster(3主3从):
Master1(槽 0-5460) ──► Slave1
Master2(槽 5461-10922)──► Slave2
Master3(槽 10923-16383)──► Slave3
Key 路由:CRC16(key) % 16384 → 对应 Masterbash
# 创建集群
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 nodesyaml
# 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 |