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