Java精選面試題(微信小程序):5000+道面試題和選擇題,真實(shí)面經(jīng)簡歷模版,包含Java基礎(chǔ)、并發(fā)、JVM、線程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架構(gòu)設(shè)計(jì)、大廠真題等,在線隨時(shí)刷題!

背景

在高性能的服務(wù)架構(gòu)設(shè)計(jì)中,緩存是一個不可或缺的環(huán)節(jié)。在實(shí)際的項(xiàng)目中,我們通常會將一些熱點(diǎn)數(shù)據(jù)存儲到Redis或Memcached 這類緩存中間件中,只有當(dāng)緩存的訪問沒有命中時(shí)再查詢數(shù)據(jù)庫。在提升訪問速度的同時(shí),也能降低數(shù)據(jù)庫的壓力。

隨著不斷的發(fā)展,這一架構(gòu)也產(chǎn)生了改進(jìn),在一些場景下可能單純使用Redis類的遠(yuǎn)程緩存已經(jīng)不夠了,還需要進(jìn)一步配合本地緩存使用,例如Guava cache或Caffeine,從而再次提升程序的響應(yīng)速度與服務(wù)性能。于是,就產(chǎn)生了使用本地緩存作為一級緩存,再加上遠(yuǎn)程緩存作為二級緩存的兩級緩存架構(gòu)。

在先不考慮并發(fā)等復(fù)雜問題的情況下,兩級緩存的訪問流程可以用下面這張圖來表示:

打開網(wǎng)易新聞 查看精彩圖片

為什么要使用本地緩存

本地緩存基于本地環(huán)境的內(nèi)存,訪問速度非常快,對于一些變更頻率低、實(shí)時(shí)性要求低的數(shù)據(jù),可以放在本地緩存中,提升訪問速度。

使用本地緩存能夠減少和Redis類的遠(yuǎn)程緩存間的數(shù)據(jù)交互,減少網(wǎng)絡(luò)I/O開銷,降低這一過程中在網(wǎng)絡(luò)通信上的耗時(shí)。

設(shè)計(jì)一個本地內(nèi)存需要有什么功能

  1. 存儲,并可以讀、寫;

  2. 原子操作(線程安全),如ConcurrentHashMap;

  3. 可以設(shè)置緩存的最大限制;

  4. 超過最大限制有對應(yīng)淘汰策略,如LRU、LFU;

  5. 過期時(shí)間淘汰,如定時(shí)、懶式、定期;

  6. 持久化;

  7. 統(tǒng)計(jì)監(jiān)控。

本地緩存方案選型

1. 使用ConcurrentHashMap實(shí)現(xiàn)本地緩存

緩存的本質(zhì)就是存儲在內(nèi)存中的KV數(shù)據(jù)結(jié)構(gòu),對應(yīng)的就是jdk中線程安全的ConcurrentHashMap,但是要實(shí)現(xiàn)緩存,還需要考慮淘汰、最大限制、緩存過期時(shí)間淘汰等等功能;

優(yōu)點(diǎn)是實(shí)現(xiàn)簡單,不需要引入第三方包,比較適合一些簡單的業(yè)務(wù)場景。缺點(diǎn)是如果需要更多的特性,需要定制化開發(fā),成本會比較高,并且穩(wěn)定性和可靠性也難以保障。對于比較復(fù)雜的場景,建議使用比較穩(wěn)定的開源工具。

2. 基于Guava Cache實(shí)現(xiàn)本地緩存

Guava是Google團(tuán)隊(duì)開源的一款 Java 核心增強(qiáng)庫,包含集合、并發(fā)原語、緩存、IO、反射等工具箱,性能和穩(wěn)定性上都有保障,應(yīng)用十分廣泛。Guava Cache支持很多特性:

  • 支持最大容量限制

  • 支持兩種過期刪除策略(插入時(shí)間和訪問時(shí)間)

  • 支持簡單的統(tǒng)計(jì)功能

  • 基于LRU算法實(shí)現(xiàn)

