對於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,完美達標。
能夠看出來,合理的使用緩存對接口性能仍是有很大提高的。