简介

layering-cache是一个支持分布式环境的多级缓存框架,使用方式和spring-cache类似。一级缓存使用Caffeine作为本地缓存,二级缓存使用redis作为集中式缓存。一级缓存和二级缓存的数据一致性是通过推和拉两种模式相结合的方式来实现的。推主要是基于redis的pub/sub机制,拉主要是基于消息队列和记录消费消息的偏移量来实现的。

支持

  • 支持缓存命中率的监控统计,统计数据上报支持自定义扩展
  • 内置dashboard,支持对缓存的管理和缓存命中率的查看
  • 支持缓存过期时间在注解上直接配置
  • 支持缓存的自动刷新(当缓存命中并发现二级缓存将要过期时,会开启一个异步线程刷新缓存)
  • 缓存Key支持SpEL表达式
  • Redis支持Kryo、FastJson、Jackson、Jdk和Protostuff序列化,默认使用Protostuff序列化,并支持自定义的序列化
  • 支持同一个缓存名称设置不同的过期时间
  • 支持禁用一级缓存,只使用二级缓存

优势

  1. 提供缓存命中率的监控统计,统计数据上报支持自定义扩展
  2. 支持本地缓存和集中式两级缓存
  3. 接入成本和使用成本都非常低
  4. 无缝集成Spring、Spring boot
  5. 内置dashboard使得缓存具备可运维性
  6. 通过缓存空值来解决缓存穿透问题、通过异步加载缓存的方式来解决缓存击穿和雪崩问题

快速开始

集成 Spring Boot

  1. 引入layering-cache
<dependency>
<groupId>com.github.xiaolyuh</groupId>
<artifactId>layering-cache-starter</artifactId>
<version>${layering.version}</version>
</dependency>
  1. 添加配置
#layering-cache 配置
layering-cache.stats=true
# 缓存命名空间,如果不配置取 "spring.application.name"
layering-cache.namespace=layering-cache-web

# redis单机
layering-cache.redis.database=0
layering-cache.redis.host=127.0.0.1
layering-cache.redis.port=6379
layering-cache.redis.password=
# redis集群
#layering-cache.redis.password=
#layering-cache.redis.cluster=127.0.0.1:6379,127.0.0.1:6378

# 设置redis值的序列化方式,默认是Protostuff
#com.github.xiaolyuh.redis.serializer.KryoRedisSerializer
#com.github.xiaolyuh.redis.serializer.FastJsonRedisSerializer
#com.github.xiaolyuh.redis.serializer.JacksonRedisSerializer
#com.github.xiaolyuh.redis.serializer.JdkRedisSerializer
#com.github.xiaolyuh.redis.serializer.ProtostuffRedisSerializer
layering-cache.redis.serializer=com.github.xiaolyuh.redis.serializer.JacksonRedisSerializer
  1. 配置类中添加注解​​@EnableLayeringCache​​​启用​​layering-cache​
@SpringBootApplication
@EnableLayeringCache
public class LayeringCacheStartDemoApplication {

public static void main(String[] args) {
SpringApplication.run(LayeringCacheStartDemoApplication.class, args);
}
}

集成 Spring

  1. 引入layering-cache
  • maven 方式
<dependency>
<groupId>com.github.xiaolyuh</groupId>
<artifactId>layering-cache-aspectj</artifactId>
<version>${layering.version}</version>
</dependency>
  • gradle 方式
compile 'com.github.xiaolyuh:layering-cache:${layering.version}'
  1. RedisClient(可以参考layering-cache-aspectj的Test配置)
@Configuration
@PropertySource({"classpath:application.properties"})
public class RedisConfig {

@Value("${spring.redis.database:0}")
private int database;

@Value("${spring.redis.host:192.168.83.128}")
private String host;

@Value("${spring.redis.password:}")
private String password;

@Value("${spring.redis.port:6378}")
private int port;

@Bean
public RedisClient layeringCacheRedisClient() {
RedisProperties redisProperties = new RedisProperties();
redisProperties.setDatabase(database);
redisProperties.setHost(host);
redisProperties.setPassword(StringUtils.isBlank(password) ? null : password);
redisProperties.setPort(port);

KryoRedisSerializer<Object> kryoRedisSerializer = new KryoRedisSerializer<>(Object.class);
StringRedisSerializer keyRedisSerializer = new StringRedisSerializer();
SingleRedisClient redisClient = new SingleRedisClient(redisProperties);
redisClient.setKeySerializer(keyRedisSerializer);
redisClient.setValueSerializer(kryoRedisSerializer);
return redisClient;
}
}
  1. 声明CacheManager和LayeringAspect
