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

news2024/10/22 5:21:15

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

添加缓存

查询商铺缓存

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

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

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

  1. 从Redis中查询商铺缓存
  2. 若是Redis中没有,则从数据库中查询,若是数据库也没有就返回没有。
  3. 数据库有,则写入到Redis中,并返回数据。
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

// 返回商铺类型的数据,给首页
// 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中添加路由:

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双写一致性?

先更新数据库,再删除缓存

前面的两个函数是查询,不是更新。那现在添加更新的函数。

更新商铺和添加商铺和删除商铺。只有在更新商铺删除商铺时候才需要删除缓存

// 更新商铺
// 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中添加对应的路由

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)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2220557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Win安装Redis

目录 1、下载 2、解压文件并修改名称 3、前台简单启动 4、将redis设置成服务后台启动 5、命令启停redis 6、配置文件设置 1、下载 【下载地址】 2、解压文件并修改名称 3、前台简单启动 redis-server.exe redis.windows.conf 4、将redis设置成服务后台启动 redis-server -…

xlnt加载excel报错:xl/workbook.xml:2:2581: error: attribute ‘localSheetId‘ expected

解决方案 大家不一定能看懂,地址里说的啥意思,地址过去主要说明了从https://github.com/musshorn/xlnt/tree/issue_685合入可以解决问题,后面再想推送到官方地址,但没人维护了。 我这边直接给大家说一个结果就是:问题…

python 爬虫 入门 四、线程,进程,协程

线程和进程大部分人估计都知道,但协程就不一定了。 一、进程 进程是操作系统分配资源和调度的基本单位,一个程序开始运行时,操作系统会给他分配一块独立的内存空间并分配一个PCB作为唯一标识。初始化内存空间后进程进入就绪态,PC…

GUI编程

GUI编程 【Java从0到架构师课程】笔记 GUI简介 GUI:图形用户界面,在计算机中采用图形的方式显示用户界面 java的GUI开发 AWT:java最早推出的GUI编程开发包,界面风格跟随操作系统SWT:eclipse就是java使用SWT开发的Sw…

【ArcGIS微课1000例】0125:ArcGIS矢量化无法自动完成面解决方案

文章目录 一、坐标系统问题二、正确使用自动完成面工具一、坐标系统问题 1. 数据库坐标系 arcgis矢量化的过程中,无法自动完成面,可能是因为图层要素没有坐标系造成的。双击数据库打开数据库属性,可以查看当前数据框的坐标系。 2. 图层坐标系 双击图层,打开图层属性,切…

从零开始学PHP之变量作用域数据类型

一、数据类型 上篇文章提到了数据类型,在PHP中支持以下几种类型 String (字符串)Integer(整型)Float (浮点型)Boolean(布尔型)Array(数组)Objec…

滤波算法与SLAM:从概率角度理解SLAM问题

滤波算法与SLAM 第三章:MAP/MLE问题和贝叶斯网络 文章目录 滤波算法与SLAM前言一、最大后验问题(MAP)与最大似然问题(MLE)二、贝叶斯网络与SLAM问题三、因子图与SLAM问题四、从概率角度理解Kalman Filter与SLAM 前言 …

我谈傅里叶变换幅值谱的显示

在图像处理和分析中通常需要可视化图像傅里叶变换的幅值谱。通过幅值谱,可以直观地观察频率成分的分布,帮助理解图像的结构和特征。 很多刊物中直接显示傅里叶变换的幅值谱。 FFT fftshift(fft2(double(Img))); FFT_mag mat2gray(log(1abs(FFT)));由…

【Linux】线程基本概念,线程控制

目录 基本概念 重新理解进程 线程真实存在吗? 问题解答 线程资源 线程控制 线程创建 如何全面看待线程函数传参 如何看到线程函数返回 线程查询 线程等待 线程终止 线程分离 基本概念 线程(thread)是指在单个进程内,多…

探索前端与 AI 的结合:如何用 GPT-4 助力开发效率

前言:开发者的“神队友” GPT-4 还记得那些深夜奋战,紧盯着屏幕敲代码的日子吗?表单不验证、布局乱飞、BUG 根本找不到,这些时刻简直能让人抓狂。你可能会想:“要是有个智能助手能帮我搞定这些多好!” 那么…

#HarmonyOS:页面和自定义组件生命周期

页面生命周期 即被Entry装饰的组件生命周期 onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。onPageHide: 页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。onBackPress: 当用户点击返回按钮是触发 组件…

全面了解 NGINX 的负载均衡算法

NGINX 提供多种负载均衡方法,以应对不同的流量分发需求。常用的算法包括:最少连接、最短时间、通用哈希、随机算法和 IP 哈希。这些负载均衡算法都通过独立指令来定义,每种算法都有其独特的应用场景。 以下负载均衡方法(IP 哈希除…

如何让自己的网站,被更多的人搜索到(免费方案)

文章目录 一、要做时间的朋友二、需要独立IP的服务器三、SEO信息如何设置设置网站TDK生成网站地图设置搜索引擎自动提交部署SSL证书加分项:定期更新文章 引言: 许多人都有这样一个问题:做好自己的网站,如何让这个网站被更多的人浏…

WPF实现类似网易云音乐的菜单切换

这里是借助三方UI框架实现了,感兴趣的小伙伴可以看一下。 深色模式:​ 浅色模式: ​这里主要使用了以下三个包: MahApps.Metro:UI库,提供菜单导航和其它控件​​​​​​​ 实现步骤:1、使用B…

SSRF-利用dict协议-攻击redis

1.靶场准备: CTFHub-技能树-Web-SSRF-Redis协议 蚁剑AntSword 2.简述: 2.1 SSRF 服务器端请求伪造,存在一个url参数,一般用于图片上传、网页重定向等,我们可以控制url参数,去访问内网服务器的敏感内容…

前端vue框架配置基础信息详解分析

前端vue2、vue3框架是我们最近常用的框架,今天我们分析一下配置基础信息、详解其中的功能含义。 1、vue.config.js 文件分析 这个 vue.config.js 文件是 Vue CLI 项目中用于配置项目构建行为和开发环境设置的文件。它能够让开发者定制打包、代理、路径、样式等方面…

干货:落地企业级RAG的实践指南

1. 什么是RAG? 检索增强生成(Retrieval-Augmented Generation,简称 RAG)通过结合大型语言模型(LLM)和信息检索系统来提高生成文本的准确性和相关性.这种方法允许模型在生成回答之前,先从权威知…

spdlog学习记录

spdlog Loggers:是 Spdlog 最基本的组件,负责记录日志消息。在 Spdlog 中,一个 Logger 对象代表着一个日志记录器,应用程序可以使用 Logger 对象记录不同级别的日志消息Sinks:决定了日志消息的输出位置。在 Spdlog 中&…

深入拆解TomcatJetty(三)

深入拆解Tomcat&Jetty(三) 专栏地址:https://time.geekbang.org/column/intro/100027701 1 Tomcat组件生命周期 Tomcat如何如何实现一键式启停 Tomcat 架构图和请求处理流程如图所示: 对组件之间的关系进行分析,…

MySQL(python开发)——(3)表数据的基本操作,增删改查

MySQL(python开发)——(1)数据库概述及其MySQL介绍 MySQL(python开发)——(2)数据库基本操作及数据类型 MySQL—— 表数据基本操作 一、表中插入(insert)数据——增 insert into 表名 values (值1&#…