暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

构建一个二级缓存,给redis减减负!

小罗技术笔记 2020-04-12
1313

点击上方“小罗技术笔记”,关注公众号

第一时间送达实用干货

作者:xiaolyuh 

来源:http://suo.im/5P0vau

简介

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

支持

  • 支持缓存监控统计

  • 支持缓存过期时间在注解上直接配置

  • 支持二级缓存的自动刷新(当缓存命中并发现缓存将要过期时会开启一个异步线程刷新缓存)

  • 刷新缓存分为强刷新和软刷新,强刷新直接调用缓存方法,软刷新直接改缓存的时间

  • 缓存Key支持SpEL表达式

  • 新增FastJsonRedisSerializer,KryoRedisSerializer序列化,重写String序列化。

  • 输出INFO级别的监控统计日志

  • 二级缓存是否允许缓存NULL值支持配置

  • 二级缓存空值允许配置时间倍率

集成

集成 Spring 4.x

1、 引入layering-cache

  • maven 方式

  1. <dependency>

  2.    <groupId>com.github.xiaolyuh</groupId>

  3.    <artifactId>layering-cache-aspectj</artifactId>

  4.    <version>${layering.version}</version>

  5. </dependency>

  • gradle 方式

  1. compile 'com.github.xiaolyuh:layering-cache:${layering.version}'

1、 声明RedisTemplate 2、 声明CacheManager和LayeringAspect

  1. /**

  2. * 多级缓存配置

  3. *

  4. * @author yuhao.wang3

  5. */

  6. @Configuration

  7. @EnableAspectJAutoProxy

  8. public class CacheConfig {

  9.    @Bean

  10.    public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {

  11.        return new LayeringCacheManager(redisTemplate);

  12.    }

  13.    @Bean

  14.    public LayeringAspect layeringAspect() {

  15.        return new LayeringAspect();

  16.    }

  17. }

集成 Spring Boot

引入layering-cache 就可以了

  1. <dependency>

  2.    <groupId>com.github.xiaolyuh</groupId>

  3.    <artifactId>layering-cache-starter</artifactId>

  4.    <version>${layering.version}</version>

  5. </dependency>

使用

注解形式

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

  • Cacheable注解

  1. @Cacheable(value = "user:info", depict = "用户信息缓存",

  2.        firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS),

  3.        secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))

  4. public User getUser(User user) {

  5.    logger.debug("调用方法获取用户名称");

  6.    return user;

  7. }

  • CachePut注解

  1. @CachePut(value = "user:info", key = "#userId", depict = "用户信息缓存",

  2.        firstCache = @FirstCache(expireTime = 4, timeUnit = TimeUnit.SECONDS),

  3.        secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 3, forceRefresh = true, timeUnit = TimeUnit.SECONDS))

  4. public User putUser(long userId) {

  5.    User user = new User();

  6.    user.setUserId(userId);

  7.    user.setAge(31);

  8.    user.setLastName(new String[]{"w", "y", "h"});

  9.    return user;

  10. }

  • CacheEvict注解

  1. @CacheEvict(value = "user:info", key = "#userId")

  2. public void evictUser(long userId) {

  3. }

  4. @CacheEvict(value = "user:info", allEntries = true)

  5. public void evictAllUser() {

  6. }

直接使用API

  1. @RunWith(SpringJUnit4ClassRunner.class)

  2. @ContextConfiguration(classes = {CacheConfig.class})

  3. public class CacheCoreTest {

  4.    private Logger logger = LoggerFactory.getLogger(CacheCoreTest.class);

  5.    @Autowired

  6.    private CacheManager cacheManager;

  7.    @Test

  8.    public void testCacheExpiration() {

  9.        FirstCacheSetting firstCacheSetting = new FirstCacheSetting(10, 1000, 4, TimeUnit.SECONDS, ExpireMode.WRITE);

  10.        SecondaryCacheSetting secondaryCacheSetting = new SecondaryCacheSetting(10, 4, TimeUnit.SECONDS, true);

  11.        LayeringCacheSetting layeringCacheSetting = new LayeringCacheSetting(firstCacheSetting, secondaryCacheSetting);

  12.        String cacheName = "cache:name";

  13.        String cacheKey = "cache:key1";

  14.        LayeringCache cache = (LayeringCache) cacheManager.getCache(cacheName, layeringCacheSetting);

  15.        cache.get(cacheKey, () -> initCache(String.class));

  16.        cache.put(cacheKey, "test");

  17.        cache.evict(cacheKey);

  18.        cache.clear();

  19.    }

  20.    private <T> T initCache(Class<T> t) {

  21.        logger.debug("加载缓存");

  22.        return (T) "test";

  23.    }

  24. }

