當前位置: 妍妍網 > 碼農

面試官讓我實作二級緩存,我秀了他一臉

2024-05-06碼農

引言:在開發過程中,我們經常需要使用緩存來提高系統效能和減少對底層資源的存取。在某些情況下,我們需要實作一個二級緩存,其中包括本地緩存和分布式緩存(如Redis)。本文將介紹如何透過Java實作一個二級緩存,結合程式碼範例和實際事例,幫助讀者深入理解緩存的工作原理和實作方式。

題目

如何實作二級緩存?怎麽延伸出更多知識點?

推薦解析

二級緩存的優勢與缺點

優點:

1)二級緩存相比只呼叫一層 Redis 緩存,存取速度更快。對於一些不經常修改的數據而查詢十分頻繁的可以直接放在本地緩存(一級)裏面。

作為面試者的擴充套件延伸:我在本地緩存的實作中,我使用到了本地緩存效能之王 Caffeine 作為一級緩存,在市面上很多像 Redisson、Druid、Hbase 等知名開源計畫都用到了 Caffeine 。它實作了更加好用的緩存淘汰演算法 W-TinyLFU 演算法,結合了 LRU(最近最久未使用演算法) 演算法以及 LFU(最少使用演算法) 演算法的優點,所以選擇它能使本地緩存的使用更加方便快速。

2)使用了本地緩存相比直接去 Redis 中取,能夠減少與遠端 Redis 的數據 I/O 網路互動,降低了網路之間的消耗。

缺點:

1)增加了本地緩存對於一致性的維護更加復雜,提高了維護成本。

2)在分布式環境下,如何解決各個節點本地緩存一致性問題?使用類 Redis 的釋出訂閱功能,當一個節點的數據發生修改時,直接通知其他節點的緩存進行更新。

是不是已經初步了解了二級緩存的套用咧~ 下來先帶你實作一下二級緩存。

簡單實作

1)引入依賴

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2)配置 Caffeine

@Configuration
public classCaffeineConfig{
@Bean
public Cache<String,Object> caffeineCache(){
return Caffeine.newBuilder()
.initialCapacity(128)//初始大小
.maximumSize(1024)//最大數量
.expireAfterWrite(60, TimeUnit.SECONDS)//過期時間
.build();
}
}

3)使用二級緩存

@Resource
private Cache<String, Object> cache;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
voidtestCache(){
// 緩存測試存取
cache.put("test""test");
assertEquals("test", cache.getIfPresent("test"));
//實作二級緩存讀取
cache.get("test",
k -> {
//先查詢 Redis
Object obj = redisTemplate.opsForValue().get(k);
if (Objects.nonNull(obj)) {
System.out.println("緩存命中:" + k + " 從 Redis 讀取成功");
return obj;
}
// Redis沒有則查詢 DB
System.out.println("緩存沒有命中:" + k + " 從 DB 讀取");
// 從 DB 讀取 ..此處模擬省略
obj = "test";
// 存入 Redis
redisTemplate.opsForValue().set(k, "test");
//存入本地緩存由cache.get執行
return obj;
});
}

這樣一個緩存簡單的二級緩存使用就是這樣啦,但這樣是不是感覺對程式碼的侵入性有點強了,使用起來不夠靈活,下面再來帶大家使用 Spring 提供的 CacheManager 介面以及註解來實作它。

使用 Spring 的 CacheManager 實作

1)引入依賴

<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2)配置 CacheManager

@Configuration
public classCacheManagerConfig{
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager=new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(128)
.maximumSize(1024)
.expireAfterWrite(60, TimeUnit.SECONDS));
return cacheManager;
}
}

3)在啟動類上面加上 @EnableCaching 註解

@SpringBootApplication()
@EnableCaching
public classApplication{
}

4)使用二級緩存查詢

@Resource
private RedisTemplate<String,Object> redisTemplate;
@Cacheable(value = "test",key = "#id")
public String getStringTextById(Long id){
String key= "test" + id;
//先查詢 Redis
String obj = (String) redisTemplate.opsForValue().get(key);
if (Objects.nonNull(obj)){
log.info("從Redis中獲取數據");
return obj;
}
// Redis沒有則查詢 DB
log.info("從DB中獲取數據");
//此處省略查詢DB的程式碼
obj = "test";
//redis 中存入
redisTemplate.opsForValue().set(key,obj,120, TimeUnit.SECONDS);
return obj;
}

