譯者 | 劉汪洋
審校 | 重樓
緩存是提高應(yīng)用性能的有效方法。我們之前發(fā)布過一篇文章,介紹了緩存的概念和好處,主要針對 Spring Boot 進行了討論。 在本文中,我們將探討優(yōu)化 Spring Boot 應(yīng)用程序緩存的 7 種技術(shù)。

目錄
1- 確定待緩存的對象
2- 緩存過期
淘汰策略
基于時間的過期策略
自定義淘汰策略
3- 條件緩存
4- 分布式緩存 vs 本地緩存
什么是本地緩存?
何時使用本地緩存 Vs. 分布式緩存?
在Spring Boot中實現(xiàn)本地緩存
5- 自定義鍵生成策略
6- 異步緩存
7- 監(jiān)控緩存指標以發(fā)現(xiàn)瓶頸
如何在Spring Boot中監(jiān)控緩存指標
總結(jié)
1.確定待緩存的對象
首先,我們需要明確哪些對象最適合緩存。一般而言,那些代價高昂且耗時的操作的結(jié)果需要優(yōu)先考慮,例如數(shù)據(jù)庫查詢、網(wǎng)絡(luò)服務(wù)調(diào)用或復(fù)雜計算的結(jié)果。然而,定義一些理想緩存候選對象的通用特征將更重要。這些特征有助于我們在應(yīng)用程序中識別適合緩存的對象:
- 頻繁訪問的數(shù)據(jù):經(jīng)常被訪問和重復(fù)訪問的數(shù)據(jù)是良好的緩存候選對象。
 - 代價高昂的獲取或計算:需要大量時間或計算資源來檢索或處理的數(shù)據(jù)。
 - 靜態(tài)或變化較少的數(shù)據(jù):變化不頻繁的數(shù)據(jù),確保緩存的數(shù)據(jù)在較長時間內(nèi)保持有效。
 - 高讀寫比率:當數(shù)據(jù)被訪問的頻率遠高于修改或更新的頻率時,可以有效地進行緩存。這保證了緩存快速讀取的優(yōu)勢超過其更新成本。
 - 可預(yù)測的訪問模式:遵循可預(yù)測訪問模式的數(shù)據(jù),允許更高效的緩存管理。
 
這些特征可以幫助我們有效地識別和緩存能夠顯著提升應(yīng)用程序性能的數(shù)據(jù)。 既然我們知道如何找到理想的緩存候選對象,就可以開始在 Spring Boot 應(yīng)用程序中啟用緩存??梢允褂米⒔饣蚓幊谭绞竭M行緩存配置。我在這篇文章中詳細討論了如何在 Spring Boot 中使用緩存,以及_ Digma_ 如何幫助我們發(fā)現(xiàn)緩存未命中或識別緩存候選對象。
2.緩存過期
緩存過期策略設(shè)置得當可以確保緩存數(shù)據(jù)的有效性和及時更新,提高內(nèi)存利用率,從而優(yōu)化 Spring Boot 應(yīng)用程序的性能和一致性。以下是一些推薦的管理 Spring Boot 應(yīng)用程序中緩存過期的方法:
淘汰策略
常見的淘汰策略包括:
- 最近最少使用(LRU):優(yōu)先淘汰最近最少訪問的對象。
 - 最不經(jīng)常使用(LFU):優(yōu)先淘汰訪問頻率最低的對象。
 - 先進先出(FIFO):優(yōu)先淘汰最早放入緩存的對象。
 
