从2开始,在Go语言后端业务系统中引入缓存

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 从2开始,在Go语言后端业务系统中引入缓存

本次我们接着上两篇文章进行讲解《从0开始,用Go语言搭建一个简单的后端业务系统》《从1开始,扩展Go语言后端业务系统的RPC功能》,如题,需求就是为了应对查询时的高qps,我们引入Redis缓存,让查询数据时不直接将请求发送到数据库,而是先通过一层缓存来抵挡qps,下面我们开始今天的分享:

1 逻辑设计

如图,本次缓存设计的逻辑就是在查询时首先查询缓存,如果查询不到则查询数据库(实际中不建议,会发生缓存穿透),在增删改时会先改数据库,再改缓存。

2 代码

2.1 项目结构

2.2 下载依赖
go get github.com/go-redis/redis/v8
2.3 具体代码和配置

配置:

package config
import (
   "fmt"
   "github.com/go-redis/redis/v8"
   "github.com/spf13/viper"
)
var RDB *redis.Client
func init() {
   var err error
   viper.SetConfigName("app")
   viper.SetConfigType("properties")
   viper.AddConfigPath("./")
   err = viper.ReadInConfig()
   if err != nil {
      panic(fmt.Errorf("Fatal error config file: %w \n", err))
   }
   if err := viper.ReadInConfig(); err != nil {
      if _, ok := err.(viper.ConfigFileNotFoundError); ok {
         fmt.Println("No file ...")
      } else {
         fmt.Println("Find file but have err ...")
      }
   }
   add := viper.GetString("redis.url")
   pwd := viper.GetString("redis.password")
   db := viper.GetInt("redis.db")
   RDB = redis.NewClient(&redis.Options{
      Addr:     add,
      Password: pwd,
      DB:       db,
   })
}

Cache层:

package cache
import (
   "context"
   "count_num/pkg/config"
   "count_num/pkg/entity"
   "encoding/json"
   "github.com/go-redis/redis/v8"
   "time"
)
type CountNumCacheDAOImpl struct {
   db *redis.Client
}
type CountNumCacheDAO interface {
   // set一个
   SetNumInfo(ctx context.Context, key string, info entity.NumInfo, t time.Duration) bool
   // 根据ID获取一个
   GetNumInfoById(ctx context.Context, key string) entity.NumInfo
}
func NewCountNumCacheDAOImpl() *CountNumCacheDAOImpl {
   return &CountNumCacheDAOImpl{db: config.RDB}
}
func (impl CountNumCacheDAOImpl) SetNumInfo(ctx context.Context, key string, info entity.NumInfo, t time.Duration) bool {
   res := impl.db.Set(ctx, key, info, t)
   result, _ := res.Result()
   if result != "OK" {
      return false
   }
   return true
}
func (impl CountNumCacheDAOImpl) GetNumInfoById(ctx context.Context, key string) entity.NumInfo {
   res := impl.db.Get(ctx, key)
   var info entity.NumInfo
   j := res.Val()
   json.Unmarshal([]byte(j), &info)
   return info
}

DAO层实现类:

package impl
import (
   "context"
   "count_num/pkg/cache"
   "count_num/pkg/config"
   "count_num/pkg/entity"
   "fmt"
   "gorm.io/gorm"
   "time"
)
var cacheTime = time.Second * 3600
type CountNumDAOImpl struct {
   db    *gorm.DB
   cache *cache.CountNumCacheDAOImpl
}
func NewCountNumDAOImpl() *CountNumDAOImpl {
   return &CountNumDAOImpl{db: config.DB, cache: cache.NewCountNumCacheDAOImpl()}
}
func (impl CountNumDAOImpl) AddNumInfo(ctx context.Context, info entity.NumInfo) bool {
   var in entity.NumInfo
   impl.db.First(&in, "info_key", info.InfoKey)
   if in.InfoKey == info.InfoKey { //去重
      return false
   }
   impl.db.Save(&info) //要使用指针,Id可以回显
   impl.cache.SetNumInfo(ctx, string(info.Id), info, cacheTime)
   return true
}
func (impl CountNumDAOImpl) GetNumInfoByKey(ctx context.Context, key string) entity.NumInfo {
   var info entity.NumInfo
   impl.db.First(&info, "info_key", key)
   return info
}
func (impl CountNumDAOImpl) FindAllNumInfo(ctx context.Context) []entity.NumInfo {
   var infos []entity.NumInfo
   impl.db.Find(&infos)
   return infos
}
func (impl CountNumDAOImpl) UpdateNumInfoByKey(ctx context.Context, info entity.NumInfo) bool {
   impl.db.Model(&entity.NumInfo{}).Where("info_key = ?", info.InfoKey).Update("info_num", info.InfoNum)
   return true
}
func (impl CountNumDAOImpl) DeleteNumInfoById(ctx context.Context, id int64) bool {
   impl.db.Delete(&entity.NumInfo{}, id)
   impl.cache.SetNumInfo(ctx, string(info.Id), "", cacheTime)
   return true
}
func (impl CountNumDAOImpl) GetNumInfoById(ctx context.Context, id int64) entity.NumInfo {
   var info entity.NumInfo
   numInfoById := impl.cache.GetNumInfoById(ctx, string(id))
   if numInfoById.InfoKey != "" {
      return numInfoById
   }
   impl.db.First(&info, "id", id)
   return info
}
func (impl CountNumDAOImpl) UpdateNumInfoById(ctx context.Context, info entity.NumInfo) bool {
   impl.db.Model(&entity.NumInfo{}).Where("id", info.Id).Updates(entity.NumInfo{Name: info.Name, InfoKey: info.InfoKey, InfoNum: info.InfoNum})
   impl.cache.SetNumInfo(ctx, string(info.Id), info, cacheTime)
   return true
}

