我所理解的Redis系列·第a篇·基於 Redis 優化訂單統計接口的解決方案

語言: CN / TW / HK

「這是我參與2022首次更文挑戰的第4天,活動詳情查看:2022首次更文挑戰

1. 場景描述

在客户性能壓測現場支持時,遇到一個問題:用户訂單狀態統計接口響應時間(RT, Response Time)較長,成為項目整體性能的瓶頸,亟需優化。 image.png 用户訂單狀態統計接口是標品提供的能力,由於其業務複雜性及數據存儲結構的特殊性(具體內容下文描述),進行了多次 ES 的查詢,導致 RT 較長,影響整體性能。

1.1 業務邏輯描述

標品原有的存儲及查詢邏輯是這樣:

  • 用户下單,RDS 進行持久化
  • DTS 監聽 bin log 變動,經由 Kafka 發送訂單變更消息
  • 交易中心監聽並消費 Kafka 消息,將訂單寫入 ES
  • 當觸點端進行查詢時,交易中心直接查詢 ES 的訂單數據返回

整體鏈路時序圖如下:

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 變更訂單狀態 Trade Center ->> RDS: 持久化訂單信息 RDS ->> Trade Center: 持久化完成 Trade Center ->> Touch Side: 訂單狀態變更完成

    Note right of RDS: 訂單異步寫入 ES 邏輯
RDS ->> DTS: 訂單變動信息
DTS ->> Kafka: 投遞訂單變動消息
Kafka ->> Trade Center: 訂閲訂單變動消息
Trade Center ->> ES: 同步訂單信息

    Note right of Touch Side: 觸點端查詢訂單統計信息邏輯
    Touch Side ->> Trade Center: 查詢訂單統計信息
    Trade Center ->> ES: 查詢 ES
    ES ->> Trade Center: 返回 ES 訂單信息
    Trade Center ->> Touch Side: 返回訂單統計信息

```

1.2 核心問題

在標品原有的業務邏輯中,有這樣兩個問題是比較麻煩的:

1.2.1 訂單狀態映射關係

第一個是訂單狀態映射關係的複雜性,觸點端需要展示的訂單狀態有:待支付、待發貨、待收貨、已完成。這些觸點端訂單狀態是由多個持久化字段的狀態共同決定的,其映射關係如下表:

| 觸點端訂單狀態 | 持久化字段需滿足的狀態 | | | | -------------- | ---------------------- | -------- | -------- | | | 支付狀態 | 發貨狀態 | 收貨狀態 | | 待支付 | 待支付 | | | | 待發貨 | 已支付 | 待發貨 | | | 待收貨 | 已支付 | 已發貨 | 未收貨 | | 已完成 | | | 已收貨 |

Ps:私以為已完成狀態的判斷條件有待商榷,比如已完成,只要收貨狀態是已收貨就可以嗎?嚴格來説,已完成的狀態應該同時滿足:支付狀態為已支付,發貨狀態為已發貨,收貨狀態為已收貨。不應該有“收貨狀態為已收貨時必定意味着已支付、已發貨”這樣的約定,當約定太多時,可能會發生很多意料之外的情況。

1.2.2 ES 數據存儲結構

交易中心在向 ES 寫入數據時,其結構與 RDS 持久化的數據結構是一致的,也就是説,查詢 ES 無法直接獲得觸點端需要的訂單狀態,這也是在查詢訂單統計結果需要多次查詢 ES 的原因。

由於 ES 中存儲的數據與 RDS 持久化的數據結構,我們可以把 ES 看成是一個更快的 MySQL,當我們查詢用户 id=1 的待收貨訂單數量時,SQL 語句如下: sql select count(*) from order where user_id = 1 and `支付狀態`='已支付' and `發貨狀態`='已發貨' and `收貨狀態`='未收貨';

而如果我們需要同時查詢到四種訂單狀態的數量,那麼只能通過四次查詢獲得: ``sql -- 待支付 select count(*) from order where user_id = 1 and支付狀態`='待支付';

-- 待發貨 select count(*) from order where user_id = 1 and 支付狀態='已支付' and 發貨狀態='待發貨';