雖然 Spring Cache 抽象本身不直接支持這些淘汰策略,但你可以根據(jù)所選的緩存提供者使用其特定配置。通過仔細選擇和配置合適的淘汰策略,可以確保緩存機制高效運行,并與應(yīng)用程序的性能和資源利用目標相一致。
基于時間的過期策略
定義緩存條目的生存時間(TTL)在不同緩存提供者中有所不同。例如,在 Spring Boot 應(yīng)用程序中使用 Redis 進行緩存時,可以通過以下配置指定生存時間:
spring.cache.redis.time-to-live=10m如果緩存提供者不支持 TTL,可以使用@CacheEvict注解和調(diào)度器來實現(xiàn),例如:
@CacheEvict(value = "cache1", allEntries = true)
@Scheduled(fixedRateString = "${your.config.key.for.ttl.in.milli}")
public void emptyCache1() {
    // 刷新緩存,這里無需編寫任何代碼,除了描述性日志!
}自定義淘汰策略
通過根據(jù)事件或情況為單個緩存條目或所有條目定義自定義過期策略,可以防止緩存污染并保持其一致性。Spring Boot 具有多種注解來支持自定義過期策略:
- @CacheEvict:從緩存中刪除一個或所有條目。
 - @CachePut:用新值更新條目。
 - CacheManager:可以使用Spring的CacheManager和Cache接口實現(xiàn)自定義淘汰策略??梢允褂胑vict()、put()或clear()等方法進行操作,還可以通過getNativeCache()方法訪問底層緩存提供者,以獲得更多功能。
 
實施自定義淘汰策略的關(guān)鍵在于找到合適的時機和條件來淘汰緩存對象。
3.條件緩存
條件緩存與淘汰策略共同在優(yōu)化緩存策略中發(fā)揮重要作用。在某些情況下,我們不需要將所有特定實體的數(shù)據(jù)存儲在緩存中。
條件緩存確保只有符合特定條件的數(shù)據(jù)才會存儲在緩存中。
這可以防止緩存中存儲不必要的數(shù)據(jù),從而優(yōu)化資源利用。 @Cacheable和@CachePut注解都具有condition和unless屬性,允許我們?yōu)榫彺骓椂x條件:
- condition:指定一個 SpEL(Spring表達式語言)表達式,該表達式必須評估為true,數(shù)據(jù)才會被緩存(或更新)。
 - unless:指定一個 SpEL 表達式,該表達式必須評估為false,數(shù)據(jù)才會被緩存(或更新)。
 
為了更清楚,請看以下代碼示例:
@Cacheable(value = "employeeByName", condition = "#result.size() > 10", unless = "#result.size() < 1000")
public List<Employee> employeesByName(String name) {
    // 檢索數(shù)據(jù)的方法邏輯
    return someEmployeeList;
}在這段代碼中,只有當結(jié)果列表的大小大于 10 且小于 1000 時,員工列表才會被緩存。 最后一點,與前一部分類似,我們也可以使用CacheManager和Cache接口以編程方式實現(xiàn)條件緩存。這提供了更多的靈活性和對緩存行為的控制。
4.分布式緩存 vs. 本地緩存
談到緩存,我們通常會想到分布式緩存,如 Redis、Memcached 或 Hazelcast。在微服務(wù)架構(gòu)盛行的時代,本地緩存也在提升應(yīng)用性能方面發(fā)揮了重要作用。 理解本地緩存和分布式緩存之間的差異,有助于選擇合適的策略來優(yōu)化 Spring Boot 應(yīng)用中的緩存。每種類型都有其優(yōu)缺點,根據(jù)應(yīng)用需求進行權(quán)衡至關(guān)重要。
什么是本地緩存?
本地緩存是一種緩存機制,其中數(shù)據(jù)存儲在與應(yīng)用運行的同一臺機器或?qū)嵗膬?nèi)存中。一些知名的本地緩存庫包括 Ehcache、Caffeine 和 Guava Cache。 本地緩存允許快速訪問緩存數(shù)據(jù),因為它避免了與遠程數(shù)據(jù)檢索(分布式緩存)相關(guān)的網(wǎng)絡(luò)延遲和開銷。本地緩存通常比分布式緩存更易于設(shè)置和管理,并且不需要額外的基礎(chǔ)設(shè)施。
何時使用本地緩存 vs. 分布式緩存?
本地緩存適用于小型應(yīng)用程序或數(shù)據(jù)集較小且可以舒適地放入單臺機器內(nèi)存中的微服務(wù)。它也適用于低延遲至關(guān)重要且實例間數(shù)據(jù)一致性不是主要問題的場景。本地緩存的優(yōu)勢在于其速度快、設(shè)置和管理簡單。 另一方面,分布式緩存系統(tǒng)適用于具有大量數(shù)據(jù)緩存需求的大規(guī)模應(yīng)用,在這些應(yīng)用中,可伸縮性、容錯性和多個實例間的數(shù)據(jù)一致性至關(guān)重要。分布式緩存能夠分擔(dān)數(shù)據(jù)存儲負擔(dān),并在節(jié)點故障時提供數(shù)據(jù)冗余。
在 Spring Boot 中實現(xiàn)本地緩存
Spring Boot 支持通過各種內(nèi)存緩存提供程序(如 Ehcache、Caffeine 或 ConcurrentHashMap)實現(xiàn)本地緩存。我們只需添加所需的依賴項,并在 Spring Boot 應(yīng)用程序中啟用緩存即可。例如,要使用 Caffeine 實現(xiàn)本地緩存,我們需要添加以下依賴項:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>然后使用 @EnableCaching 注解啟用緩存:
@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}除了通用的 Spring 緩存配置外,我們還可以使用特定的配置來調(diào)整 Caffeine 緩存,如下所示:
spring:
  cache:
    caffeine:
      spec: maximumSize=500,expireAfterAccess=10m5. 自定義鍵生成策略
