博客 / 詳情

返回

《優化接口設計的思路》系列:第九篇—用好緩存,讓你的接口速度飛起來

一、前言

大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。

作為一名從業已達六年的老碼農,我的工作主要是開發後端Java業務系統,包括各種管理後台和小程序等。在這些項目中,我設計過單/多租户體系系統,對接過許多開放平台,也搞過消息中心這類較為複雜的應用,但幸運的是,我至今還沒有遇到過線上系統由於代碼崩潰導致資損的情況。這其中的原因有三點:一是業務系統本身並不複雜;二是我一直遵循某大廠代碼規約,在開發過程中儘可能按規約編寫代碼;三是經過多年的開發經驗積累,我成為了一名熟練工,掌握了一些實用的技巧。

前面的文章都是寫接口如何設計、接口怎麼驗權以及一些接口常用的組件,這篇寫點接口性能相關的。接口性能優化有很多途徑,比如表建索引、SQL優化、加緩存、重構代碼等等,本篇文章主要講一下我是怎麼在項目中使用緩存來提高接口響應速度的。我覺得緩存的使用主要有下面幾個方面:

  • 緩存預熱

    • 定時任務預熱:定時任務在系統低峯期預加載數據到緩存中。
    • 啓動預熱:系統啓動時預加載必要的數據到緩存中。
  • 緩存層次化

    • 多級緩存:實現本地緩存和分佈式緩存相結合,例如,先在本地緩存中查詢,如果沒有再查詢Redis等分佈式緩存,最後才查詢數據庫。
    • 熱點數據緩存:對頻繁訪問的數據進行緩存,如用户會話、熱門商品信息、高頻訪問的內容等。

緩存提高接口響應速度主要是上面這些思路,不過我不是來講概念的,那是面試要用的東西。我要講的是如何用代碼實現這些思路,把它們真正用到項目中來,水平有限,我盡力説,不喜勿噴。

由於文章經常被抄襲,開源的代碼甚至被當成收費項,所以源碼裏面不是全部代碼,有需要的同學可以留個郵箱,我給你單獨發!

二、緩存預熱:手擼一個緩存處理器

上面説了緩存預熱主要是定時任務預熱、啓動預熱,那麼我們實現這個功能的時候,一般使用ConcurrentHashMapRedis來暫存數據,然後加上SpringBoot自帶的@Scheduled定時刷新緩存就夠了。雖然這樣可以實現緩存預熱,但缺陷很多,一旦需要預熱的東西多起來就會變得越來越複雜,那麼如何實現一個好的緩存處理器呢?接着看!

1、緩存處理器設計

(1)一個好的緩存處理器應該是這樣搭建的

  1. DAL實現,產出DAO和DO對象,定義緩存領域模型
  2. 定義緩存名稱,特別關注緩存的初始化順序
  3. 編寫數據倉庫,通過模型轉換器實現數據模型到緩存模型的轉化
  4. 編寫緩存管理器,推薦繼承抽象管理器 {@link AbstractCacheManager}
  5. 根據業務需求,設計緩存數據接口(putAll,get,getCacheInfo等基礎API)
  6. 完成bean配置,最好是可插拔的註冊方式,緩存管理器和數據倉庫、擴展點服務

(2)思路分析

2、代碼實現

a. 每個處理器都有緩存名字、描述信息、緩存初始化順序等信息,所以應該定義一個接口,名字為CacheNameDomain;

CacheNameDomain.java

package com.summo.demo.cache;

public interface CacheNameDomain {

    /**
     * 緩存初始化順序,級別越低,越早被初始化
     * <p>
     * 如果緩存的加載存在一定的依賴關係,通過緩存級別控制初始化或者刷新時緩存數據的加載順序<br>
     * 級別越低,越早被初始化<br>
     * <p>
     * 如果緩存的加載沒有依賴關係,可以使用默認順序<code>Ordered.LOWEST_PRECEDENCE</code>
     *
     * @return 初始化順序
     * @see org.springframework.core.Ordered
     */
    int getOrder();

