How to solve Redis cache breakdown (invalidation), cache penetration, and cache avalanche?

Raw data is stored in DB (such as MySQL, Hbase, etc.), but DB has low read and write performance and high latency.

For example, MySQL's TPS = 5000 and QPS = about 10,000 on a 4-core 8G, and the average reading and writing time is 10~100 ms.

Using Redis as a caching system can just make up for the lack of DB. "Code Brother" performed the Redis performance test on his MacBook Pro 2019 as follows:

$ redis-benchmark -t set,get -n 100000 -q
SET: 107758.62 requests per second, p50=0.239 msec
GET: 108813.92 requests per second, p50=0.239 msec
复制代码

TPS and QPS reached 100,000 , so we introduced a cache architecture to store the original data in the database, and at the same time store a total copy in the cache.

When a request comes in, the data is first removed from the cache, and if there is any, the data in the cache is returned directly.

If there is no data in the cache, go to the database to read the data and write it to the cache, and then return the result.

Is this seamless? Improper design of the cache will lead to serious consequences. This article will introduce three common problems and solutions in the use of caches:

  • cache breakdown (failure);
  • cache penetration;
  • Cache Avalanche.

Cache breakdown (failure)

High concurrent traffic, the data accessed is hot data, the requested data exists in the DB, but the copy stored in Redis has expired, and the backend needs to load the data from the DB and write it to Redis.

Keywords: single hotspot data, high concurrency, data failure

However, due to high concurrency, the DB may be overwhelmed, making the service unavailable. As shown below:

缓存击穿

solution

Expiration time + random value

For hot data, we do not set the expiration time, so that all requests can be processed in the cache, and the high throughput performance of Redis can be fully utilized.

Or add a random value to the expiration time.

When designing the expiration time of the cache, use the formula: expiration time = baes time + random time.

That is, when the same business data is written to the cache, a random expiration time is added on top of the basic expiration time, so that the data will slowly expire in the future, so as to avoid all of the expired instantaneously, which will cause excessive pressure on the DB.

warm up

Store the popular data in Redis in advance, and set the expiration time of the popular data to a very large value.

use lock

When a cache invalidation is found, data is not loaded from the database immediately.

而是先获取分布式锁,获取锁成功才执行数据库查询和写数据到缓存的操作,获取锁失败,则说明当前有线程在执行数据库查询操作,当前线程睡眠一段时间在重试。

这样只让一个请求去数据库读取数据。

伪代码如下:

public Object getData(String id) {
    String desc = redis.get(id);
        // 缓存为空,过期了
        if (desc == null) {
            // 互斥锁,只有一个请求可以成功
            if (redis(lockName)) {
                try 
                    // 从数据库取出数据
                    desc = getFromDB(id);
                    // 写到 Redis
                    redis.set(id, desc, 60 * 60 * 24);
                } catch (Exception ex) {
                    LogHelper.error(ex);
                } finally {
                    // 确保最后删除,释放锁
                    redis.del(lockName);
                    return desc;
                }
            } else {
                // 否则睡眠200ms,接着获取锁
                Thread.sleep(200);
                return getData(id);
            }
        }
}
复制代码

缓存穿透

缓存穿透:意味着有特殊请求在查询一个不存在的数据,即不数据存在 Redis 也不存在于数据库。

导致每次请求都会穿透到数据库,缓存成了摆设,对数据库产生很大压力从而影响正常服务。

如图所示:

缓存穿透

解决方案

  • 缓存空值:当请求的数据不存在 Redis 也不存在数据库的时候,设置一个缺省值(比如:None)。当后续再次进行查询则直接返回空值或者缺省值。
  • 布隆过滤器:在数据写入数据库的同时将这个 ID 同步到到布隆过滤器中,当请求的 id 不存在布隆过滤器中则说明该请求查询的数据一定没有在数据库中保存,就不要去数据库查询了。

BloomFilter 要缓存全量的 key,这就要求全量的 key 数量不大,10 亿 条数据以内最佳,因为 10 亿 条数据大概要占用 1.2GB 的内存。

说下布隆过滤器的原理吧

BloomFilter 的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。

加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。

检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。

如下图所示:

布隆过滤器

哈希函数会出现碰撞,所以布隆过滤器会存在误判。

这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值。

所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。

对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0。布隆过滤器判断存在不一定真的存在。

缓存雪崩

缓存雪崩指的是大量的请求无法在 Redis 缓存系统中处理,请求全部打到数据库,导致数据库压力激增,甚至宕机。

出现该原因主要有两种:

  • 大量热点数据同时过期,导致大量请求需要查询数据库并写到缓存;
  • Redis 故障宕机,缓存系统异常。

缓存大量数据同时过期

数据保存在缓存系统并设置了过期时间,但是由于在同时一刻,大量数据同时过期。

系统就把请求全部打到数据库获取数据,并发量大的话就会导致数据库压力激增。

缓存雪崩是发生在大量数据同时失效的场景,而缓存击穿(失效)是在某个热点数据失效的场景,这是他们最大的区别。

如下图:

缓存雪崩-大量缓存同时失效

解决方案

过期时间添加随机值

要避免给大量的数据设置一样的过期时间,过期时间 = baes 时间+ 随机时间(较小的随机数,比如随机增加 1~5 分钟)。

这样一来,就不会导致同一时刻热点数据全部失效,同时过期时间差别也不会太大,既保证了相近时间失效,又能满足业务需求。

接口限流

当访问的不是核心数据的时候,在查询的方法上加上接口限流保护。比如设置 10000 req/s。

如果访问的是核心数据接口,缓存不存在允许从数据库中查询并设置到缓存中。

这样的话,只有部分请求会发送到数据库,减少了压力。

限流,就是指,我们在业务系统的请求入口前端控制每秒进入系统的请求数,避免过多的请求被发送到数据库。

如下图所示:

缓存雪崩-限流

Redis 故障宕机

一个 Redis 实例能支撑 10 万的 QPS,而一个数据库实例只有 1000 QPS。

一旦 Redis 宕机,会导致大量请求打到数据库,从而发生缓存雪崩。

解决方案

对于缓存系统故障导致的缓存雪崩的解决方案有两种:

  • 服务熔断和接口限流;
  • 构建高可用缓存集群系统。

服务熔断和限流

在业务系统中,针对高并发的使用服务熔断来有损提供服务从而保证系统的可用性。

服务熔断就是当从缓存获取数据发现异常,则直接返回错误数据给前端,防止所有流量打到数据库导致宕机。

服务熔断和限流属于在发生了缓存雪崩,如何降低雪崩对数据库造成的影响的方案。

构建高可用的缓存集群

所以,缓存系统一定要构建一套 Redis 高可用集群,比如 《Redis 哨兵集群》或者 《Redis Cluster 集群》,如果 Redis 的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

总结

  • 缓存穿透指的是数据库本就没有这个数据,请求直奔数据库,缓存系统形同虚设。
  • **缓存击穿(失效)**指的是数据库有数据,缓存本应该也有数据,但是缓存过期了,Redis 这层流量防护屏障被击穿了,请求直奔数据库。
  • 缓存雪崩指的是大量的热点数据无法在 Redis 缓存中处理(大面积热点数据缓存失效、Redis 宕机),流量全部打到数据库,导致数据库极大压力。

参考资料

segmentfault.com/a/119000003…

cloud.tencent.com/developer/a…

learn.lianglianglee.com/

time.geekbang.org/

Guess you like

Origin juejin.im/post/7083748389732499463