项目地址:https://github.com/liwook/PublicReview
添加缓存
查询商铺缓存
我们查询商店的时候,通过接口查询到的数据有很多,我们希望在此用Redis缓存数据,提高查询速度。
对于店铺的详细数据,这种数据变化比较大,店家可能会随时修改店铺的相关信息(比如宣传语,店铺名等),所以对于这类变动较为频繁的数据,我们是直接存入Redis中,并且设置合适的有效期。
在internal目录添加shopservice文件夹,添加shopservice.go文件。
- 从Redis中查询商铺缓存
- 若是Redis中没有,则从数据库中查询,若是数据库也没有就返回没有。
- 数据库有,则写入到Redis中,并返回数据。
Go
const ShopKeyPriex = "cache:shop:"
// 根据商店id查找商店缓存数据
// get /shop/:id
func QueryShopById(c *gin.Context) {
id := c.Param("id") //获取定义的路由参数的值
if id == "" {
code.WriteResponse(c, code.ErrValidation, "id can not be empty")
return
}
//1.从redis查询商铺缓存,是string类型的
val, err := db.RedisClient.Get(context.Background(), ShopKeyPriex+id).Result()
if err == nil { //若redis存在该缓存,直接返回
var shop model.TbShop
sonic.Unmarshal([]byte(val), &shop)
code.WriteResponse(c, code.ErrSuccess, shop)
} else if err == redis.Nil { //2.若是redis没有该缓存,从mysql中查询
tbSop := query.TbShop
idInt, _ := strconv.Atoi(id)
shop, err := tbSop.Where(tbSop.ID.Eq(uint64(idInt))).First()
if err == gorm.ErrRecordNotFound {
//3.mysql若不存在该商铺,返回错误
code.WriteResponse(c, code.ErrDatabase, "this shop not found")
return
}
if err != nil {
slog.Error("mysql find shop by id bad", "error", err)
code.WriteResponse(c, code.ErrDatabase, nil)
return
}
//4.找到商铺,写回redis,并发送给客户端
//把shop进行序列化,不然写入redis会出错。序列化就是把该数据对象变成json,即是变成一个字符串
v, _ := sonic.Marshal(shop) //这里使用github.com/bytedance/sonic
_, err = db.RedisClient.Set(context.Background(), ShopKeyPriex+id, v, 0).Result()
if err != nil {
slog.Error("redis set val bad", "error", err)
code.WriteResponse(c, code.ErrDatabase, nil)
}
code.WriteResponse(c, code.ErrSuccess, shop)
} else {
code.WriteResponse(c, code.ErrSuccess, val)
}
}
查询商户类型缓存
软件首页的这块列表信息是不变动的,因此我们可以将它存入缓存中,避免每次访问时都去查询数据库
那么这里一个key就会有多个元素,那我们可以使用Redis的list类型来存储。
注意:sonic.Marshal()返回的是[]byte。要是使用[]byte,会报错redis: can't marshal [][]uint8,所以要转换成string
Go
// 返回商铺类型的数据,给首页
// get /shop/type-list
func QueryShopTypeList(c *gin.Context) {
//1.先从redis中查询
// 获取List中的元素:起始索引~结束索引,当结束索引 > llen(list)或=-1时,取出全部数据
val, err := db.RedisClient.LRange(context.Background(), ShopTypeKey, 0, -1).Result()
if err == redis.Nil || len(val) == 0 {
//2. 若是没有,从mysql中获取
shopType := query.TbShopType
typeList, err := shopType.Order(shopType.Sort).Find() //Find函数返回没有数据的话,err是nil
if err != nil {
slog.Error("shoptypelist mysql find bad", "err", err)
code.WriteResponse(c, code.ErrDatabase, nil)
return
}
if len(typeList) == 0 {
code.WriteResponse(c, code.ErrSuccess, "no data in database")
return
}
//3.序列化,并往redis中添加
//注意:要是使用[]byte,会报错redis: can't marshal [][]uint8,所以要转换成string
pipeline := db.RedisClient.Pipeline()
for _, shop := range typeList {
val, _ := sonic.Marshal(shop)
pipeline.RPush(context.Background(), ShopTypeKey, string(val))
}
_, err = pipeline.Exec(context.Background())
if err != nil {
slog.Error("redis list push bad", "err", err)
code.WriteResponse(c, code.ErrDatabase, nil)
return
}
code.WriteResponse(c, code.ErrSuccess, typeList)
} else if err != nil {
slog.Error("redis list find bad", "err", err)
code.WriteResponse(c, code.ErrDatabase, nil)
} else {
//从Redis中获取的数据是字符串格式,而不是JSON格式,所以需要反序列化
var valList = make([]*model.TbShopType, len(val))
for i, v := range val {
_ = sonic.UnmarshalString(v, &valList[i])
}
code.WriteResponse(c, code.ErrSuccess, val[0])
}
}
在router.go中添加路由:
Go
func NewRouter() *gin.Engine {
r := gin.Default()
//在测试阶段,为了方便,就不使用jwt中间件
// r.Use(middleware.JWT()) //使用jwt中间件
r.GET("/shop/:id", shopservice.QueryShopById) //添加根据id查询商铺的路由
r.GET("/shoptype", shopservice.QueryShopTypeList) //添加商铺类型的链表路由
return r
}
缓存更新策略
现在商铺信息存储在了缓存和数据库中。由于缓存和数据库是分开的,无法做到原子性的进行数据修改,可能出现缓存更新失败,或者数据库更新失败的情况,这时候会出现数据不一致,影响业务。那么如何解决数据库和缓存不一致问题?
大方向有三种:
- Cache Aside Pattern 旁路缓存模式,也叫人工编码方式:需要程序员写代码 同时维系 DB 和 cache。也称作双写方案。
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关系缓存一致性问题。但是维护这样一个服务很复杂 ,市面上也不容易找到一个这样现成的服务,开发成本高。
- Write Behind Caching Pattern:调用者只操作缓存,其他线程异步去处理数据库,最终实现一致性。但是维护这样的一个异步任务比较复杂,需要实时监控缓存中的数据更新,而其他线程异步去更新数据库也可能不太及时,而且缓存服务器如果宕机,那么缓存的数据也就丢失了。
综上所述,在企业的实际应用中,还是Cache Aside Pattern方案最可靠。现在确定了该方案,但是需要程序员去调用缓存和数据库?那因为是两个应用,那操作就有先后顺序,那是应该先操作哪个呢?还有是更新缓存还是删除缓存呢?
可以分成4种情况:
- 先更新缓存,再更新数据
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
具体的分析可以查看该文章如何保证Redis双写一致性?
先更新数据库,再删除缓存
前面的两个函数是查询,不是更新。那现在添加更新的函数。
更新商铺和添加商铺和删除商铺。只有在更新商铺 和删除商铺时候才需要删除缓存。
Go
// 更新商铺
// post /shop/update
func UpdateShop(c *gin.Context) {
var shop model.TbShop
err := c.BindJSON(&shop)
if err != nil {
slog.Error("bindjson bad", "err", err)
code.WriteResponse(c, code.ErrBind, nil)
return
}
update(c, &shop)
}
func update(c *gin.Context, shop *model.TbShop) {
//1.更新数据库
//当通过 struct 更新时,GORM 只会更新非零字段。
//若想确保指定字段被更新,应使用Select更新选定字段,或使用map来完成更新
tbshop := query.TbShop
_, err := tbshop.Where(tbshop.ID.Eq(shop.ID)).Updates(shop)
if err != nil {
slog.Error("update mysql bad", "err", err)
code.WriteResponse(c, code.ErrDatabase, nil)
return
}
//2.删除缓存
key := ShopKeyPriex + strconv.Itoa(int(shop.ID))
db.RedisClient.Del(context.Background(), key)
code.WriteResponse(c, code.ErrSuccess, nil)
}
// 添加商铺
// post /shop/add
func AddShop(c *gin.Context) {
var shop model.TbShop
err := c.BindJSON(&shop)
if err != nil {
slog.Error("bindjson bad", "err", err)
code.WriteResponse(c, code.ErrBind, nil)
return
}
err = query.TbShop.Create(&shop)
if err != nil {
slog.Error("mysql create shop err", "err", err)
code.WriteResponse(c, code.ErrDatabase, nil)
} else {
code.WriteResponse(c, code.ErrSuccess, nil)
}
}
// 删除商铺
// delet /shop/delete/:id
func DelShop(c *gin.Context) {
id := c.Param("id")
if id == "" {
code.WriteResponse(c, code.ErrValidation, "id is null")
return
}
val, _ := strconv.Atoi(id)
shop := query.TbShop
_, err := shop.Where(shop.ID.Eq(uint64(val))).Delete()
if err != nil {
code.WriteResponse(c, code.ErrDatabase, nil)
}
//删除缓存
key := ShopKeyPriex + id
db.RedisClient.Del(context.Background(), key)
code.WriteResponse(c, code.ErrSuccess, nil)
}
在router.go中添加对应的路由
Go
func NewRouter() *gin.Engine {
r := gin.Default()
// r.Use(middleware.JWT()) //使用jwt中间件
..............
r.POST("/shop/update", shopservice.UpdateShop)
r.POST("/shop/add", shopservice.AddShop)
r.DELETE("/shop/delete/:id", shopservice.DelShop)
}