-- 待收貨 select count(*) from order where user_id = 1 and 支付狀態='已支付' and 發貨狀態='已發貨' and 收貨狀態='未收貨';

-- 已完成 select count(*) from order where user_id = 1 and 支付狀態='已支付' and 發貨狀態='已發貨' and 收貨狀態='已收貨'; ```

這就是整個用户訂單統計結果接口 RT 較長的原因。

2. 解決方案

基於上述的業務及核心問題分析,我們提出了以下幾種解法:

2.1 修改 ES 存儲數據結構

每條數據新增一個訂單狀態字段,然後再寫入 ES。

這樣就將原本要N次的查詢減少到了1次,並且僅需要將訂單狀態作為查詢條件即可。但是,基於種種無法描述的原因,這個方案被放棄了(主要的原因還是儘量不對標品做大改動)。

2.2 新增緩存機制

在摒棄了第一種方案之後,我們又提出第二種解決方案:緩存,對用户訂單統計結果做緩存。

這樣就不需要每次都對 ES 進行N次查詢然後再組裝結果集進行返回了。但是,緩存的準確性和健壯性是一個需要考慮的問題:

  • 如何在異步場景下保證數據的準確性,不被例如重複消費的場景影響緩存數據?
  • 如何讓緩存數據具備健壯性?即假設緩存有誤,如何在沒有後台干預的情況下讓緩存數據能夠自我修正?
  • 如何在特定場景下使得緩存能夠持續命中(如性能壓測)?
  • ……

這些都是在設計緩存機制時需要考慮的問題。

3. 詳細設計

3.1 緩存流程設計

在確定了基於緩存對本場景進行優化的方向之後,首先需要確定的是優化後整體的業務流程,前面幾個步驟不變,還是:

  • 用户下單,RDS 進行持久化
  • DTS 監聽 bin log 變動,經由 Kafka 發送訂單變更消息
  • 交易中心監聽並消費 Kafka 消息,將訂單寫入 ES

新增了同步緩存的步驟:

  • 交易中心將訂單信息同步至緩存

當觸點端進行訂單統計查詢時,首先查詢緩存,需要分情況討論:

  • 第一次查詢(緩存未命中)
  • 交易中心直接查詢 ES 的訂單數據
  • 將查詢結果緩存,緩存分兩類
    • 用户訂單統計結果緩存:緩存了各狀態訂單數量
    • 用户訂單ID緩存:緩存了各狀態訂單ID值
  • 返回查詢結果至觸點端
  • 第N次查詢(緩存命中),這裏也需要分情況討論
  • 兩類緩存一致
    • 返回查詢結果至觸點端
  • 兩類緩存不一致
    • 交易中心直接查詢 ES 的訂單數據
    • 修正緩存,將查詢結果更新至緩存
    • 返回查詢結果至觸點端

除此之外,我們還在用户查詢指定狀態所有訂單時,對緩存進行準確性校驗及補償:

  • 觸點端查詢指定狀態所有訂單
  • 交易中心查詢 ES 中指定狀態所有訂單
  • 判斷與緩存中數據是否一致
  • 若不一致,對緩存中的數據進行補償更新
  • 返回指定狀態所有訂單至觸點端

```mermaid

sequenceDiagram autonumber Touch Side ->> Trade Center: 變更訂單狀態 Trade Center ->> RDS: 持久化訂單信息 RDS ->> DTS: 訂單變動信息 DTS ->> Kafka: 投遞訂單變動消息 Kafka ->> Trade Center: 訂閲訂單變動消息 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 同步用户訂單信息緩存

    Note right of Touch Side: 觸點端發起訂單統計查詢

    Touch Side ->> Trade Center: 發起訂單統計查詢請求
    Trade Center ->> Redis: 查詢用户訂單統計信息緩存及用户訂單ID緩存
    Redis ->> Trade Center: 緩存命中
    Trade Center ->> Trade Center: 緩存數據一致性校驗
    Trade Center ->> Touch Side: 校驗通過,返回訂單統計結果

    Note right of Trade Center: 緩存未命中或緩存數據不一致的場景

    Trade Center -->> ES: 查詢準確的訂單信息
    Trade Center -->> Redis: 數據補償及修正
    Trade Center -->> Touch Side: 返回訂單統計結果


    Note right of Touch Side: 觸點端發起查詢指定狀態所有訂單請求

    Touch Side ->> Trade Center: 發起查詢指定狀態所有訂單請求
    Trade Center ->> ES: 查詢指定狀態所有訂單
    Trade Center ->> Redis: 查詢緩存信息
    Trade Center ->> Trade Center: 緩存數據準確性校驗
    Trade Center ->> Touch Side: 緩存準確性校驗通過,返回指定狀態所有訂單

    Note right of Trade Center: 緩存準確性校驗未通過的場景

    Trade Center -->> Redis: 緩存數據補償修正
    Trade Center -->> Touch Side: 返回指定狀態所有訂單

