前兩篇文章我們介紹了快取使用的各種最佳實踐,首先介紹了快取使用的基本姿勢,分別是如何利用go-zero自動生成的快取和邏輯程式碼中快取程式碼如何寫,接著講解了在面對快取的穿透、擊穿、雪崩等常見問題時的解決方案,最後還重點講解了如何保證快取的一致性。因為快取對於高並行服務來說實在是太重要了,所以這篇文章我們還會繼續一起學習下快取相關的知識。
當我們遇到極端熱點資料查詢的時候,這個時候就要考慮本地快取了。熱點本地快取主要部署在應用伺服器的程式碼中,用於阻擋熱點查詢對於Redis等分散式快取或者資料庫的壓力。
在我們的商城中,首頁Banner中會放一些廣告商品或者推薦商品,這些商品的資訊由運營在管理後臺錄入和變更。這些商品的請求量非常大,即使是Redis也很難扛住,所以這裡我們可以使用本地快取來進行優化。
在product庫中先建一張商品運營表product_operation,為了簡化只保留必要欄位,product_id為推廣運營的商品id,status為運營商品的狀態,status為1的時候會在首頁Banner中展示該商品。
CREATE TABLE `product_operation` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`product_id` bigint unsigned NOT NULL DEFAULT 0 COMMENT '商品id',
`status` int NOT NULL DEFAULT '1' COMMENT '運營商品狀態 0-下線 1-上線',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `ix_update_time` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品運營表';
本地快取的實現比較簡單,我們可以使用map來自己實現,在go-zero的collection中提供了Cache來實現本地快取的功能,我們直接拿來用,重複造輪子從來不是一個明智的選擇,localCacheExpire為本地快取過期時間,Cache提供了Get和Set方法,使用非常簡單
localCache, err := collection.NewCache(localCacheExpire)
先從本地快取中查詢,如果命中快取則直接返回。沒有命中快取的話需要先從資料庫中查詢運營位商品id,然後再聚合商品資訊,最後回塞到本地快取中。詳細程式碼邏輯如下:
func (l *OperationProductsLogic) OperationProducts(in *product.OperationProductsRequest) (*product.OperationProductsResponse, error) {
opProducts, ok := l.svcCtx.LocalCache.Get(operationProductsKey)
if ok {
return &product.OperationProductsResponse{Products: opProducts.([]*product.ProductItem)}, nil
}
pos, err := l.svcCtx.OperationModel.OperationProducts(l.ctx, validStatus)
if err != nil {
return nil, err
}
var pids []int64
for _, p := range pos {
pids = append(pids, p.ProductId)
}
products, err := l.productListLogic.productsByIds(l.ctx, pids)
if err != nil {
return nil, err
}
var pItems []*product.ProductItem
for _, p := range products {
pItems = append(pItems, &product.ProductItem{
ProductId: p.Id,
Name: p.Name,
})
}
l.svcCtx.LocalCache.Set(operationProductsKey, pItems)
return &product.OperationProductsResponse{Products: pItems}, nil
}
使用grpurl偵錯工具請求介面,第一次請求cache miss後,後面的請求都會命中本地快取,等到本地快取過期後又會重新回源db載入資料到本地快取中
~ grpcurl -plaintext -d '{}' 127.0.0.1:8081 product.Product.OperationProducts
{
"products": [
{
"productId": "32",
"name": "電風扇6"
},
{
"productId": "31",
"name": "電風扇5"
},
{
"productId": "33",
"name": "電風扇7"
}
]
}
注意,並不是所有資訊都適用於本地快取,本地快取的特點是請求量超高,同時業務上能夠允許一定的不一致,因為本地快取一般不會主動做更新操作,需要等到過期後重新回源db後再更新。所以在業務中要視情況而定看是否需要使用本地快取。
首頁Banner場景是由運營人員來設定的,也就是我們能提前知道可能產生的熱點資料,但有些情況我們是不能提前預知資料會成為熱點的。所以就需要我們能自適應地自動的識別這些熱點資料,然後把這些資料提升為本地快取。
我們維護一個滑動視窗,比如滑動視窗設定為10s,就是要統計這10s內有哪些key被高頻存取,一個滑動視窗中對應多個Bucket,每個Bucket中對應一個map,map的key為商品的id,value為商品對應的請求次數。接著我們可以定時的(比如10s)去統計當前所有Buckets中的key的資料,然後把這些資料匯入到大頂堆中,輕而易舉的可以從大頂堆中獲取topK的key,我們可以設定一個閾值,比如在一個滑動視窗時間內某一個key存取頻次超過500次,就認為該key為熱點key,從而自動地把該key升級為本地快取。
下面介紹一些快取使用的小技巧
本篇文章介紹瞭如何使用本地熱點快取應對超高的請求,熱點快取又分為已知的熱點快取和未知的熱點快取。已知的熱點快取比較簡單,從資料庫中提前載入到記憶體中即可,未知的熱點快取我們需要自適應的識別出熱點的資料,然後把這些熱點的資料升級為本地快取。最後介紹了一些實際生產中快取使用的一些小技巧,在生產環境中要活靈活用盡量避免問題的產生。
希望本篇文章對你有所幫助,謝謝。
每週一、週四更新
程式碼倉庫: https://github.com/zhoushuguang/lebron
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支援我們!
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維條碼。