    /**
     * 緩存名稱,推薦使用英文大寫字母表示
     *
     * @return 緩存名稱
     */
    String getName();

    /**
     * 緩存描述信息,用於打印日誌
     *
     * @return 緩存描述信息
     */
    String getDescription();
}
b. 可以使用一個枚舉類將不同的緩存處理器分開,有利於管理,取名為CacheNameEnum;

CacheNameEnum.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

/**
 * @description 緩存枚舉
 */
public enum CacheNameEnum implements CacheNameDomain {
    /**
     * 系統配置緩存
     */
    SYS_CONFIG("SYS_CONFIG", "系統配置緩存", Ordered.LOWEST_PRECEDENCE),
    ;

    private String name;

    private String description;

    private int order;

    CacheNameEnum(String name, String description, int order) {
        this.name = name;
        this.description = description;
        this.order = order;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }
}
c. 緩存信息轉換工具,以便dump出更友好的緩存信息,取名為CacheMessageUtil;

CacheMessageUtil.java

package com.summo.demo.cache;


import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * @description 緩存信息轉換工具,以便dump出更友好的緩存信息
 */
public final class CacheMessageUtil {

    /** 換行符 */
    private static final char ENTERSTR  = '\n';

    /** Map 等於符號 */
    private static final char MAP_EQUAL = '=';

    /**
     * 禁用構造函數
     */
    private CacheMessageUtil() {
        // 禁用構造函數
    }