這裏要註意的是!

1) @Cacheable 註解的 value 跟 cacheNames 是互為別名的關系,表示當前方法的結果被緩存到哪個 cache 上面,它可以是一個陣列繫結多個 cache

2) @Cacheable 註解的 key 用來指定返回結果的 key,這個內容可以支持 SpringEL 運算式。

SpringEL運算式如下:

#參數名
#參數物件.內容名
#參數為陣列對應下標

5)使用二級緩存更新數據

@CachePut(cacheNames = "test",key = "#id")
public String updateOrder(String testData, Long id){
log.info("更新數據");
//更新資料庫 此處省略
//修改 Redis
redisTemplate.opsForValue().set("test" + id,
testData, 120, TimeUnit.SECONDS);
//返回要緩存原生的數據
return testData;
}

這裏要註意的是!需要返回物件或值,因為要進行本地緩存操作。

6)使用二級緩存刪除數據

@CacheEvict(cacheNames = "test",key = "#id")
publicvoiddeleteOrder(Long id){
log.info("刪除數據");
//此處省略刪除資料庫的程式碼
//刪除 Redis
redisTemplate.delete("test" + id);
}

在這裏簡單的使用就到這裏啦!還有更加優雅的方式大家可以去實作一下,透過 AOP 去實作二級緩存~

二級緩存拋磚引玉

在上面跟大家一起談了談二級緩存的實作以及使用,這裏我們可以跟面試官再次周旋一下!我曾經在閱讀 Spring 源碼時我還了解到了 Spring 的三級緩存實作。

在 Spring 框架中,迴圈依賴是指兩個或多個 Bean 之間相互依賴,形成一個迴圈參照的情況。這種情況下,Spring IOC 容器在例項化 Bean 時可能會出現問題,因為它無法決定應該首先例項化哪個 Bean。為了解決這個問題,Spring 引入了三級緩存。

三級緩存是指 Spring IOC 容器中用於解決迴圈依賴問題的一種機制,它包含三個緩存階段:

1)singletonObjects:這是 Spring IOC 容器的一級緩存,用於儲存已經完全建立並初始化的 Bean例項。當 Bean 完全建立後,它會被放置在這個緩存中。

2)earlySingletonObjects:這是 Spring IOC 容器的二級緩存,用於儲存提前暴露的、尚未完全初始化的 Bean 例項。當 Bean 正在被建立但尚未完成初始化時,它會被放置在這個緩存中。

3)singletonFactories:這是 Spring IOC 容器的三級緩存,用於儲存 Bean 的工廠物件。在建立Bean 例項時,Spring 首先會在這個緩存中尋找工廠物件,如果找到則使用該工廠物件來建立 Bean 例項。

三級緩存的作用

三級緩存的作用是為了解決迴圈依賴時的初始化順序問題。在初始化 Bean 時,Spring 會首先將 Bean 的例項放入三級緩存中,然後進行內容註入等操作。如果發現迴圈依賴,Spring 會在二級緩存中尋找對應的 Bean 例項,如果找到則直接返回,否則會呼叫Bean的工廠方法來建立 Bean 例項,並將其放入二級緩存中。當 Bean 例項化完成後,會從二級緩存中移除,並放入一級緩存中。

為什麽需要三級緩存而不是二級緩存

二級緩存只儲存了尚未初始化完成的 Bean 例項,而三級緩存儲存了 Bean 的工廠物件。這樣做的好處是,當發現迴圈依賴時,可以透過 Bean 的工廠物件來建立 Bean 例項,從而避免了直接從二級緩存中獲取可能尚未完成初始化的 Bean 例項而導致的問題。因此,三級緩存提供了更加靈活和可靠的解決方案,能夠更好地處理迴圈依賴問題。

推薦文章和書籍

文章:https://zhuanlan.zhihu.com/p/86293659

書籍:【 Java 核心技術卷 I 】

歡迎交流

當談到二級緩存時,我們可以探討以下幾個問題:

1)什麽是二級緩存?二級緩存在電腦系統中有什麽套用?

2)二級緩存的基本組成和工作原理是什麽?

3)二級緩存和一級緩存有何區別?

這些問題將幫助我們深入了解二級緩存的概念、套用和原理,從而更好地最佳化電腦系統的效能。

點燃求職熱情!每周持續更新,海量面試題和面經等你挑戰!趕緊關註面試鴨公眾號,輕松備戰春招和暑期實習!

往期推薦