记一次缓存击穿的解决

参考:
Redis 缓存雪崩、缓存穿透、缓存击穿、缓存预热
《我们一起进大厂》系列-缓存雪崩、击穿、穿透
一行代码解决缓存击穿问题
缓存击穿问题的三种解决思路

先看下缓存击穿的概念:

缓存击穿指的是某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力,这种情况就叫做缓存击穿。

说下我遇到的问题:

项目中需要请求其他远程接口的数据,这些数据会在本地接口处理一些请求的时候被使用。

首先避免频繁请求远程接口拿数据,先做个缓存:本地接口处理请求时优先查缓存,没有再请求接口获取数据并更新。

以上逻辑很简单常用,但考虑到我使用了缓存数据的本地接口可能会有很大的并发,如果缓存没有或者失效的情况,就会有很多并发线程同时去请求远程接口,给远程接口造成很大的压力,同时影响了本地接口的响应速度。

下面说下解决思路:

1、加锁排队。

优先查缓存,查不到缓存,就使用redis锁更新缓存。

redis锁:向redis写入更新缓存开始的标记,同时请求远程接口并更新缓存,更新完毕清除redis的标记。其他线程如果查到redis标记存在,就while True阻塞等待直到查出缓存。

该方法的缺点是阻塞了本地接口的并发处理,可能带来潜在问题。

2、逻辑过期时间

缓存设置永不过期,在写入缓存数据时加上生成时间和有效时间,读取缓存时判断时间是否过期,如果过期,就加上redis标记去更新缓存(保证同时只有一个线程更新缓存),其他线程检查到redis更新缓存标记已存在,就直接取走过期的缓存。

3、二级缓存

缓存两份数据,一份设置过期时间,另一份设置永不过期作为二级缓存。优先查缓存,查不到缓存,就查二级缓存,并加上redis标记去更新缓存,其他线程检查到redis更新缓存标记已存在,就直接取走二级缓存。

还有2个要注意的点:

1、在上述逻辑过期二级缓存方法中, 更新缓存这个动作可以写个异步任务去完成。

2、在上述逻辑过期二级缓存方法中,为了解决第一次查缓存就没有数据导致高并发穿过缓存的情况,需要在上线前就准备好缓存数据(比如在django的migrate中或者celery的@worker_ready.connect信号中更新缓存)。

3、其实还有一种办法也可以解决项目中的问题,就是设置一个定时任务去缓存全量数据,但是我觉得太臃肿,太依赖celery

看下代码实现

HOST_BIZ_RELATIONS_CACHE_KEY = "{}host_biz_relations".format(KEY_PREFIX)
HOST_BIZ_RELATIONS_CACHE_TTL = 1 * 24 * 60 * 60
HOST_BIZ_RELATIONS_L2_CACHE_KEY = "{}host_biz_relations_l2".format(KEY_PREFIX)
HOST_BIZ_RELATIONS_UPDATE_LOCK = "{}host_biz_relations_update_lock".format(KEY_PREFIX)
HOST_BIZ_RELATIONS_UPDATE_LOCK_TTL = 10 * 60
class HostBizRelationsManage(object):
"""主机关联业务信息管理"""
def __init__(self, bk_host_id):
self.bk_host_id = int(bk_host_id)
self.client = get_client_by_user(constants.ADMIN_USERNAME_LIST[0])
self.host_biz_relations = self._get_host_biz_relations()
def _get_host_biz_relations(self):
with get_redis_connection() as redis_cli:
# 查一级缓存
cached_relation = redis_cli.hget(constants.HOST_BIZ_RELATIONS_CACHE_KEY, self.bk_host_id)
if cached_relation:
logger.info("_get_host_biz_relations() go l1 cache")
return json.loads(cached_relation)
# 查二级缓存
l2_cached_relation = redis_cli.hget(constants.HOST_BIZ_RELATIONS_L2_CACHE_KEY, self.bk_host_id)
if l2_cached_relation:
logger.info("_get_host_biz_relations() go l2 cache")
update_lock = redis_cli.hget(constants.HOST_BIZ_RELATIONS_UPDATE_LOCK, self.bk_host_id)
if not update_lock: # 没有锁,异步更新缓存
update_host_biz_relations_task.delay(self.get_update_host_biz_relations)
return json.loads(l2_cached_relation)
# 查cmdb接口并更新缓存
logger.info("_get_host_biz_relations() go cmdb")
host_biz_relations = self.get_update_host_biz_relations()
return host_biz_relations
@property
def related_module_set(self):
related_module_set = {
"bk_module_ids": [],
"bk_set_ids": [],
}
for relation in self.host_biz_relations:
related_module_set["bk_module_ids"].append(relation["bk_module_id"]),
related_module_set["bk_set_ids"].append(relation["bk_set_id"])
return related_module_set
def get_update_host_biz_relations(self):
with get_redis_connection() as redis_cli:
try:
redis_cli.hset(constants.HOST_BIZ_RELATIONS_UPDATE_LOCK, self.bk_host_id, "updating") # 加锁
# 请求cmdb数据
host_biz_relations = CommonApiFunc.blueking_api_common_handle(
self.client.cc.find_host_biz_relations, {"bk_host_id": [self.bk_host_id]}
)
assert host_biz_relations, "指定主机ID的业务信息为空"
# 更新缓存
redis_cli.hset(constants.HOST_BIZ_RELATIONS_CACHE_KEY, self.bk_host_id, json.dumps(host_biz_relations))
redis_cli.expire(constants.HOST_BIZ_RELATIONS_CACHE_KEY, constants.HOST_BIZ_RELATIONS_CACHE_TTL)
redis_cli.hset(
constants.HOST_BIZ_RELATIONS_L2_CACHE_KEY, self.bk_host_id, json.dumps(host_biz_relations)
)
return host_biz_relations
finally:
redis_cli.hdel(constants.HOST_BIZ_RELATIONS_UPDATE_LOCK, self.bk_host_id) # 解锁
@task
def update_host_biz_relations_task(func):
logger.info("_get_host_biz_relations() after l2 cache, preparing to query cmdb and update cache")
func()

测试

1、没有缓存,请求cmdb接口,并设置一级二级缓存
image.png

2、上一步做了缓存,走一级缓存
image.png

3、手动删除模拟一级缓存过期
image.png

走了二级缓存,并发异步任务请求cmdb更新缓存
image.png

异步任务执行记录
image.png

缓存更新,一级缓存也有了,再跑一遍就走一级缓存
image.png

posted @   云白Li  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示