为什么单层Redis缓存不够用
单层Redis缓存在高并发场景下有三个瓶颈:一是Redis本身成为热点(大量请求打到同一分片);二是Redis故障时缓存全部失效,请求瞬间压垮数据库;三是网络延迟——即使Redis响应1ms,跨机房场景下单次请求的Redis往返可能达到5-10ms。
多级缓存通过在应用层(JVM本地缓存)和分布式缓存(Redis)之间增加一层,将热点数据的读取延迟降低到纳秒级,同时降低Redis集群压力。
多级缓存架构设计
典型的三级缓存架构:
请求 → 本地缓存(L1) → Redis缓存(L2) → 数据库(L3)
Caffeine Redis Cluster MySQL/PG
延迟: ~ns 延迟: ~1ms 延迟: ~5ms
容量: ~GB 容量: ~100GB 容量: ~TB
L1本地缓存选型:
Caffeine是Java生态性能最好的本地缓存,基于W-TinyLFU淘汰算法:
// Caffeine配置
Cache<String, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000) // 最大条目数
.expireAfterWrite(Duration.ofMinutes(10)) // 写入10分钟后过期
.refreshAfterWrite(Duration.ofMinutes(5)) // 写入5分钟后异步刷新
.recordStats() // 开启统计
.build();
// 多级缓存读取
public Product getProduct(Long productId) {
String key = "product:" + productId;
// L1: 本地缓存
Product product = localCache.getIfPresent(key);
if (product != null) {
return product;
}
// L2: Redis缓存
product = redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(key, product); // 回填L1
return product;
}
// L3: 数据库
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
localCache.put(key, product);
}
return product;
}
缓存雪崩防护
缓存雪崩指大量缓存Key在同一时刻过期,请求全部穿透到数据库。
方案一:过期时间加随机偏移
def set_cache_with_jitter(key, value, base_ttl_minutes=30):
"""设置缓存时加入随机偏移,避免同时过期"""
jitter = random.randint(0, base_ttl_minutes // 3)
ttl = base_ttl_minutes * 60 + jitter * 60
redis.setex(key, ttl, value)
方案二:互斥锁重建缓存
缓存失效时只允许一个线程重建,其他线程等待:
public Product getProductWithMutex(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:" + key;
// 尝试从缓存获取
Product product = getFromCache(key);
if (product != null) return product;
// 缓存未命中,尝试获取互斥锁
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {
try {
// 双重检查
product = getFromCache(key);
if (product != null) return product;
// 查数据库并重建缓存
product = productMapper.selectById(productId);
setCacheWithJitter(key, product);
return product;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,短暂等待后重试
try { Thread.sleep(50); } catch (InterruptedException e) {}
return getProductWithMutex(productId);
}
}
方案三:逻辑过期
不设置TTL,而是在Value中存储逻辑过期时间。过期后返回旧数据,异步更新:
// 逻辑过期数据结构
public class CacheData<T> {
private T data;
private LocalDateTime expireTime;
}
// 异步刷新
public Product getProductLogicalExpire(Long productId) {
String key = "product:" + productId;
CacheData<Product> cacheData = redisTemplate.opsForValue().get(key);
if (cacheData == null) {
return loadAndCache(productId, key);
}
if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
return cacheData.getData();
}
// 逻辑过期,异步刷新
CACHE_REFRESH_EXECUTOR.submit(() -> loadAndCache(productId, key));
// 返回旧数据
return cacheData.getData();
}
private static final ExecutorService CACHE_REFRESH_EXECUTOR =
Executors.newFixedThreadPool(10, r -> {
Thread t = new Thread(r, "cache-refresh");
t.setDaemon(true);
return t;
});
缓存穿透防护
缓存穿透指查询不存在的数据,缓存无法命中,请求直达数据库。
方案一:布隆过滤器
Redis 7.x原生支持RedisBloom模块:
# 加载布隆过滤器(启动时从数据库预热)
bf.reserve user_ids 0.001 1000000 # 误判率0.1%,初始容量100万
# 插入所有存在的用户ID
for user_id in all_user_ids:
bf.add user_ids $user_id
# 查询前先检查布隆过滤器
def get_user(user_id):
exists = redis.execute_command('BF.EXISTS', 'user_ids', user_id)
if not exists:
return None # 布隆过滤器说不存在,一定不存在
return get_user_from_cache_or_db(user_id)
方案二:空值缓存
对查询结果为空的Key也缓存,设置较短的TTL:
def get_user(user_id):
key = f"user:{user_id}"
cached = redis.get(key)
if cached is not None:
if cached == "NULL_MARKER":
return None
return json.loads(cached)
user = db.query_user(user_id)
if user:
redis.setex(key, 1800, json.dumps(user))
else:
redis.setex(key, 60, "NULL_MARKER")
return user
缓存击穿防护
缓存击穿指热点Key过期的瞬间,大量并发请求同时穿透到数据库。与雪崩的区别是:击穿是单个热点Key,雪崩是大量Key同时过期。
方案:热点Key永不过期 + 异步刷新
# 热点Key检测
class HotKeyDetector:
def __init__(self, threshold=1000, window=60):
self.counter = {}
self.threshold = threshold
self.window = window
def check(self, key):
count = self.counter.get(key, 0) + 1
self.counter[key] = count
return count >= self.threshold
def reset(self):
self.counter.clear()
# 定时任务:检测热点Key并设置永不过期
async def hotkey_protection():
while True:
await asyncio.sleep(60)
for key in detector.get_hot_keys():
ttl = redis.ttl(key)
if 0 < ttl < 300:
redis.persist(key)
await schedule_refresh(key)
Redis 7.x新特性在缓存场景的应用
Function替代Lua脚本:
Redis 7.x引入Function,比Lua脚本更易管理和版本化:
# 注册Function
redis.function load REPLACE /path/to/cache_functions.lua
# cache_functions.lua
redis.register_function{
name = 'get_or_set_cache',
flags = { 'no-writes' },
fn = function(keys, args)
local key = keys[1]
local value = redis.call('GET', key)
if value then
return value
end
local lock_key = 'lock:' .. key
local locked = redis.call('SETNX', lock_key, '1')
if locked == 1 then
redis.call('EXPIRE', lock_key, 10)
return nil
else
return 'RETRY'
end
end
}
Multi-part AOF——Redis 7.x的AOF文件分为base和incremental两部分,重启加载更快,缓存恢复时间缩短。
Client-side caching——Redis 7.x完善了客户端缓存协议,应用可直接在本地维护缓存子集,Redis推送失效通知:
# 开启客户端缓存
CLIENT TRACKING ON REDIRECT <client_id> BCAST PREFIX product:
# Redis自动推送product:前缀的失效通知
# 应用收到通知后清除本地缓存
缓存架构的设计没有银弹——多级缓存增加了复杂度,本地缓存和分布式缓存的一致性维护需要额外机制。选择几级缓存取决于流量规模和延迟要求:日PV百万级别单层Redis即可,千万级以上才需要多级缓存。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis7x-duo-ji-huan-cun-jia-gou-she-ji-yu-xue-beng-chuan/