20 圖 |6 千字|緩存實戰(上篇)

回覆 PDF 領取資料 html

這是悟空的第 96 篇原創文章
前端

做者 | 悟空聊架構java

來源 | 悟空聊架構(ID:PassJava666)git

轉載請聯繫受權(微信ID:PassJava)

前言

先說個小事情,今天試了下作動圖,就一張動圖都花了我 1 個小時,還作得很難看。。在線求個作動圖的好軟件~本文主要內容以下:github

上一篇講到如何作性能調優的方式:《48 張圖 | 手摸手教你微服務的性能監控、壓測和調優》,好比給表加索引、動靜分離、減小沒必要要的日誌打印。但有一個很強大的優化方式沒有提到,那就是加緩存,好比查詢小程序的廣告位配置,由於沒什麼人會去頻繁的改,將廣告位配置丟到緩存裏面再適合不過了。那咱們就給開源 Spring Cloud 實戰項目 PassJava 加下緩存來提高下性能。web

我把後端前端小程序都上傳到同一個倉庫裏面了,你們能夠經過 Github碼雲訪問。地址以下:redis

Github: https://github.com/Jackson0714/PassJava-Platformspring

碼雲:https://gitee.com/jayh2018/PassJava-Platformdocker

配套教程:www.passjava.cn數據庫

在實戰以前,咱們先來看下使用緩存的原理和問題。

1、緩存

1.1 爲何要用緩存

20 年前常見的系統就是單機的,好比 ERP 系統,對性能要求不高,使用緩存的並不常見,但現現在,已經步入到互聯網時代,高併發、高可用、高性能老是被提起,而緩存在這「三高」中立下汗馬功勞

咱們經過會將部分數據放入緩存中,來提升訪問速度,而後數據庫承擔存儲的工做。

那麼哪些數據適合放入緩存中呢?

  • 即時性。例如查詢最新的物流狀態信息。

  • 數據一致性要求不高。例如門店信息,修改後,數據庫中已經改了,5 分鐘後緩存中才是最新的,但不影響功能使用。

  • 訪問量大且更新頻率不高。好比首頁的廣告信息,訪問量,可是不會常常變化。

當咱們想要查詢數據時,使用緩存的流程以下:

讀模式緩存使用流程

1.2  本地緩存

好比如今有一個需求:前端小程序須要查詢題目的類型,而題目類型放在小程序的首頁在,訪問量是很是高的,可是又不是常常變化的數據,因此能夠將題目類型數據放到緩存中。

最簡單的使用緩存的方式是使用本地緩存,也就是在內存中緩存數據,能夠用 HashMap、數組等數據結構來緩存數據。

1.2.1 不使用緩存

咱們先來看下不使用緩存的狀況:前端的請求先通過網關,而後請求到題目微服務,而後查詢數據庫,返回查詢結果。

再來看下核心代碼是怎麼樣的。

先自定義一個 Rest API 用來查詢題目類型列表,數據是從數據庫查詢出來後直接返回給前端。

@RequestMapping("/list")
public R list(){
    // 從數據庫中查詢數據
    typeEntityList = ITypeService.list(); 
    return R.ok().put("typeEntityList", typeEntityList);
}

1.2.2 使用緩存

來看下使用緩存的狀況:前端先通過網關,而後到題目微服務,先判斷緩存中有沒有數據,若是沒有,則查詢數據庫再更新緩存,最後返回查詢到的結果。

那咱們如今建立一個 HashMap 來緩存題目的類型列表:

private Map<String, Object> cache = new HashMap<>();

先獲取緩存的類型列表

List<TypeEntity> typeEntityListCache = (List<TypeEntity>) cache.get("typeEntityList");

若是緩存中沒有,則先從數據庫中獲取。固然,第一次查詢緩存時,確定是沒有這個數據的。

// 若是緩存中沒有數據
if (typeEntityListCache == null) {
  System.out.println("The cache is empty");
  // 從數據庫中查詢數據
  List<TypeEntity> typeEntityList = ITypeService.list();
  // 將數據放入緩存中
  typeEntityListCache = typeEntityList;
  cache.put("typeEntityList", typeEntityList);
}
return R.ok().put("typeEntityList", typeEntityListCache);

咱們用 Postman 工具來看下查詢結果:

請求URL:https://github.com/Jackson0714/PassJava-Platform

返回了題目類型列表,共 14 條數據。

之後再次查詢時,由於緩存中已經有該數據了,因此直接走緩存,不會再從數據庫中查詢數據了。

從上面的例子中咱們能夠知道本地緩存有哪些優勢呢?

  • 減小和數據庫的交互,下降因磁盤 I/O 引發的性能問題。
  • 避免數據庫的死鎖問題。
  • 加速相應速度。

