Redis 热点 Key 自动续期解决方案
· 阅读需 3 分钟
在高并发系统中,缓存是提升性能的关键组件,但不当的缓存过期策略可能导致:
缓存击穿:热点 Key 突然失效,大量请求直接穿透到数据库
缓存雪崩:大量 Key 同时过期导致系统负载激增
资源浪费:冷数据占用缓存空间时间过长
问题
在高并发的场景下,我们经常会使用缓存来提高系统的性能和响应速度。然而,缓存的使用也带来了一些问题,常见的问题就是缓存的过期时间。如果热 key 缓存的过期时间设置得太短了,可能会导致缓存击穿的问题,设置太长好像又没有一个时间的概念 🤣。 细想了一下,为啥不实现一个 Redis 的过期时间自动续期策略。设置一个 热点 key 缓存的过期时间即将到达时,我们可以自动延长缓存的过期时间,从而避免缓存击穿 🤔。
解决方案
- 正常路径:缓存命中 → 检查TTL → 返回数据
- 回源路径:缓存未命中 → 查询DB → 写缓存 → 返回数据
- 热点续期:满足条件时异步延长TTL
时序图
代码实现
为了方便演示,就拿 TS 写一个 demo 👇,如有不足请多多指点
export const RedisConf = {
ExpiredTime: {
DEFAULT: 60 * 60 * 8,
},
} as const;
/**
* 通过 Redis 缓存获取数据的工具函数
*
* @template T - 缓存数据的类型
* @param {Redis} redis - Redis 实例
* @param {string} key - 缓存的键名
* @param {() => Promise<T>} dataFetcher - 获取数据的异步函数
* @param {number} [expireTime=RedisConf.ExpiredTime.DEFAULT] - 缓存过期时间(秒)
* @param {number} [hotMaxKeepTime=0] - 热点数据最大保持时间(秒),为 0 时不启用热点延期
* @returns {Promise<T>} 返回缓存的数据或新获取的数据
*
* @description
* 1. 如果缓存不存在,调用 dataFetcher 获取数据并设置缓存
* 2. 如果缓存存在且设置了 hotMaxKeepTime,当 TTL 小于 hotMaxKeepTime 一半时,随机延长过期时间
*/
export async function redisCache<T>(
redis: Redis,
key: string,
dataFetcher: () => Promise<T>,
expireTime = RedisConf.ExpiredTime.DEFAULT,
hotMaxKeepTime = 0,
): Promise<T> {
const cachedData = await redis.get(key);
let parsedData = cachedData ? JSON.parse(cachedData) : null;
if (cachedData === null) {
parsedData = await dataFetcher();
await redis.set(key, JSON.stringify(parsedData), 'EX', expireTime);
} else if (hotMaxKeepTime > 0) {
const ttl = await redis.ttl(key);
const halfHotMaxKeepTime = Math.floor(hotMaxKeepTime / 2);
const numRange = 10;
if (ttl > 0 && ttl < halfHotMaxKeepTime) {
const extraExpireTime = expireTime * Math.floor(Math.random() * numRange);
await redis.expire(key, ttl + extraExpireTime);
}
}
return parsedData;
}
热点续期策略
主要我们还是得动态感知:自动检测热点Key渐进式续期:不是一次性续到最大值,而是随机的去逐步延长。
总结
我这个想法只是简单的基于 ttl 去判定的。用法简单,相当于二合一的分阀。根据实际业务规模和痛点选择合适的优化层级,避免过度设计。对于大多数应用,轻度优化+本地缓存已经能解决90%的热点问题。如果你们有更好用的方法或者意见。请留言或者联系我。🥳