文档

@Cacheable

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

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

@FirstCache

一级缓存配置项

名称默认值说明
initialCapacity10缓存初始Size
maximumSize5000缓存最大Size
expireTime9缓存有效时间
timeUnitTimeUnit.MINUTES时间单位,默认分钟
expireModeExpireMode.WRITE缓存失效模式,ExpireMode.WRITE:最后一次写入后到期失效,ExpireMode.ACCESS:最后一次访问后到期失效

@SecondaryCache

二级缓存配置项

名称默认值说明
expireTime5缓存有效时间
preloadTime1缓存主动在失效前强制刷新缓存的时间,建议是 expireTime * 0.2
timeUnitTimeUnit.HOURS时间单位,默认小时
forceRefreshfalse是否强制刷新(直接执行被缓存方法)
isAllowNullValuefalse是否允许缓存NULL值
magnification1非空值和null值之间的时间倍率,默认是1。isAllowNullValue=true才有效

@CachePut

将数据放到缓存中

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

@CacheEvict

删除缓存

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

打开监控统计功能

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

Spring 4.x

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

  1. /**

  2. * 多级缓存配置

  3. *

  4. * @author yuhao.wang3

  5. */

  6. @Configuration

  7. @EnableAspectJAutoProxy

  8. public class CacheConfig {

  9.    @Bean

  10.    public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {

  11.         LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);

  12.        // 默认开启统计功能

  13.        layeringCacheManager.setStats(true);

  14.        return layeringCacheManager;

  15.    }

  16.   ...

  17. }

Spring Boot

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

  1. layering-cache.stats=true

打开内置的监控页面

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

这个LayeringCacheServlet的用途包括:

  • 提供监控信息展示的html页面

  • 提供监控信息的JSON API

