• 缓存穿透、缓存击穿、缓存雪崩


    前言:

    在设计缓存系统时,就不得不考虑所谓:缓存穿透、缓存击穿、缓存雪崩,这三大问题。

    缓存设计一般遵循如下流程图:

    一、缓存穿透:

    缓存穿透是指查询一个一定不存在的数据(某Key对应的缓存和DB数据都不存在),由于缓存是不命中需要从数据库中查询,查询不到则不会写缓存,此时若缓存和DB 都查询不到,那么这将导致每次请求数据都要到数据库去查询,造成缓存穿透

    这种情况失去了缓存的意义,在流量大时,DB很可能就挂掉了,要是有人利用不存在的Key频繁攻击我们,这就是我们的漏洞。

    解决方案:

    1.布隆过滤:

    (1)可以对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃。例如我们查找CountryCode+Currency 对应的货币符号,我们可以把这个Key的所有可能值都以hash形式存储,不符合这个hash的数据则不能进行查询缓存,直接就返回了。

    (2)所谓布隆过滤,即将所有可能存在参数数据哈希到一个足够大的bitmap中,一个一定不存在的查询参数被这个bitmap拦截掉,从而避免了对底层系统的查询压力。这样做的好处是优先从控制层进行过滤,不符合条件的Key被拒绝掉,减轻查询压力。

    2.缓存空对象

      缓存空对象,将null变成一个值,具体做法是不管缓存能否查询到数据,将该查询参数对应的数据以空的形式存储,但切记设置过期时间,一般不会超过5分钟。

    缓存空对象带来的问题:

      第一、每个不存在的Key对应的数据都以空值存储,那么首先它将消耗更多的内存空间,一旦遇到攻击,那么后果很严重,所以这种办法一般都会设置一个较短的过期时间,过期后数据自动剔除。

      第二、缓存层和业务层存在一段时间二者数据不一致问题,可能对业务会有影响。例如过期时间设置为5分钟,那么在这5分钟内一旦DB中有新数据添加,而此时缓存中还缓存的空值,那么二者存在数据不一致性问题。

      解决该问题,可以利用消息系统或其他方式清除缓存中的空数据。

    二、缓存雪崩:

    如果缓存在一段时间内同时失效,例如我们在设置缓存时,采用了相同的过期时间,导致在某一时刻所有缓存同时失效,请求全部到DB上,DB瞬时压力过重导致雪崩。

    解决方案:

      首先强调的是缓存雪崩对底层系统的冲击非常可怕。但很遗憾的是目前并没有完美的解决方案。

      1.大多数设计者考虑“加锁”或者“队列”方式保证缓存的单线程(进程)写,从而避免大量并发请求落到底层存储系统上。比如某个Key只允许一个线程查询和写缓存,其他线程等待。

      2.有一个简单处理方案,就是将缓存失效时间分散开,比如我们在原有失效时间上增加一个随机值,如1~5分钟随机,尽量让缓存不要同时失效,从而尽量避免缓存雪崩。

    三、缓存击穿

    对于一些设置了过期时间的Key,当这些Key在被某些时间点大量高并发访问时,这个时候就需要考虑缓存被“击穿”的问题,这个问题和雪崩区别在于只针对某个Key的缓存,而缓存雪崩是针对多个Key的缓存。

    简单来说,就是当某个时间点某个Key被高并发访问,此时恰好缓存过期,那么所有请求都落到DB上了,这是瞬时的大并发就有可能导致将DB压垮,这种现象就叫缓存击穿。

    解决方案:

     1.使用互斥锁(mutex key)

      业界常用的方法,就是加互斥锁,简单来说就是缓存失效的时候,不要直接load DB,而是加一个锁,简单处理就是如下伪代码方式,锁没释放前,第二个线程过来需要等待才能去DB中load数据。

    上述代码说明:

    (1)缓存中有数据,直接走上述13行代码直接返回结果。

    (2)缓存中没有数据,第一个进入的线程,获取锁并从数据库中取数据,没释放锁之前,其他并行线程进入的线程会等待100ms,再重新去缓存中取数据,这样就起到了防止都去DB中重复取数据,重复往缓存中更新数据的情况。

    (3)上图这种是简化处理,理论上如果能根据key值加锁就更好了,就是线程A 从数据库取Key1的数据并不妨碍线程B取Key2的数据,上述代码明显做不到这一点。

    互斥锁业界常用方法(Redis和Memcache):

    业界最常用的方法,就是加互斥锁,简单来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load DB,而是先使用缓存公共的某些带成功返回值得操作(比如Redis的SETNX或者Memcache的ADD),去Set一个Mutex key,当操作返回成功时,再进行Load DB 的操作。SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

    public String get(key) {
          String value = redis.get(key);
          if (value == null) { //代表缓存值过期
              //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
              if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
                   value = db.get(key);
                          redis.set(key, value, expire_secs);
                          redis.del(key_mutex);
                  } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                          sleep(50);
                          get(key);  //重试
                  }
              } else {
                  return value;      
              }
     }
    View Code

     memcache代码:

    if (memcache.get(key) == null) {  
        // 3 min timeout to avoid mutex holder crash  
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {  
            value = db.get(key);  
            memcache.set(key, value);  
            memcache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    } 
    View Code 

    2.设置缓存“永远不过期”

      这里包含两层意思:

    (1)从Redis上看,确实没有设置过期时间,这就保证了不会出现热点key过期的问题,也就是“物理”不过期。

    (2)从功能上看,如果不过期,那不成了静态的吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台异步线程进行缓存的构建,也就是“逻辑”过期。

    从实战上来看,这种方法对于性能非常友好,唯一不足的就是构建缓存的时候,其余线程(非构建缓存的线程)可能访问的是老数据,但对于一般的互联网功能来说这个还是可以忍受的。

    示例代码:

    String get(final String key) {  
            V v = redis.get(key);  
            String value = v.getValue();  
            long timeout = v.getTimeout();  
            if (v.timeout <= System.currentTimeMillis()) {  
                // 异步更新后台异常执行  
                threadPool.execute(new Runnable() {  
                    public void run() {  
                        String keyMutex = "mutex:" + key;  
                        if (redis.setnx(keyMutex, "1")) {  
                            // 3 min timeout to avoid mutex holder crash  
                            redis.expire(keyMutex, 3 * 60);  
                            String dbValue = db.get(key);  
                            redis.set(key, dbValue);  
                            redis.delete(keyMutex);  
                        }  
                    }  
                });  
            }  
            return value;  
    }
    View Code
  • 相关阅读:
    Handling Errors and Exceptions
    Advanced Features of Delphi DLLs
    How to put a relative path for a DLL statically loaded?
    Delphi DLL制作和加载 Static, Dynamic, Delayed 以及 Shared-Memory Manager
    The useful App Paths registry key : HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionApp Paths
    DLL Dynamic-Link Library Search Order
    Can a windows dll retrieve its own filename?
    Restrict form resize -- Delphi
    Programmer in Google Code
    How to open a web site with the default web browser in a NEW window
  • 原文地址:https://www.cnblogs.com/vpersie2008/p/12253429.html
Copyright © 2020-2023  润新知