背景
在高性能的服務架構設計中,緩存是一個不可或缺的環節。在實際的項目中,我們通常會将一些熱點資料存儲到Redis或Memcached 這類緩存中間件中,隻有當緩存的通路沒有命中時再查詢資料庫。在提升通路速度的同時,也能降低資料庫的壓力。
随着不斷的發展,這一架構也産生了改進,在一些場景下可能單純使用Redis類的遠端緩存已經不夠了,還需要進一步配合本地緩存使用,例如Guava cache或Caffeine,進而再次提升程式的響應速度與服務性能。于是,就産生了使用本地緩存作為一級緩存,再加上遠端緩存作為二級緩存的兩級緩存架構。
在先不考慮并發等複雜問題的情況下,兩級緩存的通路流程可以用下面這張圖來表示:
為什麼要使用本地緩存
- 本地緩存基于本地環境的記憶體,通路速度非常快,對于一些變更頻率低、實時性要求低的資料,可以放在本地緩存中,提升通路速度
- 使用本地緩存能夠減少和Redis類的遠端緩存間的資料互動,減少網絡I/O開銷,降低這一過程中在網絡通信上的耗時
設計一個本地記憶體需要有什麼功能
- 存儲,并可以讀、寫;
- 原子操作(線程安全),如ConcurrentHashMap
- 可以設定緩存的最大限制;
- 超過最大限制有對應淘汰政策,如LRU、LFU
- 過期時間淘汰,如定時、懶式、定期;
- 持久化
- 統計監控
本地緩存方案選型
1. 使用ConcurrentHashMap實作本地緩存
緩存的本質就是存儲在記憶體中的KV資料結構,對應的就是jdk中線程安全的ConcurrentHashMap,但是要實作緩存,還需要考慮淘汰、最大限制、緩存過期時間淘汰等等功能;
優點是實作簡單,不需要引入第三方包,比較适合一些簡單的業務場景。缺點是如果需要更多的特性,需要定制化開發,成本會比較高,并且穩定性和可靠性也難以保障。對于比較複雜的場景,建議使用比較穩定的開源工具。
2. 基于Guava Cache實作本地緩存
Guava是Google團隊開源的一款 Java 核心增強庫,包含集合、并發原語、緩存、IO、反射等工具箱,性能和穩定性上都有保障,應用十分廣泛。Guava Cache支援很多特性:
- 支援最大容量限制
- 支援兩種過期删除政策(插入時間和通路時間)
- 支援簡單的統計功能
- 基于LRU算法實作
使用代碼如下:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
@Slf4j
public class GuavaCacheTest {
public static void main(String[] args) throws ExecutionException {
Cache<String, String> cache = CacheBuilder.newBuilder()
.initialCapacity(5) // 初始容量
.maximumSize(10) // 最大緩存數,超出淘汰
.expireAfterWrite(60, TimeUnit.SECONDS) // 過期時間
.build();
String orderId = String.valueOf(123456789);
// 擷取orderInfo,如果key不存在,callable中調用getInfo方法傳回資料
String orderInfo = cache.get(orderId, () -> getInfo(orderId));
log.info("orderInfo = {}", orderInfo);
}
private static String getInfo(String orderId) {
String info = "";
// 先查詢redis緩存
log.info("get data from redis");
// 當redis緩存不存在查db
log.info("get data from mysql");
info = String.format("{orderId=%s}", orderId);
return info;
}
}
3. Caffeine
Caffeine是基于java8實作的新一代緩存工具,緩存性能接近理論最優。可以看作是Guava Cache的增強版,功能上兩者類似,不同的是Caffeine采用了一種結合LRU、LFU優點的算法:W-TinyLFU,在性能上有明顯的優越性
使用代碼如下:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
@Slf4j
public class CaffeineTest {
public static void main(String[] args) {
Cache<String, String> cache = Caffeine.newBuilder()
.initialCapacity(5)
// 超出時淘汰
.maximumSize(10)
//設定寫緩存後n秒鐘過期
.expireAfterWrite(60, TimeUnit.SECONDS)
//設定讀寫緩存後n秒鐘過期,實際很少用到,類似于expireAfterWrite
//.expireAfterAccess(17, TimeUnit.SECONDS)
.build();
String orderId = String.valueOf(123456789);
String orderInfo = cache.get(orderId, key -> getInfo(key));
System.out.println(orderInfo);
}
private static String getInfo(String orderId) {
String info = "";
// 先查詢redis緩存
log.info("get data from redis");
// 當redis緩存不存在查db
log.info("get data from mysql");
info = String.format("{orderId=%s}", orderId);
return info;
}
}
4. Encache
Encache是一個純Java的程序内緩存架構,具有快速、精幹等特點,是Hibernate中預設的CacheProvider。同Caffeine和Guava Cache相比,Encache的功能更加豐富,擴充性更強:
- 支援多種緩存淘汰算法,包括LRU、LFU和FIFO
- 緩存支援堆記憶體儲、堆外存儲、磁盤存儲(支援持久化)三種
- 支援多種叢集方案,解決資料共享問題
使用代碼如下:
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.9.7</version>
</dependency>
@Slf4j
public class EhcacheTest {
private static final String ORDER_CACHE = "orderCache";
public static void main(String[] args) {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
// 建立cache執行個體
.withCache(ORDER_CACHE, CacheConfigurationBuilder
// 聲明一個容量為20的堆内緩存
.newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20)))
.build(true);
// 擷取cache執行個體
Cache<String, String> cache = cacheManager.getCache(ORDER_CACHE, String.class, String.class);
String orderId = String.valueOf(123456789);
String orderInfo = cache.get(orderId);
if (StrUtil.isBlank(orderInfo)) {
orderInfo = getInfo(orderId);
cache.put(orderId, orderInfo);
}
log.info("orderInfo = {}", orderInfo);
}
private static String getInfo(String orderId) {
String info = "";
// 先查詢redis緩存
log.info("get data from redis");
// 當redis緩存不存在查db
log.info("get data from mysql");
info = String.format("{orderId=%s}", orderId);
return info;
}
}
本地緩存問題及解決
1. 緩存一緻性
兩級緩存與資料庫的資料要保持一緻,一旦資料發生了修改,在修改資料庫的同時,本地緩存、遠端緩存應該同步更新。
解決方案1: MQ
一般現在部署都是叢集部署,有多個不同節點的本地緩存; 可以使用MQ的廣播模式,當資料修改時向MQ發送消息,節點監聽并消費消息,删除本地緩存,達到最終一緻性;
解決方案2:Canal + MQ
如果你不想在你的業務代碼發送MQ消息,還可以适用近幾年比較流行的方法:訂閱資料庫變更日志,再操作緩存。Canal 訂閱Mysql的 Binlog日志,當發生變化時向MQ發送消息,進而也實作資料一緻性。
2. 如何提高本地緩存命中率
參考:如何提高緩存命中率
3. 本地記憶體的技術選型問題
- 從易用性角度,Guava Cache、Caffeine和Encache都有十分成熟的接入方案,使用簡單。
- 從功能性角度,Guava Cache和Caffeine功能類似,都是隻支援堆内緩存,Encache相比功能更為豐富
- 從性能上進行比較,Caffeine最優、GuavaCache次之,Encache最差(下圖是三者的性能對比結果)
對于本地緩存的方案中,我比較推薦Caffeine,性能上遙遙領先。
雖然Encache功能更為豐富,甚至提供了持久化和叢集的功能,但是這些功能完全可以依靠其他方式實作。真實的業務工程中,建議使用Caffeine作為本地緩存,另外使用redis或者memcache作為分布式緩存,構造多級緩存體系,保證性能和可靠性。
來源:https://blog.csdn.net/One_hundred_nice