```

3.2 Redis 數據結構設計

在客户的項目中使用的緩存中間件是 Redis。

在緩存設計中提到,我們會將緩存分為兩類,其含義及數據結構如下:

  • 用户訂單統計結果緩存:緩存了各狀態訂單數量
  • Redis 數據結構(Hash)
  • 鍵:UserOrderCountCache:userId
  • 值:(訂單狀態:統計數量結果)
  • 用户訂單ID緩存:緩存了各狀態訂單ID值
  • Redis 數據結構(Zset)
  • 鍵:UserOrderDetailCache:userId:訂單狀態
  • 值:(訂單ID)

3.3 補償機制設計

基於對緩存的不信任,我們設計了兩種補償機制。

3.3.1 主動補償

  • 查詢訂單統計信息(Redis 緩存內部修正)

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 發送訂單統計查詢請求 Trade Center ->> ES: 第1次查詢 ES ->> Trade Center: 返回訂單統計查詢結果 Trade Center ->> Redis: 第N次查詢 Redis ->> Trade Center: 訂單ID緩存與訂單統計結果緩存校驗 Trade Center ->> Touch Side: 兩類緩存一致,返回查詢結果 Note right of Trade Center: 兩類緩存不一致的操作 Trade Center -->> ES: 查詢用户所有訂單 Trade Center -->> Redis: 數據補償,更新緩存 Note right of Redis: 更新訂單ID緩存及訂單統計結果緩存

    Trade Center -->> Touch Side: 補償更新緩存結束,返回訂單統計查詢結果

```

  • 查詢指定狀態所有訂單(懶加載主動補償修正)

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 發送查詢指定狀態所有訂單請求 Trade Center ->> ES: 查詢指定訂單狀態的訂單信息 Trade Center ->> Redis: 查詢用户訂單ID緩存與統計結果緩存 Trade Center ->> Trade Center: 緩存準確性校驗 Trade Center ->> Touch Side: 緩存準確,返回查詢結果 Note right of Trade Center: 緩存不準確的操作 Trade Center -->> Redis: 數據補償,更新緩存 Note right of Redis: 更新訂單ID緩存及訂單統計結果緩存

    Trade Center -->> Touch Side: 補償更新緩存結束,返回查詢指定狀態所有訂單結果

```

3.3.2 定時補償

基於定時任務,進行兩類緩存的一致性判斷,若不一致查詢 ES 進行緩存補償更新。

```mermaid sequenceDiagram autonumber Schedule ->> Trade Center: 定時任務啟動 Trade Center ->> Redis: 查詢緩存 Redis ->> Trade Center: 用户訂單ID緩存與統計結果緩存校驗 Trade Center ->> Schedule: 兩類緩存一致,定時任務結束 Note right of Trade Center: 兩類緩存不一致的操作 Trade Center -->> ES: 查詢用户所有訂單 Trade Center -->> Redis: 數據補償,更新緩存 Note right of Redis: 更新訂單ID緩存及訂單統計結果緩存

    Trade Center -->> Schedule: 補償更新緩存結束,定時任務結束

