架構設計 | 快取管理模式,監控和記憶體回收策略

知了一笑發表於2020-05-26

本文原始碼:GitHub·點這裡 || GitEE·點這裡

一、快取設計

1、快取的作用

在業務系統中,查詢時最容易出現效能問題的模組,查詢面對的資料量大,篩選條件複雜,所以在系統架構中引入快取層,則是非常必要的,用來快取熱點資料,達到快速響應的目的。

快取使用的基本原則:

  • 所有快取資料,必須設定過期時間;
  • 核心業務流程不通過快取層;
  • 快取層移除,不影響現有流程;
  • 系統各個端首頁資料不實時查詢;
  • 報表資料不實時查詢載入;
  • 歸檔資料(定時統計的結果資料)不實時查詢;

這裡是業務架構中常用的快取策略,快取通過犧牲強一致性來提高效能,所以並不是所有的業務都適合用快取,實際考量都會針對具體的業務,比如使用者相關維度的資料修改頻率低,會使用快取,但是使用者許可權資料(比如:免費次數)會考慮實時校驗,快取層使用的相對較少。

2、快取設計模式

Cache-Aside模式

業務中最常用的快取層設計模式,基本實現邏輯和相關概念如下:

  • 快取命中:直接查詢快取且命中,返回資料;
  • 快取載入:查詢快取未命中,從資料庫中查詢資料,獲取資料後並載入到快取;
  • 快取失效:資料更新寫到資料庫,操作成功後,讓快取失效,查詢時候再重新載入;
  • 快取穿透:查詢資料庫不存在的物件,也就不存在快取層的命中;
  • 快取擊穿:熱點key在失效的瞬間,高併發查詢這個key,擊穿快取,直接請求資料庫;
  • 快取雪崩:快取Key大批量到過期時間,導致資料庫壓力過大;
  • 命中率:快取設計的是否合理要看命中率,命中率高說明快取有效抗住了大部分請求,命中率可以通過Redis監控資訊計算,一般來說命中率在(70-80)%都算合理。
    併發問題

執行讀操作未命中快取,然後查詢資料庫中取資料,資料已經查詢到還沒放入快取,同時一個更新寫操作讓快取失效,然後讀操作再把查詢到資料載入快取,導致快取的髒資料。

在遵守快取使用原則下出現該情況概率非常低,可以通過複雜的Paxos協議保證一致性,一般情況是不考量該場景的處理,如果快取管理過於複雜,會和快取層核心理念相悖。

基本描述程式碼:

@Service
public class KeyValueServiceImpl extends ServiceImpl<KeyValueMapper, KeyValueEntity> implements KeyValueService {

    @Resource
    private RedisService redisService ;

    @Override
    public KeyValueEntity select(Integer id) {
        // 查詢快取
        String redisKey = RedisKeyUtil.getObectKey(id) ;
        String value = redisService.get(redisKey) ;
        if (!StringUtils.isEmpty(value) && !value.equals("null")){
            return JSON.parseObject(value,KeyValueEntity.class);
        }
        // 查詢庫
        KeyValueEntity keyValueEntity = this.getById(id) ;
        if (keyValueEntity != null){
            // 快取寫入
            redisService.set(redisKey,JSON.toJSONString(keyValueEntity)) ;
        }
        // 返回值
        return keyValueEntity ;
    }

    @Override
    public boolean update(KeyValueEntity keyValueEntity) {
        // 更新資料
        boolean updateFlag = this.updateById(keyValueEntity) ;
        // 清除快取
        if (updateFlag){
            redisService.delete(RedisKeyUtil.getObectKey(keyValueEntity.getId()));
        }
        return updateFlag ;
    }
}

Read-Throug模式

當應用系統向快取系統請求資料時,如果快取中並沒有對應的資料存在,快取系統將向底層資料來源的讀取資料。如果資料在快取中存在,則直接返回快取中存在的資料。把更新資料庫的操作由快取層代勞了。

Write-Through模式

更新寫資料時,如果沒有命中快取,則直接更新資料庫,如果命中了快取,則先更新快取,然後由快取系統自行更新資料庫。

Write-Behind模式

應用系統對快取中的資料進行更新時,只更新快取,不更新資料庫,快取系統會非同步批量向底層資料來源更新資料。

二、資料一致問題

業務開發模式中,會涉及到一個問題:如何最大限度保證資料庫和Redis快取的資料一致性?