固然,本地緩存也存在一些問題:

  • 佔用本地內存資源。
  • 機器宕機重啓後,緩存丟失。
  • 可能會存在數據庫數據和緩存數據不一致的問題。
  • 同一臺機器中的多個微服務緩存的數據不一致。
  • 集羣環境下存在緩存的數據不一致的問題。

基於本地緩存的問題,咱們引入了分佈式緩存 Redis 來解決。

2、緩存 Redis

2.1 Docker 安裝 Redis

首先須要安裝 Redis,我是經過 Docker 來安裝 Redis。另外我在 ubuntu 和 Mac M1 上都裝過 docker 版的 Redis,你們能夠參照這兩篇來安裝。

《Ubuntu 上到 Docker 安裝redis》

《M1 和 Docker 談了個戀愛...》

2.2 引入 Redis 組件

我用的是 passjava-question 微服務,因此是在 passjava-question 模塊下的配置文件 pom.xml 中引入 redis 組件。

文件路徑:/passjava-question/pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2.3 測試 Redis

咱們能夠寫一個測試方法來測試引入的 redis 是否能存數據,以及可否查出存的數據。

咱們都是使用 StringRedisTemplate 庫來操做 Redis,因此能夠自動裝載下 StringRedisTemplate

@Autowired
StringRedisTemplate stringRedisTemplate;

而後在測試方法中,測試存儲方法:ops.set(),以及 查詢方法:ops.get()

@Test
public void TestStringRedisTemplate() {
    // 初始化 redis 組件
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    // 存儲數據
    ops.set("悟空""悟空聊架構_" + UUID.randomUUID().toString());
    // 查詢數據
    String wukong = ops.get("悟空");
    System.out.println(wukong);
}

set 方法的第一個參數是 key,好比示例中的 「悟空」。

get 方法的參數也是 key。

最後打印出了 redis 中 key = 「悟空」 的緩存的值:

另外也能夠經過客戶端工具來查看,以下圖所示:

我下載的是這個軟件:Redis Desktop Manager windows,Mac M1 上正常使用。下載地址:

http://www.pc6.com/softview/SoftView_450180.html

2.4 用  Redis 改造業務邏輯

用 redis 替換 hashmap 也不難,把用到 hashmap 的地方都用 redis 改下。另外須要注意的是:

從數據庫中查詢到的數據先要序列化成 JSON 字符串後再存入到 Redis 中,從 Redis 中查詢數據時,也須要將 JSON 字符串反序列化爲對象實例。

public List<TypeEntity> getTypeEntityList() {
  // 1.初始化 redis 組件
  ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
  // 2.從緩存中查詢數據
  String typeEntityListCache = ops.get("typeEntityList");
  // 3.若是緩存中沒有數據
  if (StringUtils.isEmpty(typeEntityListCache)) {
    System.out.println("The cache is empty");
    // 4.從數據庫中查詢數據
    List<TypeEntity> typeEntityListFromDb = this.list();
    // 5.將從數據庫中查詢出的數據序列化 JSON 字符串
    typeEntityListCache = JSON.toJSONString(typeEntityListFromDb);
    // 6.將序列化後的數據存入緩存中
    ops.set("typeEntityList", typeEntityListCache);
    return typeEntityListFromDb;
  }
  // 7.若是緩存中有數據,則從緩存中拿出來,並反序列化爲實例對象
  List<TypeEntity> typeEntityList = JSON.parseObject(typeEntityListCache, new TypeReference<List<TypeEntity>>(){});
  return typeEntityList;
}

整個流程以下:

  • 1.初始化 redis 組件。

  • 2.從緩存中查詢數據。

  • 3.若是緩存中沒有數據,執行步驟 四、五、6。

  • 4.從數據庫中查詢數據。

  • 5.將從數據庫中查詢出的數據轉化爲 JSON 字符串。

  • 6.將序列化後的數據存入緩存中,並返回數據庫中查詢到的數據。

  • 7.若是緩存中有數據,則從緩存中拿出來,並反序列化爲實例對象。

2.5 測試業務邏輯

咱們仍是用 postman 工具進行測試:

經過屢次測試,第一次請求會稍微慢點,後面幾回速度很是快。說明使用緩存後性能有提高。

另外咱們用 Redis 客戶端看下結果:

Redis key = typeEntityList,Redis value 是一個 JSON 字符串,裏面的內容是題目分類列表。

3、緩存穿透、雪崩、擊穿

高併發下使用緩存會帶來的幾個問題:緩存穿透、雪崩、擊穿。

3.1 緩存穿透

3.1.1 緩存穿透的概念

緩存穿透指一個必定不存在的數據,因爲緩存未命中這條數據,就會去查詢數據庫,數據庫也沒有這條數據,因此返回結果是 null。若是每次查詢都走數據庫,則緩存就失去了意義,就像穿透了緩存同樣。

