Bpazy / blog

我的博客,欢迎关注和讨论
https://github.com/Bpazy/blog/issues
MIT License
41 stars 2 forks source link

缓存系统 #301

Open Bpazy opened 1 year ago

Bpazy commented 1 year ago

几个重点:

  1. 击穿、穿透、雪崩
  2. 二级缓存
  3. 布隆过滤器

image

Bpazy commented 1 year ago

缓存雪崩

当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。 image

造成缓存雪崩的关键:在同一时间大规模的key失效。

出现缓存雪崩的原因:

  1. 可能是 Redis 宕机。

    如果 Redis 集群中的某个节点宕机,会导致该节点上的所有 key 同时失效,进而导致请求全部转向其他节点,从而引发雪崩

  2. 可能是采用了相同的过期时间。

缓存雪崩的解决方案

  1. 搭建 Redis 集群,防止 Redis 宕机导致缓存雪崩的问题,提高 Redis 的容灾性。
  2. 在原有的失效时间上加一个随机值(比如 1-5 分钟随机),避免采用相同过期时间的 key 同时失效。
  3. 提高数据库的容灾能力,可以使用分库分表,读写分离的策略。
  4. 增加兜底措施(熔断机制、服务降级、布隆过滤器),防止过多请求打到数据库。
  5. 缓存后加锁。
Bpazy commented 1 year ago

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

名词造的挺多,和雪崩的区别是热点 key 失效导致大量请求打到数据库: image

解决方案和雪崩类似,另外可以考虑让热点缓存不要过期

Bpazy commented 1 year ago

缓存穿透

当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透的发生一般有这两种情况:

应对缓存穿透的方案,常见的方案有三种。

第一种方案,非法请求的限制

当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。

第二种方案,缓存空值或者默认值

当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。

关于布隆过滤器的知识可以参考这里: https://github.com/Bpazy/blog/issues/302

Bpazy commented 1 year ago

What ttl means?

Time to live,缓存存在时长

Bpazy commented 1 year ago

容量预估、热key、大key、穿透、缓存和数据库一致性问题、建议换新key(上线是否有兼容问题)、过期时间(本地缓存)、redis抖动

Bpazy commented 1 year ago

堆外缓存

在互联网项目中,一般以堆内缓存的使用居多,无论是Guava,Memcache,还是JDK自带的HashMap,ConcurrentHashMap等,都是在堆内内存中做数据计算操作。这样做的好处显而易见,用户完全不必在意数据的分配,溢出,回收等操作,全部交由JVM来进行处理。由于JVM提供了诸多的垃圾回收算法,可以保证在不影响甚至微影响系统的前提下,做到堆内内存接近完美的管控。君不见,小如图书管理这样的系统,大如整个电商交易平台,都在JVM的加持下,服务于几个,十几个,乃至于上亿用户,而在这些系统中,堆内缓存组件所带来的收益可是居功至伟。在自下而上的互联网架构中,堆内缓存就像把卫这宫廷入口的剑士,神圣而庄严,真可谓谁敢横刀立马,唯我堆内缓存将军。

堆内缓存的劣势

但是,事物都是有两面性的,堆内缓存在JVM的管理下,纵然无可挑剔,但是在GC过程中产生的程序小停顿和程序大停顿,则像一把利剑一样,斩断了对构造出完美高并发系统的念想。简单的以HashMap这个JDK自带的缓存组件为例,benchmark结果如下:

Benchmark                                   Mode  Cnt          Score          Error             Units
localCacheBenchmark.testlocalCacheSet      thrpt   20      85056.759 ±   126702.544  ops/s

其插入速度最快为85056.759+126702.544=211759.303ops,最慢为0,也就是每秒插入速度最快为20w,最慢为0。之所以为0,是因为HashMap中的数据在快速的增长过程中,引起了频繁的GC操作,为了给当前HashMap腾出足够的空间进行插入操作,不得不释放一些对象。频繁的GC,势必对插入速度有不小的影响,造成应用的偶尔性暂停。所以这也能解释为啥最慢的时候,ops为0了。 同时从benchmark数据,我们可以看到误差率为126702.544ops,比正常操作的85056.756要大很多,说明GC的影响,对HashMap的插入操作影响特别的大。

由于GC的存在,堆内缓存操作的ops会受到不小的影响,会造成原本小流量下10ms能够完成的内存计算,大流量下500ms还未完成。如果内存计算过于庞杂,则造成整体流程的ops吞吐量降低,也是极有可能的事儿。所以从这里可以看出,堆内缓存组件,在高并发的压力下,如果计算量巨大,尤其是写操作巨大,使其不会成为护城的利剑,反而成了性能的帮凶,何其可惧。

为了缓解在高并发,高写入操作下,堆内缓存组件造成的频繁GC问题,堆外缓存应运而生。

堆外缓存优秀项目很多,比如 https://github.com/ben-manes/caffeine, native 代码部分依赖的是由 rust 编写的项目 https://github.sheincorp.cn/moka-rs/moka

引用: https://www.cnblogs.com/scy251147/p/9634766.html