场景
java
场景,一般在我们的服务可能有上百个,但是作为读的redis可能只有三四个实例,那么每一次服务查询不到redis都要去数据库里查,怎么解决这种问题?
一、架构层面优化
1. 多级缓存体系
graph TD
A[客户端] --> B[本地缓存]
B --> C[分布式Redis]
C --> D[数据库]
本地缓存:每个服务实例使用Guava/Caffeine/Ehcache
设置合理的过期时间(如30秒)
适合热点数据和变更不频繁的数据
分布式缓存:现有Redis集群
存储全量缓存数据
设置比本地缓存更长的TTL
2. Redis集群扩展方案
读写分离:
- 主实例写,多个只读副本分担查询压力
分片集群:
按业务域拆分(如用户数据/商品数据分开)
使用Redis Cluster或代理分片(Twemproxy/Codis)
二、缓存策略优化
1. 缓存预热机制
// 服务启动时加载热点数据
@PostConstruct
public void initHotData() {
List<String> hotKeys = getHotKeysFromDB();
hotKeys.forEach(key -> {
Object value = getFromDB(key);
redisTemplate.opsForValue().set(key, value);
});
}
2. 智能缓存穿透防护
布隆过滤器:
- 前置校验key是否存在
- 减少无效查询
空值缓存:
def get_data(key):
data = redis.get(key)
if data is None:
data = db.get(key)
if data is None:
redis.set(key, "NULL", ex=300) # 缓存空值5分钟
else:
redis.set(key, data, ex=3600)
elif data == "NULL":
return None
return data
3. 并发控制策略
- 互斥锁防止缓存击穿:
func GetData(key string) interface{} {
data := redis.Get(key)
if data == nil {
// 获取分布式锁
if acquireLock(key) {
defer releaseLock(key)
data = db.Query(key)
redis.Set(key, data, TTL)
} else {
// 等待短暂时间后重试
time.Sleep(100 * time.Millisecond)
return GetData(key)
}
}
return data
}
三、性能优化措施
1. 批量查询优化
- Pipeline批量操作:
# Redis管道示例
echo -e "GET key1\nGET key2\nGET key3" | redis-cli --pipe
多键值合并存储:
- 将关联数据合并为Hash结构存储
2. 缓存更新策略
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
定时刷新 | 实现简单 | 实时性差 | 变化不频繁的数据 |
写时更新 | 数据一致性好 | 写压力大 | 强一致性要求 |
过期剔除 | 系统负载均衡 | 可能短暂不一致 | 最终一致性场景 |
3. 监控与自动扩缩容
关键监控指标:
Redis命中率(低于90%需预警)
连接数使用率
CPU/内存负载
弹性扩缩容:
# 根据监控自动扩展Redis节点示例
if [ $hit_ratio -lt 85 ]; then
scale_redis_nodes +1
fi
四、实施路线建议
短期应急方案:
为所有服务添加本地缓存
实现空值缓存和基本互斥锁
中期优化方案:
引入多级缓存体系
实施缓存预热
增加Redis只读副本
长期架构方案:
按业务域拆分Redis集群
实现自动化监控扩缩容
构建统一的缓存服务层
五、特别注意事项
缓存一致性:
- 考虑使用CDC(变更数据捕获)工具如Debezium同步数据库变更
热点Key处理: java
复制
下载
// 热点Key检测与分散
String cacheKey = "product_" + productId;
if(isHotKey(cacheKey)) {
// 添加随机后缀分散请求
cacheKey = cacheKey + "_" + ThreadLocalRandom.current().nextInt(10);
}
服务降级方案:
当Redis不可用时自动降级到本地缓存
实现熔断机制防止雪崩