高并发下 Redis 缓存穿透,击穿,雪崩

本文详细阐述了Redis缓存中常见的三大问题:缓存穿透、缓存击穿及缓存雪崩的概念及其解决方法,并提供了具体的代码实现。

Redis 缓存穿透,击穿,雪崩

概念
  • 缓存穿透: 当访问一个并不存在的Key的时候。这个请求就会请求到数据源。在高并发的情况下,可能会压垮数据源 。
  • 缓存击穿: Redis中数据的Key存在但是马上要过期。当这个key过期的时候,此时有大量的请求发送过来,这些请求发现缓存过期一般都会从后端数据库加载数据然后再写入缓存,这个时候在高并发的情况下可能会瞬间就把服务端DB击穿。
  • 缓存雪崩: 但缓存服务器重启或者大量的缓存集中在某一个时间段失效,这样在失效的时候也会给后端(例如DB带来很大的压力)
解决办法
  • 缓存穿透

    (1)第一种办法就是做一个足够大的索引map。将所有可能存在的Key都写入map。这样子的话当大量并发的时候请求就会被map所拦截下来而不会去访问数据源。

    (2)第二种办法,也是最暴力的解决办法,就是当访问一个并不存在的key的时候,直接把这个空的结果进行缓存,然后为这个空缓存设置一个较短的过期时间。

  • 缓存击穿

    (1)使用互斥锁 mutex key . 当缓存失效的时候,不要立即去load db,可以先使用带有返回值操作的setnx(具有原子性) 方法来设置一个mutex key 。然后再进行load db操作并重新设置缓存,新的缓存设置成功之后删掉mutex key。这样的话若是或得到了mutex key 说明缓存正在重写。可以使用sleep方法等待一段时间,然后再次调用自身 直到获取不到mutex key为止。

    ​ 代码实现:

    def get_catch_key(key):
        catch_obj = redis.get(key)
        if not catch_obj:
            # 代表设置成功 加上了互斥锁 ,此时进行数据缓存
            if(redis.setnx('multx_key',1,3*60) == 1): 
                try:
                  catch_obj = get_value(key)  # 此方法从数据库中加载
                  redis.set(key,catch_obj,expire_secs)  # 重新设置缓存
                  redis.del('multx_key')  # 去掉互斥锁
                except Exception as e: 
                  # 此时的异常捕捉是为了防止数据没有设置成功而导致锁丢失从而阻塞其他线程
                  redis.del('multx_key')  
            else:   # 说明互斥锁设置失败。此时已经有其他的线程在进行数据缓存了。
                time.sleep(0.1)         # 睡眠0.1s后重试
                get_catch_key(key)    
    
        return catch_obj
    
  • 缓存雪崩

    (1)在设置key的过期时间的时候可以使用随机。可以使key在原来的基础上再随机添加 1~5 min的过期时间,这样就不会导致大量的ke在同一时间失效从而对服务器造成大量的冲击。

    (2)标记缓存的方法。每次设置一个Key的时候都设置一个标记sign key。无论何时sign key的过期时间都是key的一半。在每次获取数据的时候先获取 sign key若sign key已过期,再加互斥锁mutex key重新更新key 和 sign key的缓存数据以及时间。这样就保证了标记sign key 永远比key先失效,然后再去更新 key。

    ​ 代码实现:

    def get_catch_key(key):
        catch_obj = redis.get(key)
        if not catch_obj:
            if redis.setnx('multx_key', 1, 3 * 60) == 1: 
                    catch_obj = get_value(key)  
                    redis.set(key, catch_obj, expire_secs)  # 重新设置缓存
                    # 设置标记key缓存过期时间为key的一半
                    redis.set(key + '_sign', 1, expire_secs / 2)  
                    redis.del('multx_key')  
                except Exception as e:
                    redis.del('multx_key') 
            else:  
                time.sleep(0.1)  
                get_catch_key(key)
    
        # 这里可以放在Celery异步任务中去处理 提升程序运行效率
        if not redis.get(key + '_sign'):  # 说明标记key已过期,重新设置缓存和标记key
            if redis.setnx('multx_sign_key', 1, 3 * 60) == 1:
                try:
                    catch_obj = get_value(key)
                    redis.set(key, catch_obj, expire_secs)
                    redis.set(key + '_sign', 1, expire_secs / 2)
                    redis.del('multx_sign_key')
                except Exception as e:
                    redis.del('multx_sign_key')
    
        return catch_obj
    
