天天看點

高性能的本地緩存方案選型,看這篇就夠了!

作者:老誠不bug

背景

在高性能的服務架構設計中,緩存是一個不可或缺的環節。在實際的項目中,我們通常會将一些熱點資料存儲到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           

繼續閱讀