深入解析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
- pom.xml中添加依賴
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.4</version>
</dependency>
- 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");
}
- 監(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");
}
- 統(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()
獲取到CacheStats
。CacheStats
提供以下統(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到磁盤上免胃。
這是典型 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
當(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都支持這種模式;
- 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);
}
- 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识腿,成功之后在更新緩存。
- 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)建渡讼、變更和刪除操作,只有寫成功了才會去更新緩存
- 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ù)有丟失的可能。
- Caffeine要想實現(xiàn)Write Behind可以在
CacheLoaderWriter.write
方法中把數(shù)據(jù)發(fā)送到MQ中满粗,實現(xiàn)異步的消費辈末,這樣可以保證數(shù)據(jù)的安全,但是要想實現(xiàn)合并操作就需要擴展功能更強大的CacheLoaderWriter
映皆。 - 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