记一次缓存击穿的解决
参考:
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接口,并设置一级二级缓存
2、上一步做了缓存,走一级缓存
3、手动删除模拟一级缓存过期
走了二级缓存,并发异步任务请求cmdb更新缓存
异步任务执行记录
缓存更新,一级缓存也有了,再跑一遍就走一级缓存
本文来自博客园,作者:云白Li,转载请注明原文链接:https://www.cnblogs.com/libaiyun/p/16477367.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下