/**
* 多级缓存配置
*
* @author yuhao.wang3
*/
@Configuration
@Import({RedisConfig.class})
@EnableAspectJAutoProxy
public class CacheConfig {

@Bean
public CacheManager layeringCacheManager(RedisClient layeringCacheRedisClient, CacheStatsReportService cacheStatsReportService, LayeringCacheProperties layeringCacheProperties) {

LayeringCacheManager layeringCacheManager = new LayeringCacheManager(layeringCacheRedisClient);
// 默认开启统计功能
layeringCacheManager.setStats(layeringCacheProperties.isStats());
// 上报缓存统计信息
layeringCacheManager.setCacheStatsReportService(cacheStatsReportService);
// 设置缓存命名空间
GlobalConfig.setNamespace(StringUtils.isBlank(layeringCacheProperties.getNamespace()) ? applicationName : layeringCacheProperties.getNamespace());
return layeringCacheManager;
}

@Bean
@ConditionalOnMissingBean(CacheStatsReportService.class)
public CacheStatsReportService cacheStatsReportService() {
return new DefaultCacheStatsReportServiceImpl();
}
}

使用

注解形式

直接在需要缓存的方法上加上Cacheable、CacheEvict、CachePut注解。

  • Cacheable注解
@Cacheable(value = "user:info", depict = "用户信息缓存", enableFirstCache = true,
firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))
public User getUser(User user) {
logger.debug("调用方法获取用户名称");
return user;
}
  • CachePut注解
@CachePut(value = "user:info", key = "#userId", depict = "用户信息缓存", enableFirstCache = true,
firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS),
secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))
public User putUser(long userId) {
User user = new User();
user.setUserId(userId);
user.setAge(31);
user.setLastName(new String[]{"w", "y", "h"});

return user;
}
  • CacheEvict注解
@CacheEvict(value = "user:info", key = "#userId")
public void evictUser(long userId) {

}

@CacheEvict(value = "user:info", allEntries = true)
public void evictAllUser() {
}

直接使用API

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {CacheConfig.class})
public class CacheCoreTest {
private Logger logger = LoggerFactory.getLogger(CacheCoreTest.class);

@Autowired
private CacheManager cacheManager;

@Test
public void testCacheExpiration() {
FirstCacheSetting firstCacheSetting = new FirstCacheSetting(10, 1000, 4, TimeUnit.SECONDS, ExpireMode.WRITE);
SecondaryCacheSetting secondaryCacheSetting = new SecondaryCacheSetting(10, 4, TimeUnit.SECONDS, true);
LayeringCacheSetting layeringCacheSetting = new LayeringCacheSetting(firstCacheSetting, secondaryCacheSetting);

String cacheName = "cache:name";
String cacheKey = "cache:key1";
LayeringCache cache = (LayeringCache) cacheManager.getCache(cacheName, layeringCacheSetting);
cache.get(cacheKey, () -> initCache(String.class));
cache.put(cacheKey, "test");
cache.evict(cacheKey);
cache.clear();
}

private <T> T initCache(Class<T> t) {
logger.debug("加载缓存");
return (T) "test";
}
}

注解说明

@Cacheable

表示用的方法的结果是可以被缓存的,当该方法被调用时先检查缓存是否命中,如果没有命中再调用被缓存的方法,并将其返回值放到缓存中。

名称

默认值

说明

value

空字符串数组

缓存名称,cacheNames的别名

cacheNames

空字符串数组

缓存名称

key

空字符串

缓存key,支持SpEL表达式

depict

空字符串

缓存描述(在缓存统计页面会用到)

