记一次缓存击穿的解决

参考:
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 @ 2022-07-14 13:24  云白Li  阅读(81)  评论(0)    收藏  举报