在上一篇 緩存設計的好,服務基本不會倒 介紹了db層緩存,回顧一下,db層緩存主要設計可以總結為:
- 緩存只刪除不更新
- 行記錄始終只存儲一份,即主鍵對應行記錄
- 唯一索引僅緩存主鍵值,不直接緩存行記錄(參考mysql索引思想)
- 防緩存穿透設計,默認一分鍾,防止緩存擊穿和雪崩
- 不緩存多行記錄
前言
在大型業務系統中,通過對持久層添加緩存,對於大多數單行記錄查詢,相信緩存能夠幫持久層減輕很大的訪問壓力,但在實際業務中,數據讀取不僅僅只是單行記錄,面對大量多行記錄的查詢,這對持久層也會造成不小的訪問壓力,除此之外,像秒殺系統、選課系統這種高並發的場景,單純靠持久層的緩存是不現實的,本文我們來介紹 go-zero 實踐中的緩存設計之biz cache。
適用場景舉例
- 選課系統
- 內容社交系統
- 秒殺
像這些系統,我們可以在業務層再增加一層緩存來存儲系統中的關鍵信息,如選課系統中學生選課信息,課程剩余名額;內容社交系統中某一段時間之間的內容信息等。
接下來,我們以內容社交系統來進行舉例說明。
在內容社交系統中,我們一般是先查詢一批內容列表,然后點擊某條內容查看詳情,
在沒有添加biz緩存前,內容信息的查詢流程圖應該為:
從上圖以及上一篇文章 緩存設計的好,服務基本不會倒 中我們可以知道,內容列表的獲取是沒辦法依賴緩存的,
如果我們在業務層添加一層緩存用來存儲列表中的關鍵信息(甚至完整信息),那么多行記錄的訪問不再是一個問題,這就是biz redis要做的事情。 接下來我們來看一下設計方案,假設內容系統中單行記錄包含以下字段
字段名稱 | 字段類型 | 備注 |
---|---|---|
id | string | 內容id |
title | string | 標題 |
content | string | 詳細內容 |
createTime | time.Time | 創建時間 |
我們的目標是獲取一批內容列表,而盡量避免內容列表走db造成訪問壓力,首先我們采用redis的sort set數據結構來存儲,根需要存儲的字段信息量,有兩種redis存儲方案:
-
緩存局部信息
對其關鍵字段信息(如:id等)按照一定規則壓縮,並存儲,score我們用createTime
毫秒值(時間值相等這里不討論),這種存儲方案的好處是節約redis存儲空間,
那另一方面,缺點就是需要對列表詳細內容進行二次回查(但這次回查是會利用到持久層的行記錄緩存的) -
緩存完整信息
對發布的所有內容按照一定規則壓縮后均進行存儲,同樣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 支持我們!