go-zero微服務實戰系列(七、請求量這麼高該如何優化)

前兩篇文章我們介紹了緩存使用的各種最佳實踐,首先介紹了緩存使用的基本姿勢,分別是如何利用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升級爲本地緩存。

緩存使用技巧

下面介紹一些緩存使用的小技巧

  • key的命名要儘量易讀,即見名知意,在易讀的前提下長度要儘可能的小,以減少資源的佔用,對於value來說可以用int就儘量不要用string,對於小於N的value,redis內部有shared_object緩存。
  • 在redis使用hash的情況下進行key的拆分,同一個hash key會落到同一個redis節點,hash過大的情況下會導致內存以及請求分佈的不均勻,考慮對hash進行拆分爲小的hash,使得節點內存均勻避免單節點請求熱點。
  • 爲了避免不存在的數據請求,導致每次請求都緩存miss直接打到數據庫中,進行空緩存的設置。
  • 緩存中需要存對象的時候,序列化儘量使用protobuf,儘可能減少數據大小。
  • 新增數據的時候要保證緩存務必存在的情況下再去操作新增,使用Expire來判斷緩存是否存在。
  • 對於存儲每日登錄場景的需求,可以使用BITSET,爲了避免單個BITSET過大或者熱點,可以進行sharding。
  • 在使用sorted set的時候,避免使用zrange或者zrevrange返回過大的集合,複雜度較高。
  • 在進行緩存操作的時候儘量使用PIPELINE,但也要注意避免集合過大。
  • 避免超大的value。
  • 緩存儘量要設置過期時間。
  • 慎用全量操作命令,比如Hash類型的HGETALL、Set類型的SMEMBERS等,這些操作會對Hash和Set的底層數據結構進行全量掃描,如果數據量較多的話,會阻塞Redis主線程。
  • 獲取集合類型的全量數據可以使用SSCAN、HSCAN等命令分批返回集合中的數據,減少對主線程的阻塞。
  • 慎用MONITOR命令,MONITOR命令會把監控到的內容持續寫入輸出緩衝區,如果線上命令操作很多,輸出緩衝區很快就會溢出,會對Redis性能造成影響。
  • 生產環境禁用KEYS、FLUSHALL、FLUSHDB等命令。

結束語

本篇文章介紹瞭如何使用本地熱點緩存應對超高的請求,熱點緩存又分爲已知的熱點緩存和未知的熱點緩存。已知的熱點緩存比較簡單,從數據庫中提前加載到內存中即可,未知的熱點緩存我們需要自適應的識別出熱點的數據,然後把這些熱點的數據升級爲本地緩存。最後介紹了一些實際生產中緩存使用的一些小技巧,在生產環境中要活靈活用盡量避免問題的產生。

希望本篇文章對你有所幫助,謝謝。

每週一、週四更新

代碼倉庫: https://github.com/zhoushuguang/lebron

項目地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支持我們!

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章