### 三级标题:Redis缓存击穿穿透雪崩的区别与解决方法 Redis缓存高并发系统中提高性能的重要工具,但其在特定场景下可能引发缓存击穿缓存穿透缓存雪崩等问题。这些问题虽然都与缓存失效有关,但其触发条件和影响范围各不相同,解决方案也各有侧重。 #### 缓存击穿 缓存击穿指的是某个热点数据在缓存中过期后,短时间内有大量请求直接访问数据库,导致数据库压力骤增[^2]。这种情况通常发生在访问频率极高的数据上,例如双十一期间的商品信息。解决缓存击穿的方法包括: - **永不过期策略**:将热点数据设置为永不过期,或者通过后台异步更新机制来刷新缓存,避免因过期而引发数据库压力[^2]。 - **互斥锁(Mutex)**:当缓存未命中时,第一个线程获取互斥锁并查询数据库,其他线程等待锁释放后再从缓存中获取数据,从而避免并发请求直接访问数据库[^3]。 #### 缓存穿透 缓存穿透是指查询一个既不在缓存也不在数据库中的数据,导致每次请求都直接访问数据库[^1]。攻击者可以通过构造不存在的查询来恶意攻击数据库。常见的解决方法有: - **布隆过滤器(Bloom Filter)**:在请求到达数据库之前,使用布隆过滤器判断数据是否存在。如果布隆过滤器返回不存在,则直接返回错误,不查询数据库[^1]。 - **缓存空值(Null Value)**:对于查询结果为空的情况,可以将空值也缓存一段时间,并设置较短的过期时间,以减少对数据库的无效查询[^1]。 #### 缓存雪崩 缓存雪崩是指大量缓存数据在同一时间过期,或者Redis服务宕机导致所有请求都转发到数据库,可能引发数据库崩溃[^1]。这种情况通常发生在系统时间同步或缓存集中失效时。解决缓存雪崩的方法包括: - **分散过期时间**:为缓存数据设置随机的过期时间,避免所有缓存同时失效。例如,在基础过期时间上加上一个随机值。 - **集群部署与高可用**:通过Redis集群部署和主从复制机制,确保即使某个节点宕机,其他节点仍能提供服务,从而避免缓存雪崩[^1]。 - **降级熔断机制**:当Redis服务不可用时,系统可以启用降级模式,例如返回默认值或缓存中的历史数据,以减轻数据库压力[^1]。 #### 总结 缓存击穿穿透雪崩虽然都与缓存失效有关,但它们的触发条件和影响范围不同: - **缓存击穿**:热点数据过期后引发大量请求访问数据库。 - **缓存穿透**:查询不存在的数据,导致请求直接访问数据库。 - **缓存雪崩**:大量缓存同时失效或Redis服务宕机导致请求全部转发到数据库。 针对不同的问题,需要采取相应的解决方案,例如互斥锁、布隆过滤器、分散过期时间和高可用部署等。 ```java // 示例:使用互斥锁解决缓存击穿问题 public String getData(String key) { String data = redis.get(key); if (data == null) { synchronized (this) { data = redis.get(key); if (data == null) { data = db.query(key); // 从数据库中查询数据 redis.set(key, data, 60); // 设置缓存 } } } return data; } ``` ```java // 示例:使用布隆过滤器解决缓存穿透问题 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 1000000); public String getDataWithBloomFilter(String key) { if (!bloomFilter.mightContain(key)) { return "Key does not exist"; } String data = redis.get(key); if (data == null) { data = db.query(key); redis.set(key, data, 60); } return data; } ``` ```java // 示例:分散过期时间解决缓存雪崩问题 public void setCacheWithRandomExpire(String key, String value) { int expireTime = 60 + new Random().nextInt(20); // 基础过期时间加上随机值 redis.setex(key, expireTime, value); } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值