为监控而生的多级缓存框架 layering-cache
简介
layering-cache是在Spring Cache基础上扩展而来的一个缓存框架,主要目的是在使用注解的时候支持配置过期时间。layering-cache其实是一个两级缓存,一级缓存使用Caffeine作为本地缓存,二级缓存使用redis作为集中式缓存。并且基于redis的Pub/Sub做缓存的删除,所以它是一个适用于分布式环境下的一个缓存系统。
支持
- 支持缓存监控统计
- 支持缓存过期时间在注解上直接配置
- 支持二级缓存的自动刷新(当缓存命中并发现缓存将要过期时会开启一个异步线程刷新缓存)
- 刷新缓存分为强刷新和软刷新,强刷新直接调用缓存方法,软刷新直接改缓存的时间
- 缓存Key支持SpEL表达式
- 新增FastJsonRedisSerializer,KryoRedisSerializer序列化,重写String序列化。
- 输出INFO级别的监控统计日志
- 二级缓存是否允许缓存NULL值支持配置
- 二级缓存空值允许配置时间倍率
集成
集成 Spring 4.x
- 引入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}'
-
声明RedisTemplate 声明RedisTemplate
-
声明CacheManager和LayeringAspect
/**
* 多级缓存配置
*
* @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 就可以了
<dependency>
<groupId>com.github.xiaolyuh</groupId>
<artifactId>layering-cache-starter</artifactId>
<version>${layering.version}</version>
</dependency>
使用
注解形式
直接在需要缓存的方法上加上Cacheable、CacheEvict、CachePut注解。
- 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
表示用的方法的结果是可以被缓存的,当该方法被调用时先检查缓存是否命中,如果没有命中再调用被缓存的方法,并将其返回值放到缓存中。
名称 | 默认值 | 说明 |
---|---|---|
value | 空字符串数组 | 缓存名称,cacheNames的别名 |
cacheNames | 空字符串数组 | 缓存名称 |
key | 空字符串 | 缓存key,支持SpEL表达式 |
depict | 空字符串 | 缓存描述(在缓存统计页面会用到) |
ignoreException | 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 | 是否强制刷新(直接执行被缓存方法) |
isAllowNullValue | false | 是否允许缓存NULL值 |
magnification | 1 | 非空值和null值之间的时间倍率,默认是1。isAllowNullValue=true才有效 |
@CachePut
将数据放到缓存中
名称 | 默认值 | 说明 |
---|---|---|
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的优缺点正好相反,所以他们可以有效的互补。
数据读取流程
数据删除流程
缓存更新同步
基于redis pub/sub 实现一级缓存的更新同步。主要原因有两点:
- 使用缓存本来就允许脏读,所以有一定的延迟是允许的 。
- 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

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
白话SpringCloud | 第九章:路由网关(Zuul)的使用
前言 介绍完分布式配置中心,结合前面的文章。我们已经有了一个微服务的框架了,可以对外提供api接口服务了。但现在试想一下,在微服务框架中,每个对外服务都是独立部署的,对外的api或者服务地址都不是不尽相同的。对于内部而言,很简单,通过注册中心自动感知即可。但我们大部分情况下,服务都是提供给外部系统进行调用的,不可能同享一个注册中心。同时一般上内部的微服务都是在内网的,和外界是不连通的。而且,就算我们每个微服务对外开放,对于调用者而言,调用不同的服务的地址或者参数也是不尽相同的,这样就会造成消费者客户端的复杂性,同时想想,可能微服务可能是不同的技术栈实现的,有的是http、rpc或者websocket等等,也会进一步加大客户端的调用难度。所以,一般上都有会有个api网关,根据请求的url不同,路由到不同的服务上去,同时入口统一了,还能进行统一的身份鉴权、日志记录、分流等操作。接下来,我们就来了解今天要讲解的路由服务:zuul。 一点知识 为什么要使用微服务网关 简单来说,微服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均...
-
下一篇
golang 使用gorequest 遇到的序列化的坑
golang 序列化的库没其他语言那么好用,然后就掉坑里了。 是用gorequest 库发现的,这个高start 的库,还是有些问题。我使用场景是这样的: package main import ( "fmt" "github.com/parnurzeal/gorequest" ) type InStruct struct { Param float64 } func main() { postData := InStruct{Param: int64(72057594088097496)} resp, bodyBytes, errs := gorequest.New().Get("http://127.0.0.1:8080").Query(postData).EndBytes() fmt.Println(resp, bodyBytes, errs) } 然后,我发送的数据变成了:Param=72057594088097500。相当于是数据被四舍五入了,一番追查,发现底层做了这个操作,把数据给变了: func (s *SuperAgent)...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- MySQL数据库在高并发下的优化方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境