爲監控而生的多級緩存框架 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

相關文章
相關標籤/搜索