微服務複雜查詢之快取策略

dustinny發表於2021-03-15

在上一篇 快取設計的好,服務基本不會倒 介紹了db層快取,回顧一下,db層快取主要設計可以總結為:

  • 快取只刪除不更新
  • 行記錄始終只儲存一份,即主鍵對應行記錄
  • 唯一索引僅快取主鍵值,不直接快取行記錄(參考mysql索引思想)
  • 防快取穿透設計,預設一分鐘,防止快取擊穿和雪崩
  • 不快取多行記錄

前言

在大型業務系統中,通過對持久層新增快取,對於大多數單行記錄查詢,相信快取能夠幫持久層減輕很大的訪問壓力,但在實際業務中,資料讀取不僅僅只是單行記錄,面對大量多行記錄的查詢,這對持久層也會造成不小的訪問壓力,除此之外,像秒殺系統、選課系統這種高併發的場景,單純靠持久層的快取是不現實的,本文我們來介紹 go-zero 實踐中的快取設計之biz cache

適用場景舉例

  • 選課系統
  • 內容社交系統
  • 秒殺

像這些系統,我們可以在業務層再增加一層快取來儲存系統中的關鍵資訊,如選課系統中學生選課資訊,課程剩餘名額;內容社交系統中某一段時間之間的內容資訊等。

接下來,我們以內容社交系統來進行舉例說明。

在內容社交系統中,我們一般是先查詢一批內容列表,然後點選某條內容檢視詳情,

在沒有新增biz快取前,內容資訊的查詢流程圖應該為:

redis-cache-05

從上圖以及上一篇文章 快取設計的好,服務基本不會倒 中我們可以知道,內容列表的獲取是沒辦法依賴快取的,
如果我們在業務層新增一層快取用來儲存列表中的關鍵資訊(甚至完整資訊),那麼多行記錄的訪問不再是一個問題,這就是biz redis要做的事情。 接下來我們來看一下設計方案,假設內容系統中單行記錄包含以下欄位

欄位名稱 欄位型別 備註
id string 內容id
title string 標題
content string 詳細內容
createTime time.Time 建立時間

我們的目標是獲取一批內容列表,而儘量避免內容列表走db造成訪問壓力,首先我們採用redis的sort set資料結構來儲存,根需要儲存的欄位資訊量,有兩種redis儲存方案:

  • 快取區域性資訊
    biz-redis-02
    對其關鍵欄位資訊(如:id等)按照一定規則壓縮,並儲存,score我們用createTime毫秒值(時間值相等這裡不討論),這種儲存方案的好處是節約redis儲存空間,
    那另一方面,缺點就是需要對列表詳細內容進行二次回查(但這次回查是會利用到持久層的行記錄快取的)

  • 快取完整資訊
    biz-redis-01
    對釋出的所有內容按照一定規則壓縮後均進行儲存,同樣score我們還是用createTime毫秒值,這種儲存方案的好處是業務的增、刪、查、改均走reids,而db層這時候
    就可以不用考慮行記錄快取了,持久層僅提供資料備份和恢復使用,從另一方面來看,其缺點也很明顯,需要的儲存空間、配置要求更高,費用也會隨之增大。

示例程式碼:

type Content struct {
    Id         string    `json:"id"`
    Title      string    `json:"title"`
    Content    string    `json:"content"`
    CreateTime time.Time `json:"create_time"`
}

const bizContentCacheKey = `biz#content#cache`

// AddContent 提供內容儲存
func AddContent(r redis.Redis, c *Content) error {
    v := compress(c)
    _, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v)
    return err
}

// DelContent 提供內容刪除
func DelContent(r redis.Redis, c *Content) error {
    v := compress(c)
    _, err := r.Zrem(bizContentCacheKey, v)

    return err
}

// 內容壓縮
func compress(c *Content) string {
    // todo: do it yourself
    var ret string
    return ret
}

// 內容解壓
func uncompress(v string) *Content {
	// todo: do it yourself
	var ret Content
	return &ret
}

// ListByRangeTime提供根據時間段進行資料查詢
func ListByRangeTime(r redis.Redis, start, end time.Time) ([]*Content, error) {
	kvs, err := r.ZrangebyscoreWithScores(bizContentCacheKey, start.UnixNano()/1e6, end.UnixNano()/1e6)
	if err != nil {
		return nil, err
	}

	var list []*Content
	for _, kv := range kvs {
		data := uncompress(kv.Key)
		list = append(list, data)
	}

	return list, nil
}

在以上例子中,redis是沒有設定過期時間的,我們將增、刪、改、查操作均同步到redis,我們認為內容社交系統的列表訪問請求是比較高的情況下才做這樣的方案設計,
除此之外,還有一些資料訪問,沒有像內容設計系統這麼頻繁的訪問, 可能是某一時間段內訪問量突如其來的增加,之後可能很長一段時間才會再訪問一次,以此間隔,或者說不會再訪問了,面對這種場景,我們又該如何考慮快取的設計呢?在go-zero內容實踐中,有兩種方案可以解決這種問題:

  • 增加記憶體快取:通過記憶體快取來儲存當前可能突發訪問量比較大的資料,常用的儲存方案採用map資料結構來儲存,map資料儲存實現比較簡單,但快取過期處理則需要增加定時器來處理,另一宗方案是通過go-zero庫中的 Cache ,其是專門用於記憶體快取管理。
  • 採用biz redis,並設定合理的過期時間

總結

以上兩個場景可以包含大部分的多行記錄快取,對於多行記錄查詢量不大的場景,暫時沒必要直接把biz redis放進去,可以先嚐試讓db來承擔,開發人員可以根據持久層監控及服務監控來衡量何時需要引入biz cache。

專案地址

https://github.com/tal-tech/go-zero

歡迎使用 go-zero 並 star 支援我們!

相關文章