1. 程式人生 > 實用技巧 >Java實現本地小資料量快取嘗試與實踐&設計思考

Java實現本地小資料量快取嘗試與實踐&設計思考

話不多說先貼程式碼

/**
 * 快取工具
 */
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不可用會導致系統不可用

  • 存在擊穿風險