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

简介

layering-cache是在Spring Cache基础上扩展而来的一个缓存框架,主要目的是在使用注解的时候支持配置过时时间。layering-cache实际上是一个两级缓存,一级缓存使用Caffeine做为本地缓存,二级缓存使用redis做为集中式缓存。而且基于redis的Pub/Sub作缓存的删除,因此它是一个适用于分布式环境下的一个缓存系统。html

支持

  • 支持缓存监控统计
  • 支持缓存过时时间在注解上直接配置
  • 支持二级缓存的自动刷新(当缓存命中并发现缓存将要过时时会开启一个异步线程刷新缓存)
  • 刷新缓存分为强刷新和软刷新,强刷新直接调用缓存方法,软刷新直接改缓存的时间
  • 缓存Key支持SpEL表达式
  • 新增FastJsonRedisSerializer,KryoRedisSerializer序列化,重写String序列化。
  • 输出INFO级别的监控统计日志
  • 二级缓存是否容许缓存NULL值支持配置
  • 二级缓存空值容许配置时间倍率

集成

集成 Spring 4.x

  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. 声明RedisTemplate 声明RedisTemplatejava

  2. 声明CacheManager和LayeringAspectgit

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

    @Bean
    public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        return new LayeringCacheManager(redisTemplate);
    }

    @Bean
    public LayeringAspect layeringAspect() {
        return new LayeringAspect();
    }
}

集成 Spring Boot

引入layering-cache 就能够了github

<dependency>
    <groupId>com.github.xiaolyuh</groupId>
    <artifactId>layering-cache-starter</artifactId>
    <version>${layering.version}</version>
</dependency>

使用

注解形式

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

  • Cacheable注解