    /**
     * 緩存信息轉換工具,以便dump出更友好的緩存信息<br>
     * 對於List<?>的類型轉換
     *
     * @param cacheDatas 緩存數據列表
     * @return 緩存信息
     */
    public static String toString(List<?> cacheDatas) {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < cacheDatas.size(); i++) {
            Object object = cacheDatas.get(i);
            builder.append(object);

            if (i != cacheDatas.size() - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

    /**
     * 緩存信息轉換工具,以便dump出更友好的緩存信息<br>
     * 對於Map<String, Object>的類型轉換
     *
     * @param map 緩存數據
     * @return 緩存信息
     */
    public static String toString(Map<?, ?> map) {
        StringBuilder builder = new StringBuilder();
        int count = map.size();
        for (Iterator<?> i = map.keySet().iterator(); i.hasNext();) {
            Object name = i.next();
            count++;

            builder.append(name).append(MAP_EQUAL);
            builder.append(map.get(name));

            if (count != count - 1) {
                builder.append(ENTERSTR);
            }
        }

        return builder.toString();
    }

}
d. 每個處理器都有生命週期,如初始化、刷新、獲取處理器信息等操作,這應該也是一個接口,處理器都應該聲明這個接口,名字為CacheManager;

CacheManager.java

package com.summo.demo.cache;

import org.springframework.core.Ordered;

public interface CacheManager extends Ordered {

    /**
     * 初始化緩存
     */
    public void initCache();

    /**
     * 刷新緩存
     */
    public void refreshCache();

    /**
     * 獲取緩存的名稱
     *
     * @return 緩存名稱
     */
    public CacheNameDomain getCacheName();

    /**
     * 打印緩存信息
     */
    public void dumpCache();

    /**
     * 獲取緩存條數
     *
     * @return
     */
    public long getCacheSize();
}
e. 定義一個緩存處理器生命週期的處理器,會聲明CacheManager,做第一次的處理,也是所有處理器的父類,所以這應該是一個抽象類,名字為AbstractCacheManager;

AbstractCacheManager.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;

/**
 * @description 緩存管理抽象類,緩存管理器都要集成這個抽象類
 */
public abstract class AbstractCacheManager implements CacheManager, InitializingBean {

    /**
     * LOGGER
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractCacheManager.class);

    /**
     * 獲取可讀性好的緩存信息,用於日誌打印操作
     *
     * @return 緩存信息
     */
    protected abstract String getCacheInfo();

    /**
     * 查詢數據倉庫,並加載到緩存數據
     */
    protected abstract void loadingCache();

    /**
     * 查詢緩存大小
     *
     * @return
     */
    protected abstract long getSize();

    /**
     * @see InitializingBean#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() {
        CacheManagerRegistry.register(this);
    }

    @Override
    public void initCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start init {}", description);

        loadingCache();

        afterInitCache();

        LOGGER.info("{} end init", description);
    }

    @Override
    public void refreshCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start refresh {}", description);

        loadingCache();

        afterRefreshCache();

        LOGGER.info("{} end refresh", description);
    }

    /**
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        return getCacheName().getOrder();
    }

    @Override
    public void dumpCache() {

        String description = getCacheName().getDescription();

        LOGGER.info("start print {} {}{}", description, "\n", getCacheInfo());

        LOGGER.info("{} end print", description);
    }

    /**
     * 獲取緩存條目
     *
     * @return
     */
    @Override
    public long getCacheSize() {
        LOGGER.info("Cache Size Count: {}", getSize());
        return getSize();
    }

    /**
     * 刷新之後,其他業務處理,比如監聽器的註冊
     */
    protected void afterInitCache() {
        //有需要後續動作的緩存實現
    }

    /**
     * 刷新之後,其他業務處理,比如緩存變通通知
     */
    protected void afterRefreshCache() {
        //有需要後續動作的緩存實現
    }
}
f. 當有很多緩存處理器的時候,那麼需要一個統一註冊、統一管理的的地方,可以實現對分散在各處的緩存管理器統一維護,名字為CacheManagerRegistry;

CacheManagerRegistry.java

package com.summo.demo.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.OrderComparator;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description 緩存管理器集中註冊接口,可以實現對分散在各處的緩存管理器統一維護
 */
@Component
public final class CacheManagerRegistry implements InitializingBean {

    /**
     * LOGGER
     */
    private static final Logger logger = LoggerFactory.getLogger(CacheManagerRegistry.class);

    /**
     * 緩存管理器
     */
    private static Map<String, CacheManager> managerMap = new ConcurrentHashMap<String, CacheManager>();

    /**
     * 註冊緩存管理器
     *
     * @param cacheManager 緩存管理器
     */
    public static void register(CacheManager cacheManager) {
        String cacheName = resolveCacheName(cacheManager.getCacheName().getName());
        managerMap.put(cacheName, cacheManager);
    }

    /**
     * 刷新特定的緩存
     *
     * @param cacheName 緩存名稱
     */
    public static void refreshCache(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return;
        }

        cacheManager.refreshCache();
        cacheManager.dumpCache();
    }

    /**
     * 獲取緩存總條數
     */
    public static long getCacheSize(String cacheName) {
        CacheManager cacheManager = managerMap.get(resolveCacheName(cacheName));
        if (cacheManager == null) {
            logger.warn("cache manager is not exist,cacheName=", cacheName);
            return 0;
        }
        return cacheManager.getCacheSize();
    }

    /**
     * 獲取緩存列表
     *
     * @return 緩存列表
     */
    public static List<String> getCacheNameList() {
        List<String> cacheNameList = new ArrayList<>();
        managerMap.forEach((k, v) -> {
            cacheNameList.add(k);
        });
        return cacheNameList;
    }

    public void startup() {
        try {

            deployCompletion();

        } catch (Exception e) {

            logger.error("Cache Component Init Fail:", e);

            // 系統啓動時出現異常,不希望啓動應用
            throw new RuntimeException("啓動加載失敗", e);
        }
    }

    /**
     * 部署完成,執行緩存初始化
     */
    private void deployCompletion() {

        List<CacheManager> managers = new ArrayList<CacheManager>(managerMap.values());

        // 根據緩存級別進行排序,以此順序進行緩存的初始化
        Collections.sort(managers, new OrderComparator());

        // 打印系統啓動日誌
        logger.info("cache manager component extensions:");
        for (CacheManager cacheManager : managers) {
            String beanName = cacheManager.getClass().getSimpleName();
            logger.info(cacheManager.getCacheName().getName(), "==>", beanName);
        }

        // 初始化緩存
        for (CacheManager cacheManager : managers) {
            cacheManager.initCache();
            cacheManager.dumpCache();
        }
    }

    /**
     * 解析緩存名稱,大小寫不敏感,增強刷新的容錯能力
     *
     * @param cacheName 緩存名稱
     * @return 轉換大寫的緩存名稱
     */
    private static String resolveCacheName(String cacheName) {
        return cacheName.toUpperCase();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        startup();
    }
}

3、使用方式

項目結構如下:

這是完整的項目結構圖,具體的使用步驟如下:
step1、在CacheNameEnum中加一個業務枚舉,如 SYS_CONFIG("SYS_CONFIG", "系統配置緩存", Ordered.LOWEST_PRECEDENCE)
step2、自定義一個CacheManager繼承AbstractCacheManager,如public class SysConfigCacheManager extends AbstractCacheManager
step3、實現loadingCache()方法,這裏將你需要緩存的數據查詢出來,但注意不要將所有的數據都放在一個緩存處理器中,前面CacheNameEnum枚舉類的作用就是希望按業務分開處理;
step4、在自定義的CacheManager類中寫自己的查詢數據方法,因為不同業務的場景不同,查詢參數、數據大小、格式、類型都不一致,所以AbstractCacheManager並沒有定義統一的取數方法,沒有意義;

下面是一個完整的例子
SysConfigCacheManager.java

package com.summo.demo.cache.manager;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.summo.demo.cache.AbstractCacheManager;
import com.summo.demo.cache.CacheMessageUtil;
import com.summo.demo.cache.CacheNameDomain;
import com.summo.demo.cache.CacheNameEnum;
import org.springframework.stereotype.Component;

/**
 * 系統配置管理器
 */
@Component
public class SysConfigCacheManager extends AbstractCacheManager {

