Redis缓存设计与性能优化指南
一、缓存穿透
问题描述
查询不存在的数据,缓存和DB均未命中,导致每次请求直达数据库。
原因
- 自身业务代码或数据问题
- 恶意攻击/爬虫大量请求不存在的数据
解决方案
✅ 缓存空对象
String get(String key) {
String cacheValue = cache.get(key);
if (StringUtils.isBlank(cacheValue)) {
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 存储值为空时设置短过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 300);
}
return storageValue;
}
return cacheValue;
}
✅ 布隆过滤器(Bloom Filter)
- 原理:使用位数组+多个无偏哈希函数,判断元素一定不存在或可能存在
- 特点:
- 适用于数据固定、实时性低的场景
- 不支持删除操作(需重新初始化)
- Redisson实现:
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("sample"); bloomFilter.tryInit(100000L, 0.03); // 初始化元素量+误判率 bloomFilter.add("key1"); // 添加数据 bloomFilter.contains("key1"); // 校验存在性
二、缓存击穿(失效)
问题描述
大批量缓存在同一时间失效,导致请求穿透至数据库。
解决方案
// 批量设置缓存时分散过期时间
for (int i = 0; i < items.size(); i++) {
int expireTime = 1800 + new Random().nextInt(300); // 1800±300秒
cache.set(key, value, expireTime);
}
三、缓存雪崩
问题描述
缓存层崩溃后,流量直接冲击存储层导致级联故障。
解决方案
- 高可用架构:使用Redis Cluster/Sentinel
- 限流降级:
- 非核心数据:返回预定义空值/错误信息
- 核心数据(如库存):允许查缓存或DB
- 容灾演练:提前模拟缓存宕机场景
四、热点Key重建优化
问题:热点Key失效瞬间,大量线程并发重建缓存。
方案:互斥锁控制重建
String rebuildCache(String key) {
String value = cache.get(key);
if (value == null) {
String lockKey = "lock:" + key;
if (lock.tryLock()) { // 获取分布式锁
value = db.get(key); // 查询数据库
cache.set(key, value); // 重建缓存
lock.unlock();
} else {
Thread.sleep(100); // 等待其他线程完成
return cache.get(key); // 重试获取
}
}
return value;
}
五、缓存与DB双写不一致
场景分析
- 双写不一致:并发写导致数据覆盖
- 读写不一致:读操作与写操作并发
解决方案
场景 | 策略 |
---|---|
低并发数据 | 设置缓存过期时间 + 定期主动更新 |
允许短暂不一致 | 缓存过期时间兜底 |
强一致性要求 | 分布式读写锁(如Redisson) |
异步监听 | 通过Canal订阅Binlog更新缓存 |
📌 写多读多且强一致性场景,建议直接读写DB或采用缓存为主存储+DB异步备份。
六、键值设计规范
🔑 Key设计
- 规范:
业务名:表名:id
(例:trade:order:1
) - 简洁性:控制长度(例:
u:{uid}:fr:m:{mid}
) - 禁用字符:空格、换行符、引号等
📦 Value设计
⚠️ 避免BigKey
- 定义:
- String类型 > 10KB
- 集合元素 > 5000个
- 危害:
- 阻塞Redis
- 网络拥塞
- 删除操作引发延迟
- 优化方案:
- 拆分:大Hash拆分为小Key(例:
user:1
→user:1:info
,user:1:orders
) - 选择合适结构:实体类型用Hash替代多个String
- 拆分:大Hash拆分为小Key(例:
⭐ 最佳实践
- 设置过期时间:
expire key seconds
- 过期时间打散:避免集中失效
七、命令使用规范
- 避免O(N)命令:
- 使用
HSCAN
/SSCAN
替代HGETALL
/SMEMBERS
- 使用
- 禁用危险命令:
rename-command KEYS "" rename-command FLUSHALL ""
- 高效批量操作:
- 原生批量命令:
MGET
/MSET
- Pipeline非原子操作(一次批量 ≤ 500元素)
- 原生批量命令:
八、客户端优化
🔗 连接池配置
参数 | 含义 | 建议值 |
---|---|---|
maxTotal |
最大连接数 | 按QPS计算(例:QPS=50000 → 50+连接) |
maxIdle |
最大空闲连接 | 与maxTotal 一致 |
minIdle |
最小空闲连接 | 预热连接至该值 |
testOnBorrow |
借出时校验 | 高并发设为false |
预热连接池示例:
List<Jedis> minIdleList = new ArrayList<>(jedisPoolConfig.getMinIdle());
for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
Jedis jedis = pool.getResource();
minIdleList.add(jedis);
jedis.ping(); // 预热连接
}
for (Jedis jedis : minIdleList) {
jedis.close(); // 归还连接池
}
九、内存策略优化
♻️ 内存淘汰策略(maxmemory-policy)
策略 | 适用场景 |
---|---|
volatile-lru |
过期Key中淘汰最近最少使用(推荐) |
allkeys-lru |
所有Key中淘汰最近最少使用 |
volatile-lfu |
过期Key中淘汰访问频率最低 |
noeviction |
不淘汰,拒绝写入(默认) |
4.0+版本支持LFU策略,适合热点数据场景。
⚙️ 内核参数调优
# 调整SWAP倾向(避免OOM Kill)
echo vm.swappiness=1 >> /etc/sysctl.conf
# 允许内存超分配(保障fork成功)
echo vm.overcommit_memory=1 >> /etc/sysctl.conf
# 增加文件句柄数
ulimit -n 65535
十、慢查询监控
# 设置慢查询阈值(单位:微秒)
config set slowlog-log-slower-than 1000
# 保存慢查询日志条数
config set slowlog-max-len 1024
# 查看慢查询日志
slowlog get 5
📌 生产环境建议阈值设为1ms(1000微秒)
总结:缓存设计的核心在于平衡性能与一致性,避免过度设计。优先解决穿透、雪崩、击穿问题,规范Key设计,警惕BigKey,合理配置连接池和淘汰策略,结合监控实现高性能缓存方案。