3.添加缓存和缓存更新策略

项目地址:https://github.com/liwook/PublicReview

添加缓存

查询商铺缓存

我们查询商店的时候,通过接口查询到的数据有很多,我们希望在此用Redis缓存数据,提高查询速度

对于店铺的详细数据,这种数据变化比较大,店家可能会随时修改店铺的相关信息(比如宣传语,店铺名等),所以对于这类变动较为频繁的数据,我们是直接存入Redis中,并且设置合适的有效期。

在internal目录添加shopservice文件夹,添加shopservice.go文件。

  1. 从Redis中查询商铺缓存
  2. 若是Redis中没有,则从数据库中查询,若是数据库也没有就返回没有。
  3. 数据库有,则写入到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)
}
相关推荐
呼啦啦啦啦啦啦啦啦12 分钟前
【Redis】事务
数据库·redis·缓存
猿小飞1 小时前
redis 5.0版本和Redis 7.0.15的区别在哪里
数据库·redis·缓存
qq_3927944814 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存
方圆想当图灵14 小时前
缓存之美:万文详解 Caffeine 实现原理(下)
java·redis·缓存
老大白菜14 小时前
GoFrame 缓存组件
缓存·goframe
LuckyRich117 小时前
2024年博客之星主题创作|2024年度感想与新技术Redis学习
数据库·redis·缓存
boring_11119 小时前
多级缓存以及热点监测
缓存
兩尛19 小时前
缓存商品、购物车(day07)
java·spring boot·缓存
Shimir19 小时前
高并发内存池_各层级的框架设计及ThreadCache(线程缓存)申请内存设计
c语言·c++·学习·缓存·哈希算法·项目
Y编程小白20 小时前
Redis可视化工具--RedisDesktopManager的安装
数据库·redis·缓存