在 Go 項目中使用 Redis 的幾個實用建議
在上代碼之前我還是要廢話幾句,在大家開發(fā)需求用到Redis時一定要多想個兩分鐘 "我是不是把Redis當(dāng)數(shù)據(jù)庫用了?" 因為數(shù)據(jù)在數(shù)據(jù)庫和Redis里存兩份就就得考慮它們的一致性怎么維護(hù),賊麻煩,而這個一致性不做上線后還經(jīng)常會出BUG,所以不是必要我一般不用Redis。
需要過期的數(shù)據(jù)肯定是要存Redis的,比如用戶的 token 之類的數(shù)據(jù),否則存在數(shù)據(jù)庫里還得寫定時任務(wù)來實現(xiàn)token過期刪除的功能 。
PS:Token 別用JWT,最好自己實現(xiàn)一套,后面會跟大家聊一些這方面的經(jīng)驗。
Redis 客戶端的初始化
Redis 客戶端的初始化,這個我建議還是在做好的Redis分層里通過 Go 自帶的init 函數(shù)來實現(xiàn)初始化,別在整個項目的main方法里一個個調(diào)用自己定制化的 InitRedis 之類的方法去實現(xiàn)。
這個有人問為什么? 很簡單因為Go的那些個init函數(shù)是在main方法之前執(zhí)行的,就是被設(shè)計用來做初始化工作的。而且我們也不必?fù)?dān)心初始化順序的問題,被依賴地最深層次的包會最先被初始化。
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的客戶端初始化完成后,如果不手動執(zhí)行Ping 或者是其他Redis操作的話是不會真的去連接Redis服務(wù)器的,如果你希望在項目啟動時嘗試連接Redis服務(wù)器,失敗則停止啟動。那么就加一個Ping測試,連接不上用panic 讓程序直接退出。
if err := redisClient.Ping(context.Background()).Err(); err != nil {
// 連接不上redis 讓項目停止啟動
panic(err)
}
當(dāng)然如果你的程序有Redis連接不上讀數(shù)據(jù)庫的兜底策略,可以選擇在項目啟動的時候不進(jìn)行Redis連接性的測試。
Redis Key 的命名Tips
我在項目中被 Redis 搞的頭大最多的情況是,有的人特別喜歡在A項目里緩存了個什么數(shù)據(jù),然后下游的B項目再去讀這個數(shù)據(jù),根據(jù)緩存里數(shù)據(jù)的狀態(tài)執(zhí)行不同的邏輯分支。
這個使用場景沒問題,但是很多時候Redis 的 Key 攜帶的信息實在是太少,有的時候我在項目B里面DEBUG,查問題看到從Redis里讀取到的數(shù)據(jù)跟預(yù)想的不一樣,但是我在整個項目里也沒發(fā)現(xiàn)這個緩存從哪存的。 這個時候如果你們團(tuán)隊的微服務(wù)拆地足夠好(bushi,服務(wù)比人還多。。。。。。 會有當(dāng)場去世的感覺。
別笑,項目比開發(fā)多是真事兒,因為以前50多人的團(tuán)隊造了10多個20多個項目,現(xiàn)在能給你縮減到5個人都不是怪事兒。
所以我們在使用Redis的時候,最好把Key 放在項目里統(tǒng)一的地方進(jìn)行管理,同時在命名上加上包含業(yè)務(wù)、項目、模塊信息的前綴名,通過它們在查問題的時候我們最起碼能快速定位到緩存是哪個項目寫進(jìn)去的。
存結(jié)構(gòu)化數(shù)據(jù),用String 還是 Hash
用Redis時還有一個問題,就是很多時候我們的結(jié)構(gòu)數(shù)據(jù)是JSON序列化后存到 Redis 的 String 類型中去的,Redis中還有Hash類型類似于編程語言里的哈希Map。
那么我們存儲結(jié)構(gòu)數(shù)據(jù)的時候應(yīng)該存到 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 層面把數(shù)據(jù)的結(jié)構(gòu)化體現(xiàn)的更好一點,那么就用Hash,這里需要注意的是go-redis支持把結(jié)構(gòu)體數(shù)據(jù)直接存到Redis Hash 的前提是要在結(jié)構(gòu)體字段的tag 上攜帶 redis 標(biāo)識。
這里有官方對這塊的的解釋。
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.
所以我們的數(shù)據(jù)結(jié)構(gòu)必須像下面這樣定義:
type DummyOrder struct {
OrderNo string `redis:"orderNo"`
UserId int64 `redis:"userId"`
}
然后go-redis 才能把數(shù)據(jù)通過HSET 存到Redis的Hash中,而直接讀取Hash數(shù)據(jù)到比如上面定義的結(jié)構(gòu)體的時候,需要用到go-redis 提供的HGetAll 和 Scan 方法,同理接受數(shù)據(jù)的結(jié)構(gòu)體的字段也需要在tag中攜帶redis標(biāo)識,不帶這個標(biāo)識Scan方法不會把數(shù)據(jù)填充到字段上。
總結(jié)
Redis的使用Tips上就先講這么多,歡迎大家在評論區(qū)里補充,另外Go項目中用到redis時也有人會選擇用redigo,我在工作時也用過,不過都是集成給我的一些老項目,不知道是不是redigo這個庫出的時間更早。