enableFirstCache

true

是否启用一级缓存

firstCache

一级缓存配置

secondaryCache

二级缓存配置

@FirstCache

一级缓存配置项

名称

默认值

说明

initialCapacity

10

缓存初始Size

maximumSize

5000

缓存最大Size

expireTime

9

缓存有效时间

timeUnit

TimeUnit.MINUTES

时间单位,默认分钟

expireMode

ExpireMode.WRITE

缓存失效模式,ExpireMode.WRITE:最后一次写入后到期失效,ExpireMode.ACCESS:最后一次访问后到期失效

@SecondaryCache

二级缓存配置项

名称

默认值

说明

expireTime

5

缓存有效时间

preloadTime

1

缓存主动在失效前强制刷新缓存的时间,建议是 expireTime * 0.2

timeUnit

TimeUnit.HOURS

时间单位,默认小时

forceRefresh

false

是否强制刷新(直接执行被缓存方法)

magnification

1

非空值和null值之间的时间倍率,默认是1。如expireTime=60秒,magnification=10,那么当缓存空值时,空值的缓存过期时间是60/10=6秒。

@CachePut

将数据放到缓存中

名称

默认值

说明

value

空字符串数组

缓存名称,cacheNames的别名

cacheNames

空字符串数组

缓存名称

key

空字符串

缓存key,支持SpEL表达式

depict

空字符串

缓存描述(在缓存统计页面会用到)

enableFirstCache

true

是否启用一级缓存

firstCache

一级缓存配置

secondaryCache

二级缓存配置

@CacheEvict

删除缓存

名称

默认值

说明

value

空字符串数组

缓存名称,cacheNames的别名

cacheNames

空字符串数组

缓存名称

key

空字符串

缓存key,支持SpEL表达式

allEntries

false

是否删除缓存中所有数据,默认情况下是只删除关联key的缓存数据,当该参数设置成 true 时 key 参数将无效

监控统计功能

Layering Cache 的监控统计功能默认是开启的

Spring

直接在声明CacheManager Bean的时候将stats设置成true。

/**
* 多级缓存配置
*
* @author yuhao.wang3
*/
@Configuration
@EnableAspectJAutoProxy
public class CacheConfig {

@Bean
public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);
// 默认开启统计功能
layeringCacheManager.setStats(true);
return layeringCacheManager;
}
...
}

Spring Boot

在application.properties文件中添加以下配置即可

layering-cache.stats=true

缓存监控数据上报扩展

缓存监控数据上报扩展

去实现​​CacheStatsReportService​​​接口,然后将实现类注入到​​CacheManager​​中,如下是上报到CAT的示例:

/**
* 多级缓存配置
*
* @author yuhao.wang3
*/
@Configuration
@Import({RedisConfig.class})
@EnableAspectJAutoProxy
public class CacheConfig {

@Bean
public CacheManager layeringCacheManager(RedisClient layeringCacheRedisClient, CacheStatsReportService cacheStatsReportService, LayeringCacheProperties layeringCacheProperties) {

LayeringCacheManager layeringCacheManager = new LayeringCacheManager(layeringCacheRedisClient);
// 默认开启统计功能
layeringCacheManager.setStats(layeringCacheProperties.isStats());
// 上报缓存统计信息
layeringCacheManager.setCacheStatsReportService(cacheStatsReportService);
// 设置缓存命名空间
GlobalConfig.setNamespace(StringUtils.isBlank(layeringCacheProperties.getNamespace()) ? applicationName : layeringCacheProperties.getNamespace());
return layeringCacheManager;
}

// 上报CAT示例
@Bean
public CacheStatsReportService cacheStatsReportService() {
return cacheStatsInfos -> {
for (CacheStatsInfo cacheStatsInfo : cacheStatsInfos) {

Transaction transaction = Cat.newTransaction("layering-cache", cacheStatsInfo.getCacheName());

try {
// 记录一个事件
Cat.logEvent("layering-cache", cacheStatsInfo.getCacheName(), Message.SUCCESS, JSON.toJSONString(cacheStatsInfo));
// 记录一个业务指标
String name = StringUtils.isBlank(cacheStatsInfo.getDepict()) ? cacheStatsInfo.getCacheName() : cacheStatsInfo.getDepict();
Cat.logMetricForSum(name + "-hitRate", cacheStatsInfo.getHitRate());
Cat.logMetricForCount(name + "-firstSize", (int) cacheStatsInfo.getFirstCacheSize());
Cat.logMetricForCount(name + "-requestCount", (int) cacheStatsInfo.getRequestCount());

transaction.setDurationInMillis(cacheStatsInfo.getTotalLoadTime());
transaction.setStatus(Message.SUCCESS);
} catch (Exception e) {
transaction.setStatus(e);
} finally {
transaction.complete();
}
}
};
}
}