    /**
     * 加個鎖,防止出現併發問題
     */
    private static final Lock LOCK = new ReentrantLock();

    /**
     * 底層緩存組件,可以使用ConcurrentMap也可以使用Redis,推薦使用Redis
     */
    private static ConcurrentMap<String, Object> CACHE;

    @Override
    protected String getCacheInfo() {
        return CacheMessageUtil.toString(CACHE);
    }

    @Override
    protected void loadingCache() {
        LOCK.lock();
        try {
            //存儲數據,這裏就模擬一下了
            CACHE = new ConcurrentHashMap<>();
            CACHE.put("key1", "value1");
            CACHE.put("key2", "value2");
            CACHE.put("key3", "value3");
        } finally {
            LOCK.unlock();
        }

    }

    @Override
    protected long getSize() {
        return null == CACHE ? 0 : CACHE.size();
    }

    @Override
    public CacheNameDomain getCacheName() {
        return CacheNameEnum.SYS_CONFIG;
    }

    /**
     * 自定義取數方法
     *
     * @param key
     * @return
     */
    public static Object getConfigByKey(String key) {
        return CACHE.get(key);
    }
}

三、緩存層次化:使用函數式編程實現

1、先舉個例子

現有一個使用商品名稱查詢商品的需求,要求先查詢緩存,查不到則去數據庫查詢;從數據庫查詢到之後加入緩存,再查詢時繼續先查詢緩存。

(1)思路分析

可以寫一個條件判斷,偽代碼如下:

//先從緩存中查詢
String goodsInfoStr = redis.get(goodsName);
if(StringUtils.isBlank(goodsInfoStr)){
    //如果緩存中查詢為空,則去數據庫中查詢
    Goods goods = goodsMapper.queryByName(goodsName);
    //將查詢到的數據存入緩存
    goodsName.set(goodsName,JSONObject.toJSONString(goods));
    //返回商品數據
    return goods;
}else{
    //將查詢到的str轉換為對象並返回
    return JSON.parseObject(goodsInfoStr, Goods.class);
}

流程圖如下

上面這串代碼也可以實現查詢效果,看起來也不是很複雜,但是這串代碼是不可複用的,只能用在這個場景。假設在我們的系統中還有很多類似上面商品查詢的需求,那麼我們需要到處寫這樣的if(...)else{...}。作為一個程序員,不能把類似的或者重複的代碼統一起來是一件很難受的事情,所以需要對這種場景的代碼進行優化。

上面這串代碼的問題在於:入參不固定、返回值也不固定,如果僅僅是參數不固定,使用泛型即可。但最關鍵的是查詢方法也是不固定的,比如查詢商品和查詢用户肯定不是一個查詢方法吧。

所以如果我們可以把一個方法(即上面的各種查詢方法)也能當做一個參數傳入一個統一的判斷方法就好了,類似於:

/**
 * 這個方法的作用是:先執行method1方法,如果method1查詢或執行不成功,再執行method2方法
 */
public static<T> T selectCacheByTemplate(method1,method2)

想要實現上面的這種效果,就不得不提到Java8的新特性:函數式編程

2、什麼是函數式編程

在Java中有一個package:java.util.function ,裏面全部是接口,並且都被@FunctionalInterface註解所修飾。

Function分類