首先說明一下:資料庫和快取強一致性同步成本太高,如果追求強一致,快取層存在的價值就會很低,如上快取模式一中幾乎可以解決大部分業務場景問題。

解決這個問題的方式很多:

方案一說明:

  • 資料庫更新寫入資料成功;
  • 準備一個先進先出模式的訊息佇列;
  • 把更新的資料包裝為一個訊息放入佇列;
  • 基於訊息消費服務更新Redis快取;

分析:訊息佇列的穩定和可靠性,操作層面資料庫和快取層解耦。

方案二說明:

  • 提供一個資料庫Binlog訂閱服務,並解析修改日誌;
  • 服務獲取修改資料,並向Redis服務傳送訊息;
  • Redis資料進行修改,類似MySQL的主從同步機制;

分析:系統架構層面多出一個服務,且需要解析MySQL日誌,操作難度較大,但流程上更為合理。

總結描述

分散式架構中,快取層面的基本需求就是提高響應速度,不斷優化,追求資料庫和Redis快取的資料快速一致性,從提供的各種方案中都可以看出,這也在增加快取層面處理的複雜性,架構邏輯複雜,就容易導致程式錯誤,所以針對業務選擇合理的處理邏輯,這點很關鍵。

三、快取監控

1、Redis服務監控

通過info命令檢視Redis服務的引數資訊,可以通過傳參檢視指定分類配置。通過config..set設定具體配置引數。例如:

@Override
public Properties info(String var) {
    if (StringUtils.isEmpty(var)){
        return redisTemplate.getRequiredConnectionFactory().getConnection().info();
    }
    return redisTemplate.getRequiredConnectionFactory().getConnection().info(var);
}

傳參說明:

  • memory:記憶體消耗相關資訊
  • server:有關Redis伺服器的常規資訊
  • clients:客戶端連線部分
  • stats:一般統計
  • cpu:CPU消耗統計資訊

應用案例:

@RestController
public class MonitorController {

    @Resource
    private RedisService redisService ;

    private static final String[] monitorParam = new String[]{"memory","server","clients","stats","cpu"} ;

    @GetMapping("/monitor")
    public List<MonitorEntity> monitor (){
        List<MonitorEntity> monitorEntityList = new ArrayList<>() ;
        for (String param:monitorParam){
            Properties properties = redisService.info(param) ;
            MonitorEntity monitorEntity = new MonitorEntity () ;
            monitorEntity.setMonitorParam(param);
            monitorEntity.setProperties(properties);
            monitorEntityList.add(monitorEntity);
        }
        return monitorEntityList ;
    }

}

通過上述引數組合,把Redis相關配置引數列印出來,然後視覺化輸出,儼然一副高階的感覺。

配置引數說明:

這裡只對兩個引數說明一下,計算命中率的關鍵資訊:

  • keyspace_misses:查詢快取Key失敗的次數;
  • keyspace_hits:查詢快取Key命中的次數;

公式:命中率=命中次數/(hits+misses)查詢總次數。

2、LRU演算法說明

Redis的資料是放在記憶體中的,所以速度快,自然也就受到記憶體大小的限制,如果記憶體使用超過配置,Redis有不同的回收處理策略。

記憶體模組引數:maxmemory_policy

  • noenviction:不回收資料,查詢直接返回錯誤,但可以執行刪除;
  • allkeys-lru:從所有的資料中挑選最近最少使用的資料淘汰;
  • volatile-lru:已設定過期時間的資料中挑選最近最少使用的資料淘汰;
  • allkeys-random:從所有資料中任意選擇資料淘汰;
  • volatile-random:從已設定過期時間的資料中任意選擇資料淘汰;
  • volatile-ttl:從已設定過期時間的資料中挑選將要過期的資料淘汰;

大部分情況下,業務都是希望最熱點資料可以被快取,所以相對使用allkeys-lru策略偏多。這裡要根據業務模式特點衡量。

四、原始碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

推薦閱讀:《架構設計系列》,蘿蔔青菜,各有所需

序號 標題
01 架構基礎:單服務.叢集.分散式,基本區別和聯絡
02 架構設計:分散式業務系統中,全域性ID生成策略
03 架構設計:分散式系統排程,Zookeeper叢集化管理
04 架構設計:介面冪等性原則,防重複提交Token管理

相關文章