监控页面

直接单独部署​​layering-cache-web​​服务即可。

日志格式:

Layering Cache 统计信息:{"cacheName":"people1","depict":"查询用户信息1","firstCacheMissCount":3,"firstCacheRequestCount":4575,"hitRate":99.9344262295082,"internalKey":"4000-15000-8000","layeringCacheSetting":{"depict":"查询用户信息1","firstCacheSetting":{"allowNullValues":true,"expireMode":"WRITE","expireTime":4,"initialCapacity":10,"maximumSize":5000,"timeUnit":"SECONDS"},"internalKey":"4000-15000-8000","secondaryCacheSetting":{"allowNullValues":true,"expiration":15,"forceRefresh":true,"preloadTime":8,"timeUnit":"SECONDS","usePrefix":true},"useFirstCache":true},"missCount":3,"requestCount":4575,"secondCacheMissCount":3,"secondCacheRequestCount":100,"totalLoadTime":142}
  • 如果项目集成了ELK之类的日志框架,那我们可以直接基于以上日志做监控和告警。
  • 统计数据每隔一分钟采集一次
  1. 添加redis配置(需要查看缓存统计的redis服务器)

为监控而生的多级缓存框架 layering-cache_注解式多级缓存

  1. 查询缓存命中率

为监控而生的多级缓存框架 layering-cache_redis_02

  1. 缓存详细

为监控而生的多级缓存框架 layering-cache_缓存_03

实现原理

总体架构

​layering-cache​​总体架构分为两层,第一层是本地缓存L1,第二层是集中式缓存L2,如下图:

为监控而生的多级缓存框架 layering-cache_缓存_04

缓存的选择

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率(​​Caffeine 缓存详解​​)。优点数据就在应用内存所以速度快。缺点受应用内存的限制,所以容量有限;没有持久化,重启服务后缓存数据会丢失;在分布式环境下缓存数据数据无法同步;
  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。优点支持多种数据类型,扩容方便;有持久化,重启应用服务器缓存数据不会丢失;他是一个集中式缓存,不存在在应用服务器之间同步数据的问题。缺点每次都需要访问redis存在IO浪费的情况。

我们可以发现Caffeine和Redis的优缺点正好相反,所以他们可以有效的互补。

数据读取