```

3.4 論證及説明

3.4.1 兩類緩存是否過度設計

設計兩類緩存的目的是為了防止重複消費

假設有一條用户下單的消息被重複消費了,如果只設計了一種緩存(訂單統計結果),那麼在消費 Kafka 消息更新緩存數據時,訂單統計結果將會+2,導致緩存數據異常。

而如果按照現在的設計有兩種緩存時,就算用户訂單統計結果緩存中數據異常,但是由於用户訂單ID緩存存儲的是訂單 ID,所以不會產生重複數據,能夠再用户進行下一次查詢時識別到本次命中的緩存數據是錯誤的,從而走 ES 的查詢。

3.4.2 對緩存進行計算是否合理

可能有人會説,對緩存結果進行計算是不安全的,假設不是重複消費而是在消費消息時在某種情況下導致 ES 中同步了訂單信息,但緩存中數據丟失的情況怎麼辦?此時兩類緩存的數據是一致的,並不會走 ES 查詢,這就導致了向觸點端返回了錯誤的數據。

確實,是存在這種可能的,所以當觸點端查詢指定狀態所有訂單時(如下圖的查詢所有代付款訂單),我們追加了一種主動補償機制。 image.png

這種主動補償機制是基於查詢指定狀態所有訂單的結果一定是準確的,其工作原理是在得到查詢結果後並不直接返回,而是對緩存數據進行一次查詢並校驗準確性,如查詢結果與緩存不一致,説明緩存數據是錯誤的,需要對緩存進行補償修正。

4. 主要場景時序圖

本部分時序圖主要是為了枚舉出所有業務場景,將交易中心基於各種場景需要對緩存進行的操作一一列舉。

4.1 用户下單(創建訂單)

省略 DTS/Kafka 等非核心節點

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 創建訂單 Trade Center ->> RDS: 持久化訂單信息 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 更新緩存信息 Note right of Redis: 統計結果緩存(待支付)+1 Note right of Redis: 訂單明細緩存(待支付)增加OrderId

    Trade Center ->> Touch Side: 返回下單結果

```

4.2 用户支付(訂單狀態變更)

省略 DTS/Kafka 等非核心節點

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 用户支付 Trade Center ->> RDS: 更新訂單狀態 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 更新緩存信息 Note right of Redis: 統計結果緩存(待支付)-1,(待發貨)+1 Note right of Redis: 訂單明細緩存(待支付)刪除OrderId,(待發貨)增加OrderId

    Trade Center ->> Touch Side: 返回支付結果

```

4.3 商家發貨(訂單狀態變更)

省略 DTS/Kafka 等非核心節點

```mermaid sequenceDiagram autonumber Management Platform ->> Trade Center: 商家發貨 Trade Center ->> RDS: 更新訂單狀態 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 更新緩存信息 Note right of Redis: 統計結果緩存(待發貨)-1,(待收貨)+1 Note right of Redis: 訂單明細緩存(待發貨)刪除OrderId,(待收貨)增加OrderId

    Trade Center ->> Management Platform: 返回發貨結果

```

4.4 用户確認收貨(訂單狀態變更)

省略 DTS/Kafka 等非核心節點

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 用户確認收貨 Trade Center ->> RDS: 更新訂單狀態 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 更新緩存信息 Note right of Redis: 統計結果緩存(待收貨)-1,(已完成)+1 Note right of Redis: 訂單明細緩存(待收貨)刪除OrderId,(已完成)增加OrderId

    Trade Center ->> Touch Side: 返回確認收貨結果

```

4.5 用户退貨(取消訂單)

省略 DTS/Kafka 等非核心節點

```mermaid sequenceDiagram autonumber Touch Side ->> Trade Center: 用户取消訂單 Trade Center ->> RDS: 更新訂單狀態 Trade Center ->> ES: 同步訂單信息 Trade Center ->> Redis: 更新緩存信息 Note right of Redis: 統計結果緩存(原訂單狀態)-1 Note right of Redis: 訂單明細緩存(原訂單狀態)刪除OrderId

    Trade Center ->> Touch Side: 返回取消訂單結果

```

5. 兜底方案

  • 定時任務:定時補償修正所有緩存
  • 開放補償接口:主動調用補償接口修正(指定/所有)緩存
  • 以批任務的形式防止出現慢 SQL 打爆數據庫導致服務不可用

至此,本次分享到此結束,感謝。最後,本文收錄於個人語雀知識庫: 我所理解的後端技術,歡迎來訪。