Java實現本地小資料量快取嘗試與實踐&設計思考
阿新 • • 發佈:2020-11-16
話不多說先貼程式碼
/** * 快取工具 */ public class ConcurrentHashMapCacheUtils{ /** * 當前快取個數 */ public static Integer CURRENT_SIZE = 0; /** * 時間一分鐘 */ static final Long ONE_MINUTE = 60 * 1000L; /** * 快取超時 */ private static final Long TTL_TIME = 60 * 1000L; /** * 快取物件 */ private static final ConcurrentHashMap<String, CacheObj> CACHE_OBJECT_MAP = new ConcurrentHashMap<>(); /** * 清理過期快取是否在執行 */ private static volatile Boolean CLEAN_THREAD_IS_RUN = false; /** * 設定快取 */ public static void setCache(String cacheKey, String cacheValue, long cacheTime) { Long ttlTime = null; if (cacheTime <= 0L) { if (cacheTime == -1L) { ttlTime = -1L; } else { return; } } CURRENT_SIZE = CURRENT_SIZE + 1; if (ttlTime == null) { ttlTime = System.currentTimeMillis() + cacheTime; } CacheObj cacheObj = new CacheObj(cacheValue, ttlTime); CACHE_OBJECT_MAP.put(cacheKey, cacheObj); } /** * 設定快取 */ public static void setCache(String cacheKey, String cacheValue) { setCache(cacheKey, cacheValue, TTL_TIME); } public static long getCurrentSize(){ return CACHE_OBJECT_MAP.mappingCount(); } public static List<String> getRecentApp(){ List<String> list = new ArrayList<>(16); for (String key:CACHE_OBJECT_MAP.keySet()){ list.add(key); } return list; } /** * 獲取快取 */ public static String getCache(String cacheKey) { startCleanThread(); if (checkCache(cacheKey)) { return CACHE_OBJECT_MAP.get(cacheKey).getCacheValue(); } return null; } /** * 刪除某個快取 */ public static void deleteCache(String cacheKey) { Object cacheValue = CACHE_OBJECT_MAP.remove(cacheKey); if (cacheValue != null) { CURRENT_SIZE = CURRENT_SIZE - 1; } } /** * 判斷快取在不在,過沒過期 */ private static boolean checkCache(String cacheKey) { CacheObj cacheObj = CACHE_OBJECT_MAP.get(cacheKey); if (cacheObj == null) { return false; } if (cacheObj.getTtlTime() == -1L) { return true; } if (cacheObj.getTtlTime() < System.currentTimeMillis()) { deleteCache(cacheKey); return false; } return true; } /** * 刪除過期的快取 */ static void deleteTimeOut() { List<String> deleteKeyList = new LinkedList<>(); for(Map.Entry<String, CacheObj> entry : CACHE_OBJECT_MAP.entrySet()) { if (entry.getValue().getTtlTime() < System.currentTimeMillis() && entry.getValue().getTtlTime() != -1L) { deleteKeyList.add(entry.getKey()); } } for (String deleteKey : deleteKeyList) { deleteCache(deleteKey); } } /** * 設定清理執行緒的執行狀態為正在執行 */ static void setCleanThreadRun() { CLEAN_THREAD_IS_RUN = true; } /** * 開啟清理過期快取的執行緒 */ private static void startCleanThread() { if (!CLEAN_THREAD_IS_RUN) { ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNamePrefix("clean-cache-pool-").build(); ThreadPoolExecutor cleanThreadPool = new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(8), namedThreadFactory ); cleanThreadPool.execute(new CleanTimeOutThread()); } } } class CacheObj { /** * 快取物件 */ private String cacheValue; /** * 快取過期時間 */ private Long ttlTime; CacheObj(String cacheValue, Long ttlTime) { this.cacheValue = cacheValue; this.ttlTime = ttlTime; } String getCacheValue() { return cacheValue; } Long getTtlTime() { return ttlTime; } @Override public String toString() { return "CacheObj {" + "cacheValue = " + cacheValue + ", ttlTime = " + ttlTime + '}'; } } /** * 每一分鐘清理一次過期快取 */ class CleanTimeOutThread implements Runnable{ private static Logger logger = LoggerFactory.getLogger(CleanTimeOutThread.class); @Override public void run() { ConcurrentHashMapCacheUtils.setCleanThreadRun(); while (true) { ConcurrentHashMapCacheUtils.deleteTimeOut(); try { Thread.sleep(ConcurrentHashMapCacheUtils.ONE_MINUTE); } catch (InterruptedException e) { logger.error("Time-out Cache has not been cleaned!{}", e.getMessage()); } if(1==2){ break; } } } }
1、背景
在公司對某個開源元件的使用中,頻繁出現客戶端無法請求到資料的情況,經排查是發生了併發數過大資料庫效能瓶頸的情況。
於是有了對服務端的優化喝如下的思考。
2、設計思考
2.1、是否選擇快取
直接查詢DB還是新增快取,這個取決於系統的併發數,如果系統併發數資料庫效能足以支援,則無使用快取的必要。
如果選擇使用快取,則需要面對的一個風險是:
服務啟動/重啟的瞬間會出現大量對於資料庫的請求,容易發生快取的擊穿/雪崩情況。
關於這種情況我做了專門的優化來避免出現快取擊穿/雪崩,這一段的程式碼後面優化後再上。
2.2、快取種類的選擇
2.2.1、Java記憶體
優點:
-
速度快
-
無額外網路開銷
-
系統複雜度低
缺點:
-
受限於熱點資料數量,對應用記憶體大小有要求
-
大量快取同時失效會發生雪崩導致服務效能瞬間下降
-
存在擊穿風險
-
多例項存在快取一致性問題,可能出現對一條資料的重複查詢
2.2.2、redis
優點:
-
支援大量資料快取,擴充套件性好
-
在多例項時不需要考慮快取一致性問題
缺點:
-
系統依賴redis,如果redis不可用會導致系統不可用
-
存在擊穿風險