@Cacheable(value = "user:info", depict = "用户信息缓存",
		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 = "用户信息缓存",
		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

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

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
depict 空字符串 缓存描述(在缓存统计页面会用到)
ignoreException true 是否忽略在操做缓存中遇到的异常,如反序列化异常
firstCache 一级缓存配置
secondaryCache 二级缓存配置

@FirstCache

一级缓存配置项spring

名称 默认值 说明
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 是否强制刷新(直接执行被缓存方法)
isAllowNullValue false 是否容许缓存NULL值
magnification 1 非空值和null值之间的时间倍率,默认是1。isAllowNullValue=true才有效

@CachePut

将数据放到缓存中json

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
depict 空字符串 缓存描述(在缓存统计页面会用到)
ignoreException true 是否忽略在操做缓存中遇到的异常,如反序列化异常
firstCache 一级缓存配置
secondaryCache 二级缓存配置

@CacheEvict

删除缓存数组

名称 默认值 说明
value 空字符串数组 缓存名称,cacheNames的别名
cacheNames 空字符串数组 缓存名称
key 空字符串 缓存key,支持SpEL表达式
allEntries false 是否删除缓存中全部数据,默认状况下是只删除关联key的缓存数据,当该参数设置成 true 时 key 参数将无效
ignoreException true 是否忽略在操做缓存中遇到的异常,如反序列化异常

打开监控统计功能

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

Spring 4.x

直接在声明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

打开内置的监控页面

Layering Cache内置提供了一个LayeringCacheServlet用于展现缓存的统计信息。

这个LayeringCacheServlet的用途包括:

  • 提供监控信息展现的html页面
  • 提供监控信息的JSON API

日志格式:

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之类的日志框架,那咱们能够直接基于以上日志作监控和告警。
  • 统计数据每隔一分钟采集一次

配置 web.xml

配置Servlet

LayeringCacheServlet是一个标准的javax.servlet.http.HttpServlet,须要配置在你web应用中的WEB-INF/web.xml中。

<servlet>
    <servlet-name>layeringcachestatview</servlet-name>
    <servlet-class>com.github.xiaolyuh.tool.servlet.layeringcacheservlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>layeringcachestatview</servlet-name>
    <url-pattern>/layering-cache/*</url-pattern>
</servlet-mapping>

根据配置中的url-pattern来访问内置监控页面,若是是上面的配置,内置监控页面的首页是/layering-cache/index.html。例如: http://localhost:8080/layering-cache/index.html http://localhost:8080/xxx/layering-cache/index.html

配置监控页面访问密码

须要配置Servlet的 loginUsername 和 loginPassword这两个初始参数。 示例以下:

<!-- 配置监控信息显示页面 -->
<servlet>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>
    <init-param>
        <!-- 用户名 -->
        <param-name>loginUsername</param-name>
        <param-value>admin</param-value>
    </init-param>
    <init-param>
        <!-- 密码 -->
        <param-name>loginPassword</param-name>
        <param-value>admin</param-value>
    </init-param>

</servlet>
<servlet-mapping>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <url-pattern>/layering-cache/*</url-pattern>
</servlet-mapping>

配置黑白名单

LayeringCacheStatView展现出来的监控信息比较敏感,是系统运行的内部状况,若是你须要作访问控制,能够配置allow和deny这两个参数。好比:

<servlet>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>
    <!--配置白名单-->
    <init-param>
        <param-name>allow</param-name>
        <param-value>128.242.127.1/24,128.242.128.1</param-value>
    </init-param>
    <!--配置黑名单-->
    <init-param>
        <param-name>deny</param-name>
        <param-value>128.242.127.4</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <url-pattern>/layering-cache/*</url-pattern>
</servlet-mapping>

判断规则

  • deny优先于allow,若是在deny列表中,就算在allow列表中,也会被拒绝。
  • 若是allow没有配置或者为空,则容许全部访问

ip配置规则 配置的格式

128.242.127.1,128.242.127.1/24

/24表示,前面24位是子网掩码,比对的时候,前面24位相同就匹配。

不支持IPV6 因为匹配规则不支持IPV6,配置了allow或者deny以后,会致使IPV6没法访问。

关闭更新数据权限

须要配置Servlet的 enableUpdate参数。若是设置成false,那么将不能重置统计数据和删除缓存。 示例以下:

<!-- 配置监控信息显示页面 -->
<servlet>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>
    <init-param>
        <!-- 是否开启更新数据权限 -->
        <param-name>enableUpdate</param-name>
        <param-value>false</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>LayeringCacheStatView</servlet-name>
    <url-pattern>/layering-cache/*</url-pattern>
</servlet-mapping>

Spring Boot

#是否开启缓存统计默认值true
spring.layering-cache.stats=true

#是否启用LayeringCacheServlet默认值true
spring.layering-cache.layering-cache-servlet-enabled=true
spring.layering-cache.url-pattern=/layering-cache/*
#用户名
spring.layering-cache.login-username=admin
#密码
spring.layering-cache.login-password=admin
#是否容许更新数据
spring.layering-cache.enable-update=true
# IP白名单(没有配置或者为空,则容许全部访问)
spring.layering-cache.allow=127.0.0.1,192.168.163.1/24
# IP黑名单 (存在共同时,deny优先于allow)
spring.layering-cache.deny=192.168.1.73

实现原理

缓存的选择

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

咱们能够发现Caffeine和Redis的优缺点正好相反,因此他们能够有效的互补。

数据读取流程

数据读取流程.jpg

数据删除流程

数据删除流程.jpg

缓存更新同步

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

  1. 使用缓存原本就容许脏读,因此有必定的延迟是容许的 。
  2. redis自己是一个高可用的数据库,而且删除动做不是一个很是频繁的动做因此使用redis原生的发布订阅在性能上是没有问题的。

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,并重置LongAdde,集中计算缓存的命中率等。监控统计API直接获取redis中的统计数据作展现分析。

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

重要提示

  • layering-cache支持同一个缓存名称设置不一样的过时时间,可是必定要保证key惟一,不然会出现缓存过时时间错乱的状况
  • 删除缓存的时候会将同一个缓存名称的不一样的过时时间的缓存都删掉
  • 在集成layering-cache以前还须要添加如下的依赖,主要是为了减小jar包冲突。
<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-redis</artifactId>
	<version>1.8.3.RELEASE</version>
</dependency>

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>

<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>

做者信息

做者博客:https://www.jianshu.com/u/4e6e80b98daa

做者邮箱: xiaolyuh@163.com

github 地址:https://github.com/wyh-chenfeng/layering-cache

相关文章
相关标签/搜索