萬字長文聊緩存(下)- 應(yīng)用級緩存

深入解析SpringMVC核心原理:從手寫簡易版MVC框架開始(SmartMvc) : https://github.com/silently9527/SmartMvc

IDEA多線程文件下載插件: https://github.com/silently9527/FastDownloadIdeaPlugin

公眾號:貝塔學(xué)JAVA

摘要

在上一篇文章 萬字長文聊緩存(上)中雪隧,我們主要如何圍繞著Http做緩存優(yōu)化裁替,在后端服務(wù)器的應(yīng)用層同樣有很多地方可以做緩存,提高服務(wù)的效率;本篇我們就來繼續(xù)聊聊應(yīng)用級的緩存。

緩存的命中率

緩存的命中率是指從緩存中獲取到數(shù)據(jù)的次數(shù)和總讀取次數(shù)的比率,命中率越高證明緩存的效果越好。這是一個很重要的指標,應(yīng)該通過監(jiān)控這個指標來判斷我們的緩存是否設(shè)置的合理辜御。

緩存的回收策略

基于時間

  • 存活期:在設(shè)置緩存的同時設(shè)置該緩存可以存活多久,不論在存活期內(nèi)被訪問了多少次屈张,時間到了都會過期
  • 空閑期:是指緩存的數(shù)據(jù)多久沒有被訪問就過期

基于空間

設(shè)置緩存的存儲空間擒权,比如:設(shè)置緩存的空間是 1G,當(dāng)達到了1G之后就會按照一定的策略將部分數(shù)據(jù)移除

基于緩存數(shù)量

設(shè)置緩存的最大條目數(shù)阁谆,當(dāng)達到了設(shè)置的最大條目數(shù)之后按照一定的策略將舊的數(shù)據(jù)移除

基于Java對象引用

  • 弱引用:當(dāng)垃圾回收器開始回收內(nèi)存的時候碳抄,如果發(fā)現(xiàn)了弱引用,它將立即被回收场绿。
  • 軟引用:當(dāng)垃圾回收器發(fā)現(xiàn)內(nèi)存已不足的情況下會回收軟引用的對象剖效,從而騰出一下空間,防止發(fā)生內(nèi)存溢出。軟引用適合用來做堆緩存

緩存的回收算法

  • FIFO 先進先出算法
  • LRU 最近最少使用算法
  • LFU 最不常用算法

Java緩存的類型

堆緩存

堆緩存是指把數(shù)據(jù)緩存在JVM的堆內(nèi)存中璧尸,使用堆緩存的好處是沒有序列化和反序列化的操作咒林,是最快的緩存。如果緩存的數(shù)據(jù)量很大爷光,為了避免造成OOM通常情況下使用的時軟引用來存儲緩存對象垫竞;堆緩存的缺點是緩存的空間有限,并且垃圾回收器暫停的時間會變長蛀序。

Gauva Cache實現(xiàn)堆緩存

Cache<String, String> cache = CacheBuilder.newBuilder()
                .build();

通過CacheBuilder構(gòu)建緩存對象

Gauva Cache的主要配置和方法

  • put : 向緩存中設(shè)置key-value
  • V get(K key, Callable<? extends V> loader) : 獲取一個緩存值欢瞪,如果緩存中沒有徐裸,那么就調(diào)用loader獲取一個然后放入到緩存
  • expireAfterWrite : 設(shè)置緩存的存活期,寫入數(shù)據(jù)后指定時間之后失效
  • expireAfterAccess : 設(shè)置緩存的空閑期倦逐,在給定的時間內(nèi)沒有被訪問就會被回收
  • maximumSize : 設(shè)置緩存的最大條目數(shù)
  • weakKeys/weakValues : 設(shè)置弱引用緩存
  • softValues : 設(shè)置軟引用緩存
  • invalidate/invalidateAll: 主動失效指定key的緩存數(shù)據(jù)
  • recordStats : 啟動記錄統(tǒng)計信息,可以查看到命中率
  • removalListener : 當(dāng)緩存被刪除的時候會調(diào)用此監(jiān)聽器粉怕,可以用于查看為什么緩存會被刪除

Caffeine實現(xiàn)堆緩存

Caffeine是使用Java8對Guava緩存的重寫版本稚晚,高性能Java本地緩存組件赏廓,也是Spring推薦的堆緩存的實現(xiàn),與spring的集成可以查看文檔https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration-caffeine尿贫。

