需要过期的数据肯定是要存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这个库出的时间更早。