基于 Guava Cache + Redis + Spring Cache 实现分布式二级缓存方案

源码地址:

源码地址:https://gitee.com/ck-jesse/l2cache
具体实现文章见:https://blog.csdn.net/icansoicrazy/article/details/106635446

基于 Caffeine实现

一、概要:

1、一级缓存

Guava Cache ,本地缓存。

2、二级缓存

Redis ,分布式缓存。
提供扩展点,可实现为基于其他分布式缓存的方案

3、使用方式:

注解方式:

1、基于Spring Cache进行扩展 ,利用Spring Cache的注解来提高使用的便捷性,同时方便与Java主流开源框架集成
注:对于过期时间需要扩展

2、自定义实现一套cache注解

API方式:

提供简洁的缓存API

二、核心流程:

1、先读本地cache

命中缓存,则返回;

未命中缓存,则读Redis

2、读Redis缓存

命中缓存,先写本地cache,再返回;

未命中缓存,则读DB;

3、读DB

命中数据,先写本地cache,再写Redis,然后返回;

缓存未命中,DB命中时,分为下面两种情况:
当大量请求访问同一个key时,导致大量请求都打到DB上,这种场景为缓存击穿
当大量请求访问不同key时,导致大量请求都打到DB上,这种场景为缓存击穿

未命中数据,则返回;

缓存和DB都未命中,这种场景为缓存穿透

注:本地cache基于Guava Cache实现,可实现只阻塞一个加载线程,其他线程阻塞

三、问题分析:

1、缓存击穿怎么处理?

概念:当大量请求访问同一个key时,导致大量请求都打到DB上,这种场景为缓存击穿。

大量请求访问同一个key的缓存击穿场景

分析:

大量请求会造成某一时刻数据库压力过载。

方案:互斥锁同步加载数据。

在第一个请求上使用互斥锁,其他请求均会阻塞至第一个请求加载数据完成(查询数据、缓存数据、释放锁),然后从缓存中获取数据。

由于会阻塞其他线程,所以系统吞吐量下降。

实现:

基于Guava Cache实现,重写CacheLoader#load方法来加载数据。

只有一个线程加载数据,其他线程阻塞的目的。简单、稳定、高效。

缓存中有旧数据:只阻塞更新数据的线程,其余线程返回旧数据。

缓存中无旧数据:一个线程去加载数据,其余线程都阻塞了。

大量请求访问不同key的缓存击穿场景

分析:

大量请求会造成某一时刻数据库压力过载。

对于大量请求访问同一个key的场景,Guava Cache默认加互斥锁同步加载数据,保证最终只有一个请求打到数据库;但是对于大量请求访问不同key的场景,在此基础上还是会有大量请求打到数据,所以关键点是要限制最终打到数据库的请求,此时可采用线程池异步加载数据,保证只有一定的请求同时打到数据库。

方案:

方案一:线程池异步加载数据。

一方面,解决单key被互斥锁同步阻塞的问题,另一方面,解决多key大量请求打到数据库的问题。

方案二:结合Hystrix或Sentinel进行限流和降级,比如一秒来了5000请求,假设设置一秒只能通过2000个请求,那么剩余的3000请求就会走限流逻辑。然后调用自定义的降级逻辑(比如设置默认值之类的),以此来保护最后的数据库不会被大量请求给打死。

实现:

还是基于Guava Cache实现,不过要重写CacheLoader#reload()方法,在此方法中将加载数据逻辑交给线程池异步执行。所有的请求都返回旧的数据,这样就不会有请求被阻塞了。

缓存中有旧数据:所有线程返回旧数据,线程池异步加载数据。

缓存中无旧数据:一个线程去加载数据,其余线程都阻塞了。所以需要在系统启动时,就预先将数据加载到缓存。

2、缓存穿透怎么处理?

概念:查询不存在数据的现象我们称为缓存穿透。

简单方法:缓存空值;

复杂方法:大数据场景应用较多。在缓存之前在加一层 BloomFilter ,在查询的时候先去 BloomFilter 去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。

3、缓存一致性的问题?

本地CacheRedis缓存一致性怎么保证?

假设过期时间expireTime=60s;

首先加载数据,添加数据到本地cache,并设置过期时间expireTime=60s,

再往Redis写入数据,并设置过期时间expireTime-1=59s,也就是redis比本地缓存提前1s过期

假设某个时间点有一个请求过来,从本地cache中没有找到数据,再从redis获取数据,若有,则设置本地缓存和过期时间(过期时间从redis数据中获取),若无,则查DB

集群中怎么保证每个节点中本地缓存的一致性?

方案一:

获取缓存的节点从Redis中获取缓存数据和剩余过期时间,然后往本地cache中添加数据,并设置本地cache的过期时间=redis中缓存的剩余过期时间。

方案二:

通过消息队列(kafka/rocketmq)通知其他节点更新缓存。

以便保证不同节点的本地cache的过期时间的一致性。

你可能感兴趣的:(缓存,java,spring,boot,分布式)