由于是對Guava緩存的重寫版本电媳,所以很多的配置參數(shù)都是和Guava緩存一致:

  • initialCapacity: 初始的緩存空間大小
  • maximumSize: 緩存的最大條數(shù)
  • maximumWeight: 緩存的最大權(quán)重
  • expireAfterAccess: 最后一次寫入或訪問后經(jīng)過固定時間過期
  • expireAfterWrite: 最后一次寫入后經(jīng)過固定時間過期
  • expireAfter : 自定義過期策略
  • refreshAfterWrite: 創(chuàng)建緩存或者最近一次更新緩存后經(jīng)過固定的時間間隔,刷新緩存
  • weakKeys: 打開key的弱引用
  • weakValues:打開value的弱引用
  • softValues:打開value的軟引用
  • recordStats:開啟統(tǒng)計功能

Caffeine的官方文檔:https://github.com/ben-manes/caffeine/wiki

  1. pom.xml中添加依賴
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.8.4</version>
</dependency>
  1. Caffeine Cache提供了三種緩存填充策略:手動庆亡、同步加載和異步加載匾乓。
  • 手動加載:在每次get key的時候指定一個同步的函數(shù),如果key不存在就調(diào)用這個函數(shù)生成一個值
public Object manual(String key) {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .expireAfterAccess(1, TimeUnit.SECONDS) //設(shè)置空閑期時長
            .maximumSize(10)
            .build();
    return cache.get(key, t -> setValue(key).apply(key));
}

public Function<String, Object> setValue(String key){
    return t -> "https://silently9527.cn";
}
  • 同步加載:構(gòu)造Cache時候又谋,build方法傳入一個CacheLoader實現(xiàn)類拼缝。實現(xiàn)load方法,通過key加載value彰亥。
public Object sync(String key){
    LoadingCache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.MINUTES) //設(shè)置存活期時長
            .build(k -> setValue(key).apply(key));
    return cache.get(key);
}

public Function<String, Object> setValue(String key){
    return t -> "https://silently9527.cn";
}
  • 異步加載:AsyncLoadingCache是繼承自LoadingCache類的咧七,異步加載使用Executor去調(diào)用方法并返回一個CompletableFuture
public CompletableFuture async(String key) {
    AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(1, TimeUnit.MINUTES)
            .buildAsync(k -> setAsyncValue().get());
    return cache.get(key);
}

public CompletableFuture<Object> setAsyncValue() {
    return CompletableFuture.supplyAsync(() -> "公眾號:貝塔學(xué)JAVA");
}
  1. 監(jiān)聽緩存被清理的事件
public void removeListener() {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .removalListener((String key, Object value, RemovalCause cause) -> {
                System.out.println("remove lisitener");
                System.out.println("remove Key:" + key);
                System.out.println("remove Value:" + value);
            })
            .build();
    cache.put("name", "silently9527");
    cache.invalidate("name");
}
  1. 統(tǒng)計
public void recordStats() {
    Cache<String, Object> cache = Caffeine.newBuilder()
            .maximumSize(10000)
            .recordStats()
            .build();
    cache.put("公眾號", "貝塔學(xué)JAVA");
    cache.get("公眾號", (t) -> "");
    cache.get("name", (t) -> "silently9527");

    CacheStats stats = cache.stats();
    System.out.println(stats);
}

通過 Cache.stats() 獲取到CacheStatsCacheStats提供以下統(tǒng)計方法:

  • hitRate(): 返回緩存命中率
  • evictionCount(): 緩存回收數(shù)量
  • averageLoadPenalty(): 加載新值的平均時間

EhCache實現(xiàn)堆緩存

EhCache 是老牌Java開源緩存框架任斋,早在2003年就已經(jīng)出現(xiàn)了继阻,發(fā)展到現(xiàn)在已經(jīng)非常成熟穩(wěn)定,在Java應(yīng)用領(lǐng)域應(yīng)用也非常廣泛废酷,而且和主流的Java框架比如Srping可以很好集成瘟檩。相比于 Guava Cache,EnCache 支持的功能更豐富澈蟆,包括堆外緩存墨辛、磁盤緩存,當(dāng)然使用起來要更重一些趴俘。使用 Ehcache 的Maven 依賴如下:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.6.3</version>
</dependency>
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);