使用代碼如下:

    com.google.guava     guava     31.1-jre @Slf4j public class GuavaCacheTest {     public static void main(String[] args) throws ExecutionException {         Cache cache = CacheBuilder.newBuilder()                 .initialCapacity(5)  // 初始容量                 .maximumSize(10)     // 最大緩存數(shù),超出淘汰                 .expireAfterWrite(60, TimeUnit.SECONDS) // 過期時(shí)間                 .build();         String orderId = String.valueOf(123456789);         // 獲取orderInfo,如果key不存在,callable中調(diào)用getInfo方法返回?cái)?shù)據(jù)         String orderInfo = cache.get(orderId, () -> getInfo(orderId));         log.info("orderInfo = {}", orderInfo);     }     private static String getInfo(String orderId) {         String info = "";         // 先查詢r(jià)edis緩存         log.info("get data from 

3. Caffeine

Caffeine是基于java8實(shí)現(xiàn)的新一代緩存工具,緩存性能接近理論最優(yōu)??梢钥醋魇荊uava Cache的增強(qiáng)版,功能上兩者類似,不同的是Caffeine采用了一種結(jié)合LRU、LFU優(yōu)點(diǎn)的算法:W-TinyLFU,在性能上有明顯的優(yōu)越性

使用代碼如下:

    com.github.ben-manes.caffeine     caffeine     2.9.3 @Slf4j public class CaffeineTest {     public static void main(String[] args) {         Cache cache = Caffeine.newBuilder()                 .initialCapacity(5)                 // 超出時(shí)淘汰                 .maximumSize(10)                 //設(shè)置寫緩存后n秒鐘過期                 .expireAfterWrite(60, TimeUnit.SECONDS)                 //設(shè)置讀寫緩存后n秒鐘過期,實(shí)際很少用到,類似于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 = "";         // 先查詢r(jià)edis緩存         log.info("get data from redis");         // 當(dāng)redis緩存不存在查db         log.info("get data from 

4. Encache

Encache是一個純Java的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點(diǎn),是Hibernate中默認(rèn)的CacheProvider。同Caffeine和Guava Cache相比,Encache的功能更加豐富,擴(kuò)展性更強(qiáng):

使用代碼如下:

    org.ehcache     ehcache     3.9.7 @Slf4j public class EhcacheTest {     private static final String ORDER_CACHE = "orderCache";     public static void main(String[] args) {         CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()                 // 創(chuàng)建cache實(shí)例                 .withCache(ORDER_CACHE, CacheConfigurationBuilder                         // 聲明一個容量為20的堆內(nèi)緩存                         .newCacheConfigurationBuilder(String.class, String.class, ResourcePoolsBuilder.heap(20)))                 .build(true);         // 獲取cache實(shí)例         Cache 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 = "";         // 先查詢r(jià)edis緩存         log.info("get data from redis");         // 當(dāng)redis緩存不存在查db         log.info("get data from mysql");         info = String.format("{orderId=%s}", orderId);         return info;     } }
本地緩存問題及解決

1. 緩存一致性

兩級緩存與數(shù)據(jù)庫的數(shù)據(jù)要保持一致,一旦數(shù)據(jù)發(fā)生了修改,在修改數(shù)據(jù)庫的同時(shí),本地緩存、遠(yuǎn)程緩存應(yīng)該同步更新。

解決方案1: MQ

一般現(xiàn)在部署都是集群部署,有多個不同節(jié)點(diǎn)的本地緩存; 可以使用MQ的廣播模式,當(dāng)數(shù)據(jù)修改時(shí)向MQ發(fā)送消息,節(jié)點(diǎn)監(jiān)聽并消費(fèi)消息,刪除本地緩存,達(dá)到最終一致性;

打開網(wǎng)易新聞 查看精彩圖片

解決方案2:Canal + MQ

如果你不想在你的業(yè)務(wù)代碼發(fā)送MQ消息,還可以適用近幾年比較流行的方法:訂閱數(shù)據(jù)庫變更日志,再操作緩存。Canal 訂閱Mysql的 Binlog日志,當(dāng)發(fā)生變化時(shí)向MQ發(fā)送消息,進(jìn)而也實(shí)現(xiàn)數(shù)據(jù)一致性。

打開網(wǎng)易新聞 查看精彩圖片

2. 如何提高本地緩存命中率

參考:如何提高緩存命中率

3. 本地內(nèi)存的技術(shù)選型問題

  • 從易用性角度,Guava Cache、Caffeine和Encache都有十分成熟的接入方案,使用簡單。

  • 從功能性角度,Guava Cache和Caffeine功能類似,都是只支持堆內(nèi)緩存,Encache相比功能更為豐富

  • 從性能上進(jìn)行比較,Caffeine最優(yōu)、GuavaCache次之,Encache最差(下圖是三者的性能對比結(jié)果)

打開網(wǎng)易新聞 查看精彩圖片

對于本地緩存的方案中,我比較推薦Caffeine,性能上遙遙領(lǐng)先。

雖然Encache功能更為豐富,甚至提供了持久化和集群的功能,但是這些功能完全可以依靠其他方式實(shí)現(xiàn)。真實(shí)的業(yè)務(wù)工程中,建議使用Caffeine作為本地緩存,另外使用redis或者memcache作為分布式緩存,構(gòu)造多級緩存體系,保證性能和可靠性。

來源:https://blog.csdn.net/One_hundred_nice/article/details/123950638

公眾號“Java精選”所發(fā)表內(nèi)容注明來源的,版權(quán)歸原出處所有(無法查證版權(quán)的或者未注明出處的均來自網(wǎng)絡(luò),系轉(zhuǎn)載,轉(zhuǎn)載的目的在于傳遞更多信息,版權(quán)屬于原作者。如有侵權(quán),請聯(lián)系,筆者會第一時(shí)間刪除處理!

最近有很多人問,有沒有讀者交流群!加入方式很簡單,公眾號Java精選,回復(fù)“加群”,即可入群!

文章有幫助的話,點(diǎn)在看,轉(zhuǎn)發(fā)吧!