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)
}
相关推荐
Daniel 大东39 分钟前
idea 解决缓存损坏问题
java·缓存·intellij-idea
起飞的风筝1 小时前
【redis】—— 环境搭建教程
数据库·redis·缓存
古人诚不我欺3 小时前
Redis设置密码认证,以及不重启服务情况下设置临时密码
数据库·redis·缓存
ktkiko111 天前
Redis中的缓存设计
数据库·redis·缓存
激流丶1 天前
【缓存策略】你知道 Write Around(缓存绕过写)这个缓存策略吗?
java·分布式·缓存
激流丶1 天前
【缓存策略】你知道 Write Through(直写)这个缓存策略吗?
java·分布式·后端·缓存·中间件
ascarl20101 天前
系统启动时将自动加载环境变量,并后台启动 MinIO、Nacos 和 Redis 服务
数据库·redis·缓存
LightOfNight1 天前
Redis设计与实现第9章 -- 数据库 总结(键空间 过期策略 过期键的影响)
数据库·redis·后端·缓存·中间件·架构
_.Switch1 天前
Python 自动化运维持续优化与性能调优
运维·开发语言·python·缓存·自动化·运维开发
Wlq04152 天前
分布式技术缓存技术
分布式·缓存