ResourcePoolsBuilder resource = ResourcePoolsBuilder.heap(10); //設(shè)置最大緩存條目數(shù)

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10)))
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
  • ResourcePoolsBuilder.heap(10)設(shè)置緩存的最大條目數(shù)睹簇,這是簡寫方式,等價于ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
  • ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB)設(shè)置緩存最大的空間10MB
  • withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10))) 設(shè)置緩存空閑時間
  • withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) 設(shè)置緩存存活時間
  • remove/removeAll主動失效緩存寥闪,與Guava Cache類似太惠,調(diào)用方法后不會立即去清除回收,只有在get或者put的時候判斷緩存是否過期
  • withSizeOfMaxObjectSize(10,MemoryUnit.KB)限制單個緩存對象的大小疲憋,超過這兩個限制的對象則不被緩存

堆外緩存

堆外緩存即緩存數(shù)據(jù)在堆外內(nèi)存中垛叨,空間大小只受本機內(nèi)存大小限制,不受GC管理柜某,使用堆外緩存可以減少GC暫停時間嗽元,但是堆外內(nèi)存中的對象都需要序列化和反序列化,KEY和VALUE必須實現(xiàn)Serializable接口喂击,因此速度會比堆內(nèi)緩存慢剂癌。在Java中可以通過 -XX:MaxDirectMemorySize 參數(shù)設(shè)置堆外內(nèi)存的上限

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
// 堆外內(nèi)存不能按照存儲條目限制,只能按照內(nèi)存大小進行限制翰绊,超過限制則回收緩存
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB);

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withDispatcherConcurrency(4)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withSizeOfMaxObjectSize(10, MemoryUnit.KB)
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo2", cacheConfig);
cache.put("website", "https://silently9527.cn");
System.out.println(cache.get("website"));

磁盤緩存

把緩存數(shù)據(jù)存放到磁盤上佩谷,在JVM重啟時緩存的數(shù)據(jù)不會受到影響旁壮,而堆緩存和堆外緩存都會丟失;并且磁盤緩存有更大的存儲空間谐檀;但是緩存在磁盤上的數(shù)據(jù)也需要支持序列化抡谐,速度會被比內(nèi)存更慢,在使用時推薦使用更快的磁盤帶來更大的吞吐率桐猬,比如使用閃存代替機械磁盤麦撵。

CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
        .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache"));

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .with(persistentManagerConfig).build(true);

//disk 第三個參數(shù)設(shè)置為 true 表示將數(shù)據(jù)持久化到磁盤上
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().disk(100, MemoryUnit.MB, true);

CacheConfiguration<String, String> config = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource).build();
Cache<String, String> cache = persistentCacheManager.createCache("userInfo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

cache.put("公眾號", "貝塔學(xué)JAVA");
System.out.println(cache.get("公眾號"));
persistentCacheManager.close();

在JVM停止時,一定要記得調(diào)用persistentCacheManager.close()溃肪,保證內(nèi)存中的數(shù)據(jù)能夠dump到磁盤上免胃。

image

這是典型 heap + offheap + disk 組合的結(jié)構(gòu)圖,上層比下層速度快惫撰,下層比上層存儲空間大羔沙,在ehcache中,空間大小設(shè)置 heap > offheap > disk厨钻,否則會報錯扼雏; ehcache 會將最熱的數(shù)據(jù)保存在高一級的緩存。這種結(jié)構(gòu)的代碼如下:

CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
        .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache"));

PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .with(persistentManagerConfig).build(true);

ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder()
        .heap(10, MemoryUnit.MB)
        .offheap(100, MemoryUnit.MB)
        //第三個參數(shù)設(shè)置為true夯膀,支持持久化
        .disk(500, MemoryUnit.MB, true);

CacheConfiguration<String, String> config = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource).build();