Spring 緩存注解中的默認鍵生成算法通常如下:
如果沒有參數(shù),則返回 0。 如果只有一個參數(shù),則返回該實例。 如果有多個參數(shù),則返回由所有參數(shù)的哈希值計算出的鍵。 只要 hashCode() 能準確反映對象的自然鍵,這種方法對具有自然鍵的對象效果良好。
但在某些情況下,默認的鍵生成策略效果并不好:
- 我們需要有意義的鍵。
 - 方法有多個相同類型的參數(shù)。
 - 方法具有可選參數(shù)或空參數(shù)。
 - 我們需要在鍵中包含上下文數(shù)據(jù),如區(qū)域、租戶 ID 或用戶角色,以使其唯一。
 
Spring Cache 提供了兩種定義自定義鍵生成策略的方法:
- 通過 key 屬性指定一個 SpEL(Spring 表達式語言)表達式,該表達式應(yīng)計算出一個新的鍵:
 
@CachePut(value = "phonebook", key = "#phoneNumber.name")
PhoneNumber create(PhoneNumber phoneNumber) {
    return phonebookRepository.insert(phoneNumber);
}- 定義一個實現(xiàn) KeyGenerator 接口的 bean,并將其指定給 keyGenerator 屬性:
 
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return "UNIQUE_KEY";
    }
}
@CachePut(value = "phonebook", keyGenerator = "customKeyGenerator")
PhoneNumber create(PhoneNumber phoneNumber) {
    return phonebookRepository.insert(phoneNumber);
}使用自定義鍵生成策略可以顯著提升應(yīng)用程序緩存的性能。設(shè)計良好的鍵生成策略能夠確保緩存條目的唯一性,最大限度地減少緩存丟失,并提高緩存命中率。
6. 異步緩存
如你所見,Spring 緩存抽象 API 是阻塞且同步的。如果你使用 WebFlux 棧,通過 Spring 緩存注解(如 @Cacheable 或 @CachePut)將緩存應(yīng)用于 Reactor 包裝對象(Mono 或 Flux)。在這種情況下,你有三種方法:
- 在 Reactor 類型上調(diào)用 cache() 方法,并在該方法上添加 Spring 緩存注解。
 - 使用底層緩存提供程序的異步 API(如果支持),并以編程方式處理緩存。
 - 實現(xiàn)一個圍繞緩存 API 的異步包裝器,使其支持異步操作(如果緩存提供程序不支持)。
 
