一、摘要
說到緩存,面試官基本上會繞不開以下幾個話題!
項目中哪些地方用到了緩存?為什么要使用緩存?怎么使用它的?引入緩存后會帶來哪些問題?
這些問題,基本上是互聯(lián)網(wǎng)公司面試時必問的一些問題,如果面試的時候,連緩存都不清楚,那確實多少顯的有些尷尬!
項目里面為什么要引入緩存?這個問題還得結(jié)合項目中的業(yè)務(wù)來回答!
引入緩存,其實主要有兩個用途: 高性能 、 高并發(fā) !
假設(shè)某個操作非常頻繁,比如網(wǎng)站的商城首頁,需要頻繁的從數(shù)據(jù)庫里面獲取商品數(shù)據(jù),可能從數(shù)據(jù)庫一頓各種亂七八糟的操作下來,平均耗時 500 ms,隨著請求頻次越高,用戶等待數(shù)據(jù)的返回結(jié)果時間越來越長,體驗越來越差。
如果此時, 引入緩存 ,將數(shù)據(jù)庫里面查詢出來的商品數(shù)據(jù)信息,放入緩存服務(wù)里面,當用戶再此發(fā)起查詢操作的時候,直接從緩存服務(wù)里面獲取,速度從耗時 500 ms,可能直接優(yōu)化成 5 ms,體驗上瞬間會上升好幾個層次!
這就是引入緩存帶來的高性能體驗結(jié)果 !
當然,除此之外, 引入緩存之前 ,以 mysql 數(shù)據(jù)庫為例,單臺機器一秒內(nèi)的請求次數(shù)到達 2000 之后就會開始報警; 引入緩存之后 ,比如以 redis 緩存服務(wù)器為例,單臺機器一秒內(nèi)的請求次數(shù)支持 110000 次,兩者支持的并發(fā)量完全不是一個數(shù)量級的。
這就是引入緩存帶來的高并發(fā)體驗結(jié)果 !
尤其是對于流量很大的業(yè)務(wù),引入緩存,給系統(tǒng)帶來的提升是十分顯著的 。
可能有的同學又會發(fā)出疑問,緩存和數(shù)據(jù)庫為啥差距這么大,有啥區(qū)別?
我們都知道在計算機領(lǐng)域,數(shù)據(jù)的存儲主要有兩處: 一處是內(nèi)存,另一處是磁盤 。
在計算機中,內(nèi)存的數(shù)據(jù)讀寫性能遠超磁盤的讀寫性能,盡管如此,其實兩者也有不同,如果數(shù)據(jù)存儲到內(nèi)存中,雖然讀寫性能非常高,但是當電腦重啟之后,數(shù)據(jù)會全部清除;而存入磁盤的數(shù)據(jù),雖然讀寫性能很差,但是電腦重啟之后數(shù)據(jù)不會丟失。
因為兩者的數(shù)據(jù)存儲方案不同,造就了不同的實踐用途 !
我們上面講到的緩存服務(wù),其實本質(zhì)就是將數(shù)據(jù)存儲到內(nèi)存中;而數(shù)據(jù)庫服務(wù),是將數(shù)據(jù)寫入到磁盤,從磁盤中讀取數(shù)據(jù)。
無論是哪種方案,沒有絕對的好與壞,主要還是取決于實際的業(yè)務(wù)用途。
在項目中如何引入緩存呢?我們通常的做法如下:
操作步驟:
- 1.當用戶發(fā)起訪問某數(shù)據(jù)的操作時,檢查緩存服務(wù)里面是否存在,如果存在,直接返回;如果不存在,走數(shù)據(jù)庫的查詢服務(wù)
- 2.從數(shù)據(jù)庫里面獲取到有效數(shù)據(jù)之后,存入緩存服務(wù),并返回給用戶
- 3.當被訪問的數(shù)據(jù)發(fā)生更新的時候,需要同時刪除緩存服務(wù),以便用戶再次查詢的時候,能獲取到最新的數(shù)據(jù)
當然以上的緩存處理辦法,對于簡單的需要緩存的業(yè)務(wù)場景,能輕松應(yīng)對。
但是面對復(fù)雜的業(yè)務(wù)場景和服務(wù)架構(gòu),尤其是對緩存要求比較高的業(yè)務(wù),引入緩存的方式,也會跟著一起變化!
從緩存面向的對象不同,緩存分為: 本地緩存 、分布式緩存和 多級緩存 。
所謂 本地緩存 ,相信大家都能理解,在單個計算機服務(wù)實例中,直接把數(shù)據(jù)緩存到內(nèi)存中進行使用。
但是現(xiàn)在的服務(wù),大多都是以集群的方式來部署,你也可以這樣理解,同一個網(wǎng)站服務(wù),同時在兩臺計算機里面部署,比如你用到的session
會話,就無法同時共享,因此需要引入一個獨立的緩存服務(wù)來連接兩臺服務(wù)器,這個獨立部署的緩存服務(wù),我們把這種技術(shù)實踐方案稱為 分布式緩存 。
在實際的業(yè)務(wù)中,本地緩存和分布式緩存會同時結(jié)合進行使用,當收到訪問某個數(shù)據(jù)的操作時,會優(yōu)先從本地緩存服務(wù)(也叫一級緩存)查詢,如果沒有,再從分布式緩存服務(wù)(也叫二級緩存)里面獲取,如果也沒有,最后再從數(shù)據(jù)庫里面獲??;從數(shù)據(jù)庫查詢完成之后,在依次更新分布式緩存服務(wù)、本次緩存服務(wù),我們把這個技術(shù)實踐方案叫 多級緩存 !
由于篇幅的原因,我們在后期給大家介紹 分布式緩存服務(wù) 、 多級緩存服務(wù) 。
今天主要圍繞本地緩存服務(wù)的技術(shù)實現(xiàn),給大家進行分享和介紹!
二、方案介紹
如果使用過緩存的同學,可以很容易想到緩存需要哪些東西,通常我們在使用緩存的時候,比較關(guān)注兩個地方,第一是內(nèi)存持久化,第二是支持緩存的數(shù)據(jù)自動過期清楚。
基于以上的要求,我們向介紹以下幾種技術(shù)實現(xiàn)方案。
2.1、手寫一個緩存服務(wù)
對于簡單的數(shù)據(jù)緩存,我們完全可以自行編寫一套緩存服務(wù),實現(xiàn)過程如下!
首先創(chuàng)建一個緩存實體類
public class CacheEntity {
/**
* 緩存鍵
*/
private String key;
/**
* 緩存值
*/
private Object value;
/**
* 過期時間
*/
private Long expireTime;
//...set、get
}
接著,編寫一個緩存操作工具類CacheUtils
public class CacheUtils {
/**
* 緩存數(shù)據(jù)
*/
private final static Map< String, CacheEntity > CACHE_MAP = new ConcurrentHashMap< >();
/**
* 定時器線程池,用于清除過期緩存
*/
private static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
static {
// 注冊一個定時線程任務(wù),服務(wù)啟動1秒之后,每隔500毫秒執(zhí)行一次
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// 清理過期緩存
clearCache();
}
},1000,500,TimeUnit.MILLISECONDS);
}
/**
* 添加緩存
* @param key 緩存鍵
* @param value 緩存值
*/
public static void put(String key, Object value){
put(key, value, 0);
}
/**
* 添加緩存
* @param key 緩存鍵
* @param value 緩存值
* @param expire 緩存時間,單位秒
*/
public static void put(String key, Object value, long expire){
CacheEntity cacheEntity = new CacheEntity()
.setKey(key)
.setValue(value);
if(expire > 0){
Long expireTime = System.currentTimeMillis() + Duration.ofSeconds(expire).toMillis();
cacheEntity.setExpireTime(expireTime);
}
CACHE_MAP.put(key, cacheEntity);
}
/**
* 獲取緩存
* @param key
* @return
*/
public static Object get(String key){
if(CACHE_MAP.containsKey(key)){
return CACHE_MAP.get(key).getValue();
}
return null;
}
/**
* 移除緩存
* @param key
*/
public static void remove(String key){
if(CACHE_MAP.containsKey(key)){
CACHE_MAP.remove(key);
}
}
/**
* 清理過期的緩存數(shù)據(jù)
*/
private static void clearCache(){
if(CACHE_MAP.size() > 0){
return;
}
Iterator< Map.Entry< String, CacheEntity >> iterator = CACHE_MAP.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry< String, CacheEntity > entry = iterator.next();
if(entry.getValue().getExpireTime() != null && entry.getValue().getExpireTime().longValue() > System.currentTimeMillis()){
iterator.remove();
}
}
}
}
最后,我們來測試一下緩存服務(wù)
// 寫入緩存數(shù)據(jù)
CacheUtils.put("userName", "張三", 3);
// 讀取緩存數(shù)據(jù)
Object value1 = CacheUtils.get("userName");
System.out.println("第一次查詢結(jié)果:" + value1);
// 停頓4秒
Thread.sleep(4000);
// 讀取緩存數(shù)據(jù)
Object value2 = CacheUtils.get("userName");
System.out.println("第二次查詢結(jié)果:" + value2);
輸出結(jié)果,與預(yù)期一致!
第一次查詢結(jié)果:張三
第二次查詢結(jié)果:null
實現(xiàn)思路其實很簡單,采用ConcurrentHashMap
作為緩存數(shù)據(jù)存儲服務(wù),然后開啟一個定時調(diào)度,每隔500
毫秒檢查一下過期的緩存數(shù)據(jù),然后清除掉!
2.2、基于 Guava Cache 實現(xiàn)本地緩存
Guava 是 Google 團隊開源的一款 Java 核心增強庫,包含集合、并發(fā)原語、緩存、IO、反射等工具箱,性能和穩(wěn)定性上都有保障,應(yīng)用十分廣泛。
相比自己編寫的緩存服務(wù),Guava Cache 要強大的多,支持很多特性如下:
- 支持最大容量限制
- 支持兩種過期刪除策略(插入時間和讀取時間)
- 支持簡單的統(tǒng)計功能
- 基于 LRU 算法實現(xiàn)
使用方面也很簡單,首先引入guava
庫包。
< !--guava-- >
< dependency >
< groupId >com.google.guava< /groupId >
< artifactId >guava< /artifactId >
< version >31.1-jre< /version >
< /dependency >
案例代碼如下:
// 創(chuàng)建一個緩存實例
Cache< String, String > cache = CacheBuilder.newBuilder()
// 初始容量
.initialCapacity(5)
// 最大緩存數(shù),超出淘汰
.maximumSize(10)
// 過期時間
.expireAfterWrite(3, TimeUnit.SECONDS)
.build();
// 寫入緩存數(shù)據(jù)
cache.put("userName", "張三");
// 讀取緩存數(shù)據(jù)
String value1 = cache.get("userName", () - > {
// 如果key不存在,會執(zhí)行回調(diào)方法
return "key已過期";
});
System.out.println("第一次查詢結(jié)果:" + value1);
// 停頓4秒
Thread.sleep(4000);
// 讀取緩存數(shù)據(jù)
String value2 = cache.get("userName", () - > {
// 如果key不存在,會執(zhí)行回調(diào)方法
return "key已過期";
});
System.out.println("第二次查詢結(jié)果:" + value2);
輸出結(jié)果:
第一次查詢結(jié)果:張三
第二次查詢結(jié)果:key已過期
2.3、基于 Caffeine 實現(xiàn)本地緩存
Caffeine 是基于 java8 實現(xiàn)的新一代緩存工具,緩存性能接近理論最優(yōu),可以看作是 Guava Cache 的增強版,功能上兩者類似,不同的是 Caffeine 采用了一種結(jié)合 LRU、LFU 優(yōu)點的算法:W-TinyLFU,在性能上有明顯的優(yōu)越性。
使用方面也很簡單,首先引入caffeine
庫包。
< !--caffeine-- >
< dependency >
< groupId >com.github.ben-manes.caffeine< /groupId >
< artifactId >caffeine< /artifactId >
< version >2.9.3< /version >
< /dependency >
案例代碼如下:
// 創(chuàng)建一個緩存實例
Cache< String, String > cache = Caffeine.newBuilder()
// 初始容量
.initialCapacity(5)
// 最大緩存數(shù),超出淘汰
.maximumSize(10)
// 設(shè)置緩存寫入間隔多久過期
.expireAfterWrite(3, TimeUnit.SECONDS)
// 設(shè)置緩存最后訪問后間隔多久淘汰,實際很少用到
//.expireAfterAccess(3, TimeUnit.SECONDS)
.build();
// 寫入緩存數(shù)據(jù)
cache.put("userName", "張三");
// 讀取緩存數(shù)據(jù)
String value1 = cache.get("userName", (key) - > {
// 如果key不存在,會執(zhí)行回調(diào)方法
return "key已過期";
});
System.out.println("第一次查詢結(jié)果:" + value1);
// 停頓4秒
Thread.sleep(4000);
// 讀取緩存數(shù)據(jù)
String value2 = cache.get("userName", (key) - > {
// 如果key不存在,會執(zhí)行回調(diào)方法
return "key已過期";
});
System.out.println("第二次查詢結(jié)果:" + value2);
輸出結(jié)果:
第一次查詢結(jié)果:張三
第二次查詢結(jié)果:key已過期
2.4、基于 Encache 實現(xiàn)本地緩存
Encache 是一個純 Java 的進程內(nèi)緩存框架,具有快速、精干等特點,是 Hibernate 中默認的 CacheProvider。
同 Caffeine 和 Guava Cache 相比,Encache 的功能更加豐富,擴展性更強,特性如下:
- 支持多種緩存淘汰算法,包括 LRU、LFU 和 FIFO
- 緩存支持堆內(nèi)存儲、堆外存儲、磁盤存儲(支持持久化)三種
- 支持多種集群方案,解決數(shù)據(jù)共享問題
使用方面也很簡單,首先引入ehcache
庫包。
< !--ehcache-- >
< dependency >
< groupId >org.ehcache< /groupId >
< artifactId >ehcache< /artifactId >
< version >3.9.7< /version >
< /dependency >
案例代碼如下:
/**
* 自定義過期策略實現(xiàn)
*/
public class CustomExpiryPolicy< K, V > implements ExpiryPolicy< K, V > {
private final Map< K, Duration > keyExpireMap = new ConcurrentHashMap();
public Duration setExpire(K key, Duration duration) {
return keyExpireMap.put(key, duration);
}
public Duration getExpireByKey(K key) {
return Optional.ofNullable(keyExpireMap.get(key))
.orElse(null);
}
public Duration removeExpire(K key) {
return keyExpireMap.remove(key);
}
@Override
public Duration getExpiryForCreation(K key, V value) {
return Optional.ofNullable(getExpireByKey(key))
.orElse(Duration.ofNanos(Long.MAX_VALUE));
}
@Override
public Duration getExpiryForAccess(K key, Supplier< ? extends V > value) {
return getExpireByKey(key);
}
@Override
public Duration getExpiryForUpdate(K key, Supplier< ? extends V > oldValue, V newValue) {
return getExpireByKey(key);
}
}
public static void main(String[] args) throws InterruptedException {
String userCache = "userCache";
// 自定義過期策略
CustomExpiryPolicy< Object, Object > customExpiryPolicy = new CustomExpiryPolicy< >();
// 聲明一個容量為20的堆內(nèi)緩存配置
CacheConfigurationBuilder configurationBuilder = CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20))
.withExpiry(customExpiryPolicy);
// 初始化一個緩存管理器
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
// 創(chuàng)建cache實例
.withCache(userCache, configurationBuilder)
.build(true);
// 獲取cache實例
Cache< String, String > cache = cacheManager.getCache(userCache, String.class, String.class);
// 獲取過期策略
CustomExpiryPolicy expiryPolicy = (CustomExpiryPolicy)cache.getRuntimeConfiguration().getExpiryPolicy();
// 寫入緩存數(shù)據(jù)
cache.put("userName", "張三");
// 設(shè)置3秒過期
expiryPolicy.setExpire("userName", Duration.ofSeconds(3));
// 讀取緩存數(shù)據(jù)
String value1 = cache.get("userName");
System.out.println("第一次查詢結(jié)果:" + value1);
// 停頓4秒
Thread.sleep(4000);
// 讀取緩存數(shù)據(jù)
String value2 = cache.get("userName");
System.out.println("第二次查詢結(jié)果:" + value2);
}
輸出結(jié)果:
第一次查詢結(jié)果:張三
第二次查詢結(jié)果:null
三、小結(jié)
從易用性角度看:Guava Cache、Caffeine 和 Encache 都有十分成熟的接入方案,使用簡單。
從功能性角度看:Guava Cache 和 Caffeine 功能類似,都是只支持堆內(nèi)緩存,Encache 相比功能更為豐富,不僅支持堆內(nèi)緩存,還支持磁盤寫入、集群實現(xiàn)。
從性能角度看:Caffeine 最優(yōu)、GuavaCache 次之,Encache 最差。
以下是網(wǎng)絡(luò)上三者性能對比的結(jié)果。
對于本地緩存的技術(shù)選型, 推薦采用 Caffeine ,性能上毫無疑問,遙遙領(lǐng)先。
雖然 Encache 功能非常的豐富,甚至提供了持久化和集群的功能,但是相比更成熟的分布式緩存中間件 redis 來說,還是稍遜一些!
關(guān)于 redis 的使用,有興趣的同學可以查看歷史文章,之前有寫過 redis 系列相關(guān)的技術(shù)實踐介紹。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
6808瀏覽量
88743 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
8958瀏覽量
85081 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
2966瀏覽量
73812 -
緩存
+關(guān)注
關(guān)注
1文章
229瀏覽量
26635 -
磁盤
+關(guān)注
關(guān)注
1文章
361瀏覽量
25154
發(fā)布評論請先 登錄
相關(guān)推薦
評論