Cache<String, String> cache = persistentCacheManager.createCache("userInfo",
        CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

//寫入緩存
cache.put("name", "silently9527");
// 讀取緩存
System.out.println(cache.get("name"));

// 再程序關(guān)閉前诗充,需要手動釋放資源
persistentCacheManager.close();

分布式集中緩存

前面提到的堆內(nèi)緩存和堆外緩存如果在多個JVM實例的情況下會有兩個問題:1.單機容量畢竟有限;2.多臺JVM實例緩存的數(shù)據(jù)可能不一致棍郎;3.如果緩存數(shù)據(jù)同一時間都失效了,那么請求都會打到數(shù)據(jù)庫上银室,數(shù)據(jù)庫壓力增大涂佃。這時候我們就需要引入分布式緩存來解決,現(xiàn)在使用最多的分布式緩存是redis

image

當(dāng)引入分布式緩存之后就可以把應(yīng)用緩存的架構(gòu)調(diào)整成上面的結(jié)構(gòu)蜈敢。

緩存使用模式的實踐

緩存使用的模式大概分為兩類:Cache-Aside辜荠、Cache-As-SoR(SoR表示實際存儲數(shù)據(jù)的系統(tǒng),也就是數(shù)據(jù)源)

Cache-Aside

業(yè)務(wù)代碼圍繞著緩存來寫抓狭,通常都是從緩存中來獲取數(shù)據(jù)伯病,如果緩存沒有命中,則從數(shù)據(jù)庫中查找否过,查詢到之后就把數(shù)據(jù)放入到緩存午笛;當(dāng)數(shù)據(jù)被更新之后,也需要對應(yīng)的去更新緩存中的數(shù)據(jù)苗桂。這種模式也是我們通常使用最多的药磺。

  • 讀場景
value = cache.get(key); //從緩存中讀取數(shù)據(jù)
if(value == null) {
    value = loadFromDatabase(key); //從數(shù)據(jù)庫中查詢
    cache.put(key, value); //放入到緩存中
}
  • 寫場景
wirteToDatabase(key, value); //寫入到數(shù)據(jù)庫
cache.put(key, value); //放入到緩存中 或者 可以刪除掉緩存 cache.remove(key) ,再讀取的時候再查一次

Spring的Cache擴展就是使用的Cache-Aside模式煤伟,Spring為了把業(yè)務(wù)代碼和緩存的讀取更新分離癌佩,對Cache-Aside模式使用AOP進行了封裝木缝,提供了多個注解來實現(xiàn)讀寫場景。官方參考文檔:

  • @Cacheable : 通常是放在查詢方法上围辙,實現(xiàn)的就是Cache-Aside讀的場景我碟,先查緩存,如果不存在在查詢數(shù)據(jù)庫姚建,最后把查詢出來的結(jié)果放入到緩存矫俺。
  • @CachePut : 通常用在保存更新方法上面,實現(xiàn)的就是Cache-Aside寫的場景桥胞,更新完成數(shù)據(jù)庫后把數(shù)據(jù)放入到緩存中恳守。
  • @CacheEvict : 從緩存中刪除指定key的緩存

對于一些允許有一點點更新延遲基礎(chǔ)數(shù)據(jù)可以考慮使用canal訂閱binlog日志來完成緩存的增量更新。

Cache-Aside還有個問題贩虾,如果某個時刻熱點數(shù)據(jù)緩存失效催烘,那么會有很多請求同時打到后端數(shù)據(jù)庫上,數(shù)據(jù)庫的壓力會瞬間增大

Cache-As-SoR

Cache-As-SoR模式也就會把Cache看做是數(shù)據(jù)源缎罢,所有的操作都是針對緩存伊群,Cache在委托給真正的SoR去實現(xiàn)讀或者寫。業(yè)務(wù)代碼中只會看到Cache的操作策精,這種模式又分為了三種

Read Through

應(yīng)用程序始終從緩存中請求數(shù)據(jù)舰始,如果緩存中沒有數(shù)據(jù),則它負責(zé)使用提供的數(shù)據(jù)加載程序從數(shù)據(jù)庫中檢索數(shù)據(jù)咽袜,檢索數(shù)據(jù)后丸卷,緩存會自行更新并將數(shù)據(jù)返回給調(diào)用的應(yīng)用程序。Gauva Cache询刹、Caffeine谜嫉、EhCache都支持這種模式;

  1. Caffeine實現(xiàn)Read Through
    由于Gauva Cache和Caffeine實現(xiàn)類似凹联,所以這里只展示Caffeine的實現(xiàn)沐兰,以下代碼來自Caffeine官方文檔
LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key));

// Lookup and compute an entry if absent, or null if not computable
Graph graph = cache.get(key);
// Lookup and compute entries that are absent
Map<Key, Graph> graphs = cache.getAll(keys);

在build Cache的時候指定一個CacheLoader

  • [1] 在應(yīng)用程序中直接調(diào)用cache.get(key)
  • [2] 首先查詢緩存,如果緩存存在就直接返回數(shù)據(jù)
  • [3] 如果不存在蔽挠,就會委托給CacheLoader去數(shù)據(jù)源中查詢數(shù)據(jù)住闯,之后在放入到緩存,返回給應(yīng)用程序