日志格式:

  1. 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中。

  1. <servlet>

  2.    <servlet-name>layeringcachestatview</servlet-name>

  3.    <servlet-class>com.github.xiaolyuh.tool.servlet.layeringcacheservlet</servlet-class>

  4. </servlet>

  5. <servlet-mapping>

  6.    <servlet-name>layeringcachestatview</servlet-name>

  7.    <url-pattern>/layering-cache/*</url-pattern>

  8. </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这两个初始参数。示例如下:

  1. <servlet>

  2.    <servlet-name>LayeringCacheStatView</servlet-name>

  3.    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>

  4.    <init-param>

  5.        <param-name>loginUsername</param-name>

  6.        <param-value>admin</param-value>

  7.    </init-param>

  8.    <init-param>

  9.        <param-name>loginPassword</param-name>

  10.        <param-value>admin</param-value>

  11.    </init-param>

  12. </servlet>

  13. <servlet-mapping>

  14.    <servlet-name>LayeringCacheStatView</servlet-name>

  15.    <url-pattern>/layering-cache/*</url-pattern>

  16. </servlet-mapping>

配置黑白名单

LayeringCacheStatView展示出来的监控信息比较敏感,是系统运行的内部情况,如果你需要做访问控制,可以配置allow和deny这两个参数。比如:

  1. <servlet>

  2.    <servlet-name>LayeringCacheStatView</servlet-name>

  3.    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>

  4.    <init-param>

  5.        <param-name>allow</param-name>

  6.        <param-value>128.242.127.1/24,128.242.128.1</param-value>

  7.    </init-param>

  8.    <init-param>

  9.        <param-name>deny</param-name>

  10.        <param-value>128.242.127.4</param-value>

  11.    </init-param>

  12. </servlet>

  13. <servlet-mapping>

  14.    <servlet-name>LayeringCacheStatView</servlet-name>

  15.    <url-pattern>/layering-cache/*</url-pattern>

  16. </servlet-mapping>

判断规则

  • deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝。

  • 如果allow没有配置或者为空,则允许所有访问

ip配置规则 配置的格式

  1.   128.242.127.1,128.242.127.1/24

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

不支持IPV6 由于匹配规则不支持IPV6,配置了allow或者deny之后,会导致IPV6无法访问。

关闭更新数据权限

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

  1. <servlet>

  2.    <servlet-name>LayeringCacheStatView</servlet-name>

  3.    <servlet-class>com.github.xiaolyuh.tool.servlet.LayeringCacheServlet</servlet-class>

  4.    <init-param>

  5.        <param-name>enableUpdate</param-name>

  6.        <param-value>false</param-value>

  7.    </init-param>

  8. </servlet>

  9. <servlet-mapping>

  10.    <servlet-name>LayeringCacheStatView</servlet-name>

  11.    <url-pattern>/layering-cache/*</url-pattern>

  12. </servlet-mapping>

Spring Boot

  1. #是否开启缓存统计默认值true

  2. spring.layering-cache.stats=true

  3. #是否启用LayeringCacheServlet默认值true

  4. spring.layering-cache.layering-cache-servlet-enabled=true

  5. spring.layering-cache.url-pattern=/layering-cache/*

  6. #用户名

  7. spring.layering-cache.login-username=admin

  8. #密码

  9. spring.layering-cache.login-password=admin

  10. #是否允许更新数据

  11. spring.layering-cache.enable-update=true

  12. # IP白名单(没有配置或者为空,则允许所有访问)

  13. spring.layering-cache.allow=127.0.0.1,192.168.163.1/24

  14. # IP黑名单 (存在共同时,deny优先于allow)

  15. spring.layering-cache.deny=192.168.1.73

实现原理

缓存的选择

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。优点数据就在应用内存所以速度快。缺点受应用内存的限制,所以容量有限;没有持久化,重启服务后缓存数据会丢失;在分布式环境下缓存数据数据无法同步;

  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。优点支持多种数据类型,扩容方便;有持久化,重启应用服务器缓存数据不会丢失;他是一个集中式缓存,不存在在应用服务器之间同步数据的问题。缺点每次都需要访问redis存在IO浪费的情况。

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

数据读取流程

数据删除流程

缓存更新同步

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

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

Cache和CacheManager接口

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

Cache接口:

  1. public interface Cache {

  2.    String getName();

  3.    Object getNativeCache();

  4.    Object get(Object key);

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

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

  7.    void put(Object key, Object value);

  8.    Object putIfAbsent(Object key, Object value);

  9.    void evict(Object key);

  10.    void clear();

  11.    CacheStats getCacheStats();

  12. }

CacheManager接口:

  1. public interface CacheManager {

  2.    Collection<Cache> getCache(String name);

  3.    Cache getCache(String name, LayeringCacheSetting layeringCacheSetting);

  4.    Collection<String> getCacheNames();

  5.    List<CacheStatsInfo> listCacheStats(String cacheName);

  6.    void resetCacheStat();

  7. }

在CacheManager里面Cache容器默认使用ConcurrentMap<string, concurrentmap

缓存的监控和统计

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

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

重要提示

  • layering-cache支持同一个缓存名称设置不同的过期时间,但是一定要保证key唯一,否则会出现缓存过期时间错乱的情况

  • 删除缓存的时候会将同一个缓存名称的不同的过期时间的缓存都删掉

  • 在集成layering-cache之前还需要添加以下的依赖,主要是为了减少jar包冲突。

  1. <dependency>

  2.    <groupId>org.springframework.data</groupId>

  3.    <artifactId>spring-data-redis</artifactId>

  4.    <version>1.8.3.RELEASE</version>

  5. </dependency>

  6. <dependency>

  7.    <groupId>redis.clients</groupId>

  8.    <artifactId>jedis</artifactId>

  9.    <version>2.9.0</version>

  10. </dependency>

  11. <dependency>

  12.    <groupId>org.springframework</groupId>

  13.    <artifactId>spring-core</artifactId>

  14.    <version>4.3.18.RELEASE</version>

  15. </dependency>

  16. <dependency>

  17.    <groupId>org.springframework</groupId>

  18.    <artifactId>spring-aop</artifactId>

  19.    <version>4.3.18.RELEASE</version>

  20. </dependency>

  21. <dependency>

  22.    <groupId>org.springframework</groupId>

  23.    <artifactId>spring-context</artifactId>

  24.    <version>4.3.18.RELEASE</version>

  25. </dependency>

  26. <dependency>

  27.    <groupId>com.alibaba</groupId>

  28.    <artifactId>fastjson</artifactId>

  29.    <version>1.2.31</version>

  30. </dependency>

  31. <dependency>

  32.    <groupId>com.esotericsoftware</groupId>

  33.    <artifactId>kryo-shaded</artifactId>

  34.    <version>3.0.3</version>

  35. </dependency>

  36. <dependency>

  37.    <groupId>org.aspectj</groupId>

  38.    <artifactId>aspectjweaver</artifactId>

  39.    <version>1.8.10</version>

  40. </dependency>



长按二维码关注

点个在看再走呗!

文章转载自小罗技术笔记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论