3.1.2 帶來的風險

利用不存在的數據進行攻擊,數據庫壓力增大,最終致使系統崩潰。

3.1.3 解決方案

對結果 null 進行緩存,並加入短暫的過時時間。

3.2 緩存雪崩

3.2.1 緩存雪崩的概念

緩存雪崩是指咱們緩存多條數據時,採用了相同的過時時間,好比 00:00:00 過時,若是這個時刻緩存同時失效,而有大量請求進來了,因未緩存數據,因此都去查詢數據庫了,數據庫壓力增大,最終就會致使雪崩。

3.2.2 帶來的風險

嘗試找到大量 key 同時過時的時間,在某時刻進行大量攻擊,數據庫壓力增大,最終致使系統崩潰。

3.2.3 解決方案

在原有的實效時間基礎上增長一個碎擠汁,好比 1-5 分鐘隨機,下降緩存的過時時間的重複率,避免發生緩存集體實效。

3.3 緩存擊穿

3.3.1 緩存擊穿的概念

某個 key 設置了過時時間,但在正好失效的時候,有大量請求進來了,致使請求都到數據庫查詢了。

3.3.2 解決方案

大量併發時,只讓一個請求能夠獲取到查詢數據庫的鎖,其餘請求須要等待,查到之後釋放鎖,其餘請求獲取到鎖後,先查緩存,緩存中有數據,就不用查數據庫。

4、加鎖解決緩存擊穿

怎麼處理緩存穿透、雪崩、擊穿的問題呢?

  • 對空結果進行緩存,用來解決緩存穿透問題。
  • 設置過時時間,且加上隨機值進行過時偏移,用來解決緩存雪崩問題。
  • 加鎖,解決緩存擊穿問題。另外須要注意,加鎖對性能會帶來影響。

這裏咱們來看下用代碼演示如何解決緩存擊穿問題。

咱們須要用 synchronized 來進行加鎖。固然這是本地鎖的方式,分佈式鎖咱們會在下篇講到。

public List<TypeEntity> getTypeEntityListByLock() {
  synchronized (this) {
    // 1.從緩存中查詢數據
    String typeEntityListCache = stringRedisTemplate.opsForValue().get("typeEntityList");
    if (!StringUtils.isEmpty(typeEntityListCache)) {
      // 2.若是緩存中有數據,則從緩存中拿出來,並反序列化爲實例對象,並返回結果
      List<TypeEntity> typeEntityList = JSON.parseObject(typeEntityListCache, new TypeReference<List<TypeEntity>>(){});
      return typeEntityList;
    }
    // 3.若是緩存中沒有數據,從數據庫中查詢數據
    System.out.println("The cache is empty");
    List<TypeEntity> typeEntityListFromDb = this.list();
    // 4.將從數據庫中查詢出的數據序列化 JSON 字符串
    typeEntityListCache = JSON.toJSONString(typeEntityListFromDb);
    // 5.將序列化後的數據存入緩存中,並返回數據庫查詢結果
    stringRedisTemplate.opsForValue().set("typeEntityList", typeEntityListCache, 1, TimeUnit.DAYS);
    return typeEntityListFromDb;
  }
}
  • 1.從緩存中查詢數據。

  • 2.若是緩存中有數據,則從緩存中拿出來,並反序列化爲實例對象,並返回結果。

  • 3.若是緩存中沒有數據,從數據庫中查詢數據。

  • 4.將從數據庫中查詢出的數據序列化 JSON 字符串。

  • 5.將序列化後的數據存入緩存中,並返回數據庫查詢結果。

5、本地鎖的問題

本地鎖只能鎖定當前服務的線程,以下圖所示,部署了多個題目微服務,每一個微服務用本地鎖進行加鎖。

本地鎖在通常狀況下沒什麼問題,可是在某些狀況下就會出問題:

好比在高併發狀況下用來鎖庫存就有問題了:

  • 1.好比當前總庫存爲 100,被緩存在 Redis 中。

  • 2.庫存微服務 A 用本地鎖釦減庫存 1 以後,總庫存爲 99。

  • 3.庫存微服務 B 用本地鎖釦減庫存 1 以後,總庫存爲 99。

  • 4.那庫存扣減了 2 次後,仍是 99,就超賣了 1 個

那如何解決本地加鎖的問題呢?

緩存實戰(中篇):實戰分佈式鎖。咱們下篇見!

- END -

寫了兩本 PDF, 回覆  分佈式  或  PDF  載。
個人 JVM 專欄已上架,回覆  JVM  領取

我是悟空,努力變強,變身超級賽亞人!

本文分享自微信公衆號 - 悟空聊架構(PassJava666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索