  • Consumer(消費):接受參數,無返回值
  • Function(函數):接受參數,有返回值
  • Operator(操作):接受參數,返回與參數同類型的值
  • Predicate(斷言):接受參數,返回boolean類型
  • Supplier(供應):無參數,有返回值

具體我就不再贅述了,可以參考:https://blog.csdn.net/hua226/article/details/124409889

3、代碼實現

核心代碼非常簡單,如下

/**
  * 緩存查詢模板
  *
  * @param cacheSelector    查詢緩存的方法
  * @param databaseSelector 數據庫查詢方法
  * @return T
  */
public static <T> T selectCacheByTemplate(Supplier<T> cacheSelector, Supplier<T> databaseSelector) {
  try {
    log.info("query data from redis ······");
    // 先查 Redis緩存
    T t = cacheSelector.get();
    if (t == null) {
      // 沒有記錄再查詢數據庫
      return databaseSelector.get();
    } else {
      return t;
    }
  } catch (Exception e) {
    // 緩存查詢出錯,則去數據庫查詢
    log.info("query data from database ······");
    return databaseSelector.get();
    }
}
這裏的Supplier<T> 就是一個加了@FunctionalInterface註解的接口。

4、使用方式

使用方式也非常簡單,如下

@Component
public class UserManager {

    @Autowired
    private CacheService cacheService;

    public Set<String> queryAuthByUserId(Long userId) {
        return BaseUtil.selectCacheByTemplate(
            //從緩存中查詢
            () -> this.cacheService.queryUserFromRedis(userId),
            //從數據庫中查詢
            () -> this.cacheService.queryUserFromDB(userId));
    }
}
這樣就可以做到先查詢Redis,查詢不到再查詢數據庫,非常簡單也非常好用,我常用於查詢一些實體信息的場景。不過這裏有一個注意的點:緩存一致性。因為有時候底層數據會變化,需要做好一致性,否則會出問題。

四、小結一下

首先,緩存確實可以提高API查詢效率,這點大家應該不會質疑,但緩存並不是萬能的,不應該將所有數據都緩存起來,應當評估數據的訪問頻率和更新頻率,以決定是否緩存。
其次,在實施緩存策略時,需要平衡緩存的開銷、複雜性和所帶來的性能提升。此外,緩存策略應該根據實際業務需求和數據特徵進行定製,不斷調整優化以適應業務發展。
最後,緩存雖好,但不要亂用哦,否則會出現令你驚喜的BUG!😇

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.