![image.png](https://img-blog.csdnimg.cn/img_convert/c9e1055c8218035ee9d4611ebd506693.png

为监控而生的多级缓存框架 layering-cache_caffeine_05

  1. 数据读取会先读L1,当L1未命中会获取本地锁;
  2. 获取到本地锁过后去读L2,如果L2未命中,则获取redis分布式锁;
  3. 获取到分布式锁过后去读DB,然后将数据放到L1和L2中。
  4. 获取到本地锁过后去读L2,如果L2命中,则将数据放入到L1中,并判断是否需要刷新缓存;

数据删除/更新

缓存的数据更新需要保证多机器下一级缓存和二级缓存的数据一致性。保证多机数据一致性的方式一般有两种,一种是推模式,这种方式实时性好,但是推的消息有可能会丢;另一种是拉模式,但是这种方式,实时性不好。

layering-cache结合了推和拉两种模式来保证多机数据的一致性。推主要是基于redis的pub/sub机制,拉主要是基于消息偏移量的方式,架构如下:

为监控而生的多级缓存框架 layering-cache_缓存_06

可用性设计

为监控而生的多级缓存框架 layering-cache_多级缓存_07

借助redis的list结构维护一个删除缓存的消息队列,所有应用服务器内存中保存一个偏移量(​​offset​​​)。​​offset​​表示该服务处理缓存消息的位置,每次处理消息后就更新offset的位置,这样就能保证消息不会丢失。最后在每天凌晨3点会去清空这个消息队列。

pub/sub断线重连设计

​layering-cache​​​会记录两个参数:最后一次处理推消息的时间A和最后一次处理拉消息的时间B。如如果​​B - A >= 10s​​则认为断线,然后发起重连尝试。

为监控而生的多级缓存框架 layering-cache_多级缓存_08

推模式数据同步

为监控而生的多级缓存框架 layering-cache_redis_09

在数据删除或更新时,首先更新DB,保证DB数据的准确性;再更新或删除redis缓存,然后向redis推送一条消息,并将这条消息保存到redis的消息队列中;最后再发送一条​​pub/sub​​​消息。应用服务器收到​​pub/sub​​​消息后,将会根据本地​​offset​​去redis消息队列中拉取需要处理的消息,然后根据拉取到的消息删除本地缓存。这里允许消息的重复消费,因为本地缓存即使删除,也会根据二级缓存重建。

基于redis pub/sub 实现一级缓存的更新同步。主要原因有两点:

  1. 使用缓存本来就允许脏读,所以有一定的延迟是允许的 。
  2. redis本身是一个高可用的数据库,并且删除动作不是一个非常频繁的动作所以使用redis原生的发布订阅在性能上是没有问题的。

拉模式数据同步

为监控而生的多级缓存框架 layering-cache_注解式多级缓存_10

这里分几种情况:

  1. 服务刚启动的时候,需要同步最新偏移量(offset)到本地。
  2. 每隔30秒会检查一下本地偏移量和远程偏移量是否一致,以此来解决redis ​​pub/sub​​消息丢失或者断线问题。
  3. 每天凌晨3点会执行一个定时任务来清空消息队列。

Cache和CacheManager接口

该框架最核心的接口有两个,一个是Cache接口:主要负责具体的缓存操作,如对缓存的增删改查;一个是CacheManager接口:主要负责对Cache的管理,最常用的方法是通过缓存名称获取对应的Cache。

Cache接口:

public interface Cache {

String getName();

Object getNativeCache();

Object get(Object key);

<T> T get(Object key, Class<T> type);

<T> T get(Object key, Callable<T> valueLoader);

void put(Object key, Object value);

Object putIfAbsent(Object key, Object value);

void evict(Object key);

void clear();

CacheStats getCacheStats();
}

CacheManager接口:

public interface CacheManager {

Collection<Cache> getCache(String name);

Cache getCache(String name, LayeringCacheSetting layeringCacheSetting);

Collection<String> getCacheNames();

List<CacheStatsInfo> listCacheStats(String cacheName);

void resetCacheStat();
}

在CacheManager里面Cache容器默认使用ConcurrentMap<String, ConcurrentMap<String, Cache>> 数据结构,以此来满足同一个缓存名称可以支持不同的缓存过期时间配置。外层key就是缓存名称,内层key是"一级缓存有效时间-二级缓存有效时间-二级缓存自动刷新时间"缓存时间全部转换成毫秒值,如"1111-2222-3333"。

缓存的监控和统计

简单思路就是缓存的命中和未命中使用LongAdder先暂存到内存,在通过定时任务同步到redis,并重置LongAdder,集中计算缓存的命中率等。监控统计API直接获取redis中的统计数据做展示分析。

因为可能是集群环境,为了保证数据准确性在同步数据到redis的时候需要加一个分布式锁。

重要提示

  • layering-cache支持同一个缓存名称设置不同的过期时间,但是一定要保证key唯一,否则会出现缓存过期时间错乱的情况
  • 删除缓存的时候会将同一个缓存名称的不同的过期时间的缓存都删掉
  • 在集成layering-cache之前还需要添加以下的依赖,主要是为了减少jar包冲突。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.18.RELEASE</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>

<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo-shaded</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>