实体类:

package entity
import "encoding/json"
type NumInfo struct {
   Id      int64  `json:"id"`
   Name    string `json:"name"`
   InfoKey string `json:"info_key"`
   InfoNum int64  `json:"info_num"`
}
func (stu NumInfo) TableName() string {
   return "num_info"
}
func (info NumInfo) MarshalJSON() ([]byte, error) {
   return json.Marshal(map[string]interface{}{
      "id":       info.Id,
      "name":     info.Name,
      "info_key": info.InfoKey,
      "info_num": info.InfoNum,
   })
}
//Redis类似序列化操作
func (info NumInfo) MarshalBinary() ([]byte, error) {
   return json.Marshal(info)
}
func (info NumInfo) UnmarshalBinary(data []byte) error {
   return json.Unmarshal(data, &info)
}

配置文件:

server.port=9888
server.rpc.port=6666
db.driver=mysql
db.url=127.0.0.1:3306
db.databases=test
db.username=root
db.password=12345
redis.url=127.0.0.1:6379
redis.db=1
redis.password=

3 遇见问题及解决

出现问题,根据提示我们大约能理解是Go语言中结构体类似序列化的问题:

解决—结构体实现接口:

//Redis类似序列化操作
func (info NumInfo) MarshalBinary() ([]byte, error) {
   return json.Marshal(info)
}
func (info NumInfo) UnmarshalBinary(data []byte) error {
   return json.Unmarshal(data, &info)
}

4 总结

引入Redis缓存是后端业务中应对高并发查询比较常见的一个做法,在软件工程学中有一句话叫做:计算机的所有问题都可以用加一层来解决。

在本次项目中可以说缓存设计的相对简单,针对Key的查询并没有增加缓存,当然也是为了方便演示。

今天的分享就到这里。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
Go 调度 开发者
Go语言中的并发编程:深入理解goroutines和channels####
本文旨在探讨Go语言中并发编程的核心概念——goroutines和channels。通过分析它们的工作原理、使用场景以及最佳实践,帮助开发者更好地理解和运用这两种强大的工具来构建高效、可扩展的应用程序。文章还将涵盖一些常见的陷阱和解决方案,以确保在实际应用中能够避免潜在的问题。 ####
|
1天前
|
测试技术 Go 索引
go语言使用 range 关键字遍历
go语言使用 range 关键字遍历
12 3
|
1天前
|
测试技术 Go 索引
go语言通过 for 循环遍历
go语言通过 for 循环遍历
10 3
|
3天前
|
安全 Go 数据处理
Go语言中的并发编程:掌握goroutine和channel的艺术####
本文深入探讨了Go语言在并发编程领域的核心概念——goroutine与channel。不同于传统的单线程执行模式,Go通过轻量级的goroutine实现了高效的并发处理,而channel作为goroutines之间通信的桥梁,确保了数据传递的安全性与高效性。文章首先简述了goroutine的基本特性及其创建方法,随后详细解析了channel的类型、操作以及它们如何协同工作以构建健壮的并发应用。此外,还介绍了select语句在多路复用中的应用,以及如何利用WaitGroup等待一组goroutine完成。最后,通过一个实际案例展示了如何在Go中设计并实现一个简单的并发程序,旨在帮助读者理解并掌
|
2天前
|
Go 索引
go语言按字符(Rune)遍历
go语言按字符(Rune)遍历
12 3
|
6天前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
26 4
|
4天前
|
存储 Go PHP
Go语言中的加解密利器:go-crypto库全解析
在软件开发中,数据安全和隐私保护至关重要。`go-crypto` 是一个专为 Golang 设计的加密解密工具库,支持 AES 和 RSA 等加密算法,帮助开发者轻松实现数据的加密和解密,保障数据传输和存储的安全性。本文将详细介绍 `go-crypto` 的安装、特性及应用实例。
14 0
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
77 6
|
14天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
15天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
下一篇
无影云桌面