在 Go 項目中使用 Redis 的幾個實用建議

今天來聊一聊 Redis,主要是聊一些在 Go 項目中使用 go-redis 代碼上的一些建議。

在上代碼之前我還是要廢話幾句,在大家開發需求用到 Redis 時一定要多想個兩分鐘 "我是不是把 Redis 當數據庫用了?" 因爲數據在數據庫和 Redis 裏存兩份就就得考慮它們的一致性怎麼維護,賊麻煩,而這個一致性不做上線後還經常會出 BUG,所以不是必要我一般不用 Redis。

需要過期的數據肯定是要存 Redis 的,比如用戶的 token 之類的數據,否則存在數據庫裏還得寫定時任務來實現 token 過期刪除的功能 。

PS:Token 別用 JWT,最好自己實現一套,後面會跟大家聊一些這方面的經驗。

Redis 的應用場景還有挺多,我之前還專門寫過一篇文章 Redis 應用場景彙總,裏面羅列了十幾個場景,大家有興趣的可以看一下。

Redis 客戶端的初始化

Redis 客戶端的初始化,這個我建議還是在做好的 Redis 分層裏通過 Go 自帶的 init 函數來實現初始化,別在整個項目的 main 方法裏一個個調用自己定製化的 InitRedis 之類的方法去實現。

這個有人問爲什麼? 很簡單因爲 Go 的那些個 init 函數是在 main 方法之前執行的,就是被設計用來做初始化工作的。而且我們也不必擔心初始化順序的問題,被依賴地最深層次的包會最先被初始化,關於 init 函數的詳細機制可以參考我之前的文章:Go 語言 init 函數的六個特徵

package cache

......

var redisClient *redis.Client

func Redis() *redis.Client {
 return redisClient
}

func init() {
 redisClient = redis.NewClient(&redis.Options{
  Addr:         config.Redis.Addr,
  Password:     config.Redis.Password,
  DB:           config.Redis.DB,
  PoolSize:     config.Redis.PoolSize,
 })

 if err := redisClient.Ping(context.Background()).Err(); err != nil {
  // 連接不上redis 讓項目停止啓動
  panic(err)
 }
}

go-redis 的客戶端初始化完成後,如果不手動執行 Ping 或者是其他 Redis 操作的話是不會真的去連接 Redis 服務器的,如果你希望在項目啓動時嘗試連接 Redis 服務器,失敗則停止啓動。那麼就加一個 Ping 測試,連接不上用 panic 讓程序直接退出。

 if err := redisClient.Ping(context.Background()).Err(); err != nil {
  // 連接不上redis 讓項目停止啓動
  panic(err)
 }

當然如果你的程序有 Redis 連接不上讀數據庫的兜底策略,可以選擇在項目啓動的時候不進行 Redis 連接性的測試。

Redis Key 的命名 Tips

我在項目中被 Redis 搞的頭大最多的情況是,有的人特別喜歡在 A 項目裏緩存了個什麼數據,然後下游的 B 項目再去讀這個數據,根據緩存裏數據的狀態執行不同的邏輯分支。

這個使用場景沒問題,但是很多時候 Redis 的 Key 攜帶的信息實在是太少,有的時候我在項目 B 裏面 DEBUG,查問題看到從 Redis 裏讀取到的數據跟預想的不一樣,但是我在整個項目裏也沒發現這個緩存從哪存的。 這個時候如果你們團隊的微服務拆地足夠好 (bushi,服務比人還多。。。。。。 會有當場去世的感覺。

別笑,項目比開發多是真事兒,因爲以前 50 多人的團隊造了 10 多個 20 多個項目,現在能給你縮減到 5 個人都不是怪事兒。

所以我們在使用 Redis 的時候,最好把 Key 放在項目裏統一的地方進行管理,同時在命名上加上包含業務、項目、模塊信息的前綴名,通過它們在查問題的時候我們最起碼能快速定位到緩存是哪個項目寫進去的。

存結構化數據,用 String 還是 Hash

用 Redis 時還有一個問題,就是很多時候我們的結構數據是 JSON 序列化後存到 Redis 的 String 類型中去的,Redis 中還有 Hash 類型類似於編程語言裏的哈希 Map。

那麼我們存儲結構數據的時候應該存到 String 還是 Hash 中呢?答案是都行—— 僅從代碼層面講,哈哈哈......,但是前提是 DAO 查詢方法返回做好明確的類型聲明,像下面這樣:

func SetOrder(ctx context.Context, order *do.Order) error {
 jsonDataBytes, _ := json.Marshal(order)
 redisKey := fmt.Sprintf(enum.REDIS_KEY_ORDER_DETAIL, order.OrderNo)
 _, err := Redis().Set(ctx, redisKey, jsonDataBytes, 0).Result()
 if err != nil {
  log.New(ctx).Error("redis error""err", err)
  return err
 }

 return nil
}

func GetOrder(ctx context.Context, orderNo string) (*do.Order, error) {
 redisKey := fmt.Sprintf(enum.REDIS_KEY_DEMO_ORDER_DETAIL, orderNo)
 jsonBytes, err := Redis().Get(ctx, redisKey).Bytes()
 if err != nil {
  log.New(ctx).Error("redis error""err", err)
  return nil, err
 }
 data := new(do.Order)
 json.Unmarshal(jsonBytes, &data)
 return data, nil
}

如果你想從 Redis 層面把數據的結構化體現的更好一點,那麼就用 Hash,這裏需要注意的是 go-redis 支持把結構體數據直接存到 Redis Hash 的前提是要在結構體字段的 tag 上攜帶 redis 標識。

這裏有官方對這塊的的解釋。

Playing struct With "redis" tag. type MyHash struct { Key1 string `redis:"key1"`; Key2 int `redis:"key2"` }

HSet("myhash", MyHash{"value1""value2"})

For struct, can be a structure pointer type, we only parse the field whose tag is redis. 

If you don't want the field to be read, you can use the `redis:"-"` flag to ignore it, or you don't need to set the redis tag. 

For the type of structure field, we only support simple data types: string, int/uint(8,16,32,64), float(32,64), time.Time(to RFC3339Nano), time.Duration(to Nanoseconds )if you are other more complex or custom data types, please implement the encoding.BinaryMarshaler interface.

所以我們的數據結構必須像下面這樣定義

type DummyOrder struct {
 OrderNo string `redis:"orderNo"`
 UserId  int64  `redis:"userId"`
}

然後 go-redis 才能把數據通過 HSET 存到 Redis 的 Hash 中,而直接讀取 Hash 數據到比如上面定義的結構體的時候,需要用到 go-redis 提供的 HGetAll 和 Scan 方法,同理接受數據的結構體的字段也需要在 tag 中攜帶redis標識,不帶這個標識 Scan 方法不會把數據填充到字段上。

總結

Redis 的使用 Tips 上就先講這麼多,歡迎大家在評論區裏補充,另外 Go 項目中用到 redis 時也有人會選擇用 redigo,我在工作時也用過,不過都是集成給我的一些老項目,不知道是不是 redigo 這個庫出的時間更早。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3AanFtF-gzvV5R3Rs92hpA