guava使用

對於Guava Cache自己就很少作介紹了,一個很是好用的本地cache lib,能夠徹底取代本身手動維護ConcurrentHashMap。mysql

背景

目前須要開發一個接口I,對性能要求有很是高的要求,TP99.9在20ms之內。初步開發後發現耗時徹底沒法知足,mysql稍微波動就超時了。sql

 

 

主要耗時在DB讀取,請求一次接口會讀取幾回配置表Entry表。而Entry表的信息更新又不頻繁,對實時性要求不高,因此想到了對DB作一個cache,理論上就能夠大幅度提高接口性能了。數據庫

 

DB表結構(這裏的代碼都是爲了演示,不過原理、流程和實際生產環境基本是一致的)緩存

複製代碼
CREATE TABLE `entry` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` int(11) NOT NULL,
  `value` varchar(50) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼

 

接口中的查詢是根據name進行select操做,此次的目的就是設計一個cache類,將DB查詢cache化。服務器

 

基礎使用

首先,天然而然的想到了最基本的guava cache的使用,以下:併發

複製代碼
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    /**
     * guava cache 緩存實體
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 緩存刷新時間
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 設置緩存個數
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 當本地緩存命沒有中時,調用load方法獲取結果並將結果緩存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }
                
                // 數據庫進行查詢
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 對外暴露的方法
     * 從緩存中取entry,沒取到就走數據庫
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }
    
}
複製代碼

這裏用了refreshAfterWrite,和expireAfterWrite區別是expireAfterWrite到期會直接刪除緩存,若是同時多個併發請求過來,這些請求都會從新去讀取DB來刷新緩存。DB速度較慢,會形成線程短暫的阻塞(相對於讀cache)。app

而refreshAfterWrite,則不會刪除cache,而是隻有一個請求線程會去真實的讀取DB,其餘請求直接返回老值。這樣能夠避免同時過時時大量請求被阻塞,提高性能。異步

可是還有一個問題,那就是更新線程仍是會被阻塞,這樣在緩存key集體過時時,可能還會使響應時間變得不知足要求。ide

 

後臺線程刷新

就像上面所說,只要刷新緩存,就必然有線程被阻塞,這個是沒法避免的。性能

雖然沒法避免線程阻塞,可是咱們能夠避免阻塞用戶線程,讓用戶無感知便可。

因此,咱們能夠把刷新線程放到後臺執行。當key過時時,有新用戶線程讀取cache時,開啓一個新線程去load DB的數據,用戶線程直接返回老的值,這樣就解決了這個問題。

代碼修改以下:

複製代碼
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    ListeningExecutorService backgroundRefreshPools =
            MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    /**
     * guava cache 緩存實體
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 緩存刷新時間
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 設置緩存個數
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 當本地緩存命沒有中時,調用load方法獲取結果並將結果緩存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }

                @Override
                // 刷新時,開啓一個新線程異步刷新,老請求直接返回舊值,防止耗時過長
                public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
                    return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
                }

                // 數據庫進行查詢
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 對外暴露的方法
     * 從緩存中取entry,沒取到就走數據庫
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }

    /**
     * 銷燬時關閉線程池
     */
    @PreDestroy
    public void destroy(){
        try {
            backgroundRefreshPools.shutdown();
        } catch (Exception e){
            log.error("thread pool showdown error!e:{}",e.getMessage());
        }

    }
}
複製代碼

 

改動就是新添加了一個backgroundRefreshPools線程池,重寫了一個reload方法。

ListeningExecutorService是guava的concurrent包裏的類,負責一些線程池相關的工做,感興趣的能夠本身去了解一下。

在reload方法裏提交一個新的線程,就能夠用這個線程來刷新cache了。

若是刷新cache沒有完成的時候有其餘線程來請求該key,則會直接返回老值。

同時,千萬不要忘記銷燬線程池。

 

初始化問題

上面兩步達到了不阻塞刷新cache的功能,可是這個前提是這些cache已經存在。

項目剛剛啓動的時候,全部的cache都是不存在的,這個時候若是大批量請求過來,一樣會被阻塞,由於沒有老的值供返回,都得等待cache的第一次load完畢。

解決這個問題的方法就是在項目啓動的過程當中,將全部的cache預先load過來,這樣用戶請求剛到服務器時就會直接讀cache,不用等待。

複製代碼
@Slf4j
@Component
public class EntryCache {

    @Autowired
    EntryMapper entryMapper;

    ListeningExecutorService backgroundRefreshPools =
            MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<>()));

    /**
     * guava cache 緩存實體
     */
    LoadingCache<String, Entry> cache = CacheBuilder.newBuilder()
            // 緩存刷新時間
            .refreshAfterWrite(10, TimeUnit.MINUTES)
            // 設置緩存個數
            .maximumSize(500)
            .build(new CacheLoader<String, Entry>() {
                @Override
                // 當本地緩存命沒有中時,調用load方法獲取結果並將結果緩存
                public Entry load(String appKey) {
                    return getEntryFromDB(appKey);
                }

                @Override
                // 刷新時,開啓一個新線程異步刷新,老請求直接返回舊值,防止耗時過長
                public ListenableFuture<Entry> reload(String key, Entry oldValue) throws Exception {
                    return backgroundRefreshPools.submit(() -> getEntryFromDB(key));
                }

                // 數據庫進行查詢
                private Entry getEntryFromDB(String name) {
                    log.info("load entry info from db!entry:{}", name);
                    return entryMapper.selectByName(name);
                }
            });

    /**
     * 對外暴露的方法
     * 從緩存中取entry,沒取到就走數據庫
     */
    public Entry getEntry(String name) throws ExecutionException {
        return cache.get(name);
    }

    /**
     * 銷燬時關閉線程池
     */
    @PreDestroy
    public void destroy(){
        try {
            backgroundRefreshPools.shutdown();
        } catch (Exception e){
            log.error("thread pool showdown error!e:{}",e.getMessage());
        }

    }

    @PostConstruct
    public void initCache() {
        log.info("init entry cache start!");
        //讀取全部記錄
        List<Entry> list = entryMapper.selectAll();

        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        for (Entry entry : list) {
            try {
                this.getEntry(entry.getName());
            } catch (Exception e) {
                log.error("init cache error!,e:{}", e.getMessage());
            }
        }
        log.info("init entry cache end!");
    }
}
複製代碼

結果

讓咱們用數據看看這個cache類的表現:

200QPS,TP99.9是9ms,完美達標。

能夠看出來,合理的使用緩存對接口性能仍是有很大提高的。

相關文章
相關標籤/搜索