然而,自Spring Framework 6.2 發(fā)布以來,如果緩存提供程序支持 WebFlux 項目的異步緩存(如 Caffeine Cache):
Spring 的聲明式緩存基礎(chǔ)設(shè)施會檢測到返回 Mono 或 Flux 的反應(yīng)式方法,并異步緩存其產(chǎn)生的值,而不是緩存返回的 Reactive Streams Publisher 實例。這需要目標緩存提供程序的支持,例如將 CaffeineCacheManager 設(shè)置為 setAsyncCacheMode(true)。
配置示例如下:
@Configuration
@EnableCaching
public class CacheConfig {
  @Bean
  public CacheManager cacheManager() {
    CaffeineCacheManager cacheManager = new CaffeineCacheManager();
    cacheManager.setCaffeine(buildCaffeineCache());
    cacheManager.setAsyncCacheMode(true); // <--
    return cacheManager;
  }
}7. 監(jiān)控緩存指標以發(fā)現(xiàn)瓶頸
監(jiān)控緩存指標對于識別瓶頸和優(yōu)化應(yīng)用程序中的緩存策略至關(guān)重要。 需要監(jiān)控的關(guān)鍵指標包括:
- 緩存命中率:緩存命中次數(shù)與總請求次數(shù)的比率,表明緩存的有效性。低命中率表明緩存未被有效利用。
 - 緩存未命中率:緩存未命中次數(shù)與總請求次數(shù)的比率,表明緩存經(jīng)常無法提供請求的數(shù)據(jù),可能是由于緩存大小不足或鍵管理不當。
 - 緩存淘汰率:緩存中項目被淘汰的頻率。高淘汰率表明緩存大小過小或淘汰策略不適合當前的訪問模式。
 - 內(nèi)存使用量:緩存使用的內(nèi)存量。
 - 延遲:從緩存中檢索數(shù)據(jù)所需的時間。
 - 錯誤率:通常指的是系統(tǒng)在處理請求時遇到的錯誤數(shù)量或比例。錯誤率可以包括緩存無法響應(yīng)請求、超時錯誤等。
 
如何在 Spring Boot 中監(jiān)控緩存指標
Spring Boot Actuator 啟動時會自動為所有可用的 Cache 實例配置 Micrometer。對于啟動后,動態(tài)創(chuàng)建或以編程方式創(chuàng)建的緩存,需要進行注冊。請查看相關(guān)文檔以確保您的緩存提供程序得到支持。 首先,添加 Actuator 和 Micrometer 依賴項:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>然后,啟用 Actuator 端點:
management.endpoints.web.exposure.include=*現(xiàn)在,可以使用 /actuator/caches 端點查看配置的緩存列表。對于緩存指標,可以使用以下端點:
- /actuator/metrics/cache.gets
 - /actuator/metrics/cache.puts
 - /actuator/metrics/cache.evictions
 - /actuator/metrics/cache.removals
 
總結(jié)
在本文中,我們詳細學(xué)習(xí)了 7 種優(yōu)化 Spring Boot 應(yīng)用緩存的技術(shù)。優(yōu)化緩存至關(guān)重要,因為它通過減少后端系統(tǒng)的負載和加快數(shù)據(jù)檢索速度,直接提升了應(yīng)用的性能和可擴展性。高效的緩存策略能夠最小化延遲,確保更快的響應(yīng)時間,從而顯著改善整體用戶體驗。
譯者介紹
劉汪洋,51CTO社區(qū)編輯,昵稱:明明如月,一個擁有 5 年開發(fā)經(jīng)驗的某大廠高級 Java 工程師,擁有多個主流技術(shù)博客平臺博客專家稱號,博客閱讀量 400W+,粉絲 3W+。2022 年騰訊云優(yōu)秀創(chuàng)作者,2022 年阿里云技術(shù)社區(qū)最受歡迎技術(shù)電子書 TOP 10 《性能優(yōu)化方法論》作者,慕課網(wǎng):剖析《阿里巴巴 Java 開發(fā)手冊》、深度解讀《Effective Java》 技術(shù)專欄作者。
原文標題:Top 7 Techniques to Optimize Caching in Spring Boot,作者:Saeed Zarinfam















 
 
 















 
 
 
 