在 Go 項目中使用 Redis 的幾個實用建議
在上代碼之前我還是要廢話幾句,在大家開發需求用到Redis時一定要多想個兩分鐘 "我是不是把Redis當數據庫用了?" 因為數據在數據庫和Redis里存兩份就就得考慮它們的一致性怎么維護,賊麻煩,而這個一致性不做上線后還經常會出BUG,所以不是必要我一般不用Redis。
需要過期的數據肯定是要存Redis的,比如用戶的 token 之類的數據,否則存在數據庫里還得寫定時任務來實現token過期刪除的功能 。
PS:Token 別用JWT,最好自己實現一套,后面會跟大家聊一些這方面的經驗。
Redis 客戶端的初始化
Redis 客戶端的初始化,這個我建議還是在做好的Redis分層里通過 Go 自帶的init 函數來實現初始化,別在整個項目的main方法里一個個調用自己定制化的 InitRedis 之類的方法去實現。
這個有人問為什么? 很簡單因為Go的那些個init函數是在main方法之前執行的,就是被設計用來做初始化工作的。而且我們也不必擔心初始化順序的問題,被依賴地最深層次的包會最先被初始化。
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查詢方法返回做好明確的類型聲明,像下面這樣:
unc 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這個庫出的時間更早。