CacheLoader不要直接返回null澳淑,建議封裝成自己定義的Null對像比原,在放入到緩存中,可以防止緩存擊穿

為了防止因為某個熱點數(shù)據(jù)失效導(dǎo)致后端數(shù)據(jù)庫壓力增大的情況杠巡,我可以在CacheLoader中使用鎖限制只允許一個請求去查詢數(shù)據(jù)庫春寿,其他的請求都等待第一個請求查詢完成后從緩存中獲取,在上一篇 《萬字長文聊緩存(上)》中我們聊到了Nginx也有類似的配置參數(shù)

value = loadFromCache(key);
if(value != null) {
    return value;
}
synchronized (lock) {
    value = loadFromCache(key);
    if(value != null) {
        return value;
    }
    return loadFromDatabase(key);
}
  1. EhCache實現(xiàn)Read Through
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設(shè)置最大緩存條目數(shù)
CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withLoaderWriter(new CacheLoaderWriter<String, String>(){
            @Override
            public String load(String key) throws Exception {
                //load from database
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {

            }

            @Override
            public void delete(String key) throws Exception {

            }
        })
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
System.out.println(cache.get("name"));

在EhCache中使用的是CacheLoaderWriter來從數(shù)據(jù)庫中加載數(shù)據(jù)忽孽;解決因為某個熱點數(shù)據(jù)失效導(dǎo)致后端數(shù)據(jù)庫壓力增大的問題和上面的方式一樣绑改,也可以在load中實現(xiàn)谢床。

Write Through

和Read Through模式類似,當(dāng)數(shù)據(jù)進行更新時厘线,先去更新SoR识腿,成功之后在更新緩存。

  1. Caffeine實現(xiàn)Write Through
Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(100)
        .writer(new CacheWriter<String, String>() {
            @Override
            public void write(@NonNull String key, @NonNull String value) {
                //write data to database
                System.out.println(key);
                System.out.println(value);
            }

            @Override
            public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause removalCause) {
                //delete from database
            }
        })
        .build();

cache.put("name", "silently9527");

Caffeine通過使用CacheWriter來實現(xiàn)Write Through造壮,CacheWriter可以同步的監(jiān)聽到緩存的創(chuàng)建渡讼、變更和刪除操作,只有寫成功了才會去更新緩存

  1. EhCache實現(xiàn)Write Through
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設(shè)置最大緩存條目數(shù)
CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
        .withLoaderWriter(new CacheLoaderWriter<String, String>(){
            @Override
            public String load(String key) throws Exception {
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {
                //write data to database
                System.out.println(key);
                System.out.println(value);
            }

            @Override
            public void delete(String key) throws Exception {
                //delete from database
            }
        })
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);
System.out.println(cache.get("name"));

cache.put("website","https://silently9527.cn");

EhCache還是通過CacheLoaderWriter來實現(xiàn)的耳璧,當(dāng)我們調(diào)用cache.put("xxx","xxx")進行寫緩存的時候成箫,EhCache首先會將寫的操作委托給CacheLoaderWriter,有CacheLoaderWriter.write去負責(zé)寫數(shù)據(jù)源

Write Behind

這種模式通常先將數(shù)據(jù)寫入緩存旨枯,再異步地寫入數(shù)據(jù)庫進行數(shù)據(jù)同步蹬昌。這樣的設(shè)計既可以減少對數(shù)據(jù)庫的直接訪問,降低壓力攀隔,同時對數(shù)據(jù)庫的多次修改可以合并操作皂贩,極大地提升了系統(tǒng)的承載能力。但是這種模式也存在風(fēng)險昆汹,如當(dāng)緩存機器出現(xiàn)宕機時明刷,數(shù)據(jù)有丟失的可能。

  1. Caffeine要想實現(xiàn)Write Behind可以在CacheLoaderWriter.write方法中把數(shù)據(jù)發(fā)送到MQ中满粗,實現(xiàn)異步的消費辈末,這樣可以保證數(shù)據(jù)的安全,但是要想實現(xiàn)合并操作就需要擴展功能更強大的CacheLoaderWriter映皆。
  2. EhCache實現(xiàn)Write Behind
//1 定義線程池
PooledExecutionServiceConfiguration testWriteBehind = PooledExecutionServiceConfigurationBuilder
        .newPooledExecutionServiceConfigurationBuilder()
        .pool("testWriteBehind", 5, 10)
        .build();

CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
        .using(testWriteBehind)
        .build(true);
ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設(shè)置最大緩存條目數(shù)

//2 設(shè)置回寫模式配置
WriteBehindConfiguration testWriteBehindConfig = WriteBehindConfigurationBuilder
        .newUnBatchedWriteBehindConfiguration()
        .queueSize(10)
        .concurrencyLevel(2)
        .useThreadPool("testWriteBehind")
        .build();

CacheConfiguration<String, String> cacheConfig = CacheConfigurationBuilder
        .newCacheConfigurationBuilder(String.class, String.class, resource)
        .withLoaderWriter(new CacheLoaderWriter<String, String>() {
            @Override
            public String load(String key) throws Exception {
                return "silently9527";
            }

            @Override
            public void write(String key, String value) throws Exception {
                //write data to database
            }

            @Override
            public void delete(String key) throws Exception {
            }
        })
        .add(testWriteBehindConfig)
        .build();

Cache<String, String> cache = cacheManager.createCache("userInfo", cacheConfig);

首先使用PooledExecutionServiceConfigurationBuilder定義了線程池配置挤聘;然后使用WriteBehindConfigurationBuilder設(shè)置會寫模式配置,其中newUnBatchedWriteBehindConfiguration表示不進行批量寫操作劫扒,因為是異步寫檬洞,所以需要把寫操作先放入到隊列中狸膏,通過queueSize設(shè)置隊列大小沟饥,useThreadPool指定使用哪個線程池; concurrencyLevel設(shè)置使用多少個并發(fā)線程和隊列進行Write Behind

EhCache實現(xiàn)批量寫的操作也很容易

  • 首先把newUnBatchedWriteBehindConfiguration()替換成newBatchedWriteBehindConfiguration(10, TimeUnit.SECONDS, 20),這里設(shè)置的是數(shù)量達到20就進行批處理湾戳,如果10秒內(nèi)沒有達到20個也會進行處理
  • 其次在CacheLoaderWriter中實現(xiàn)wirteAll 和 deleteAll進行批處理

如果需要把對相同的key的操作合并起來只記錄最后一次數(shù)據(jù)贤旷,可以通過enableCoalescing()來啟用合并

寫到最后 點關(guān)注,不迷路

文中或許會存在或多或少的不足砾脑、錯誤之處幼驶,有建議或者意見也非常歡迎大家在評論交流。

最后韧衣,白嫖不好盅藻,創(chuàng)作不易购桑,希望朋友們可以點贊評論關(guān)注三連,因為這些就是我分享的全部動力來源??

源碼地址:https://github.com/silently9527/CacheTutorial

公眾號:貝塔學(xué)JAVA

image

原文地址:https://silently9527.cn/archives/94

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末氏淑,一起剝皮案震驚了整個濱河市勃蜘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌假残,老刑警劉巖缭贡,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異辉懒,居然都是意外死亡阳惹,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門眶俩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來莹汤,“玉大人,你說我怎么就攤上這事仿便√鍐” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵嗽仪,是天一觀的道長荒勇。 經(jīng)常有香客問我,道長闻坚,這世上最難降的妖魔是什么沽翔? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮窿凤,結(jié)果婚禮上仅偎,老公的妹妹穿的比我還像新娘。我一直安慰自己雳殊,他們只是感情好橘沥,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夯秃,像睡著了一般座咆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仓洼,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天介陶,我揣著相機與錄音,去河邊找鬼色建。 笑死哺呜,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的箕戳。 我是一名探鬼主播某残,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼国撵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了玻墅?” 一聲冷哼從身側(cè)響起卸留,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎椭豫,沒想到半個月后耻瑟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡赏酥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年喳整,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裸扶。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡框都,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呵晨,到底是詐尸還是另有隱情魏保,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布摸屠,位于F島的核電站谓罗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏季二。R本人自食惡果不足惜檩咱,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胯舷。 院中可真熱鬧刻蚯,春花似錦、人聲如沸桑嘶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逃顶。三九已至讨便,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間口蝠,已是汗流浹背器钟。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工津坑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留妙蔗,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓疆瑰,卻偏偏與公主長得像眉反,于是被迫代替她去往敵國和親昙啄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容