本次我们接着上两篇文章进行讲解《从0开始,用Go语言搭建一个简单的后端业务系统》和《从1开始,扩展Go语言后端业务系统的RPC功能》,如题,需求就是为了应对查询时的高qps,我们引入Redis缓存,让查询数据时不直接将请求发送到数据库,而是先通过一层缓存来抵挡qps,下面我们开始今天的分享:
1 逻辑设计
如图,本次缓存设计的逻辑就是在查询时首先查询缓存,如果查询不到则查询数据库(实际中不建议,会发生缓存穿透),在增删改时会先改数据库,再改缓存。
2 代码
2.1 项目结构
2.2 下载依赖
代码语言:shell复制go get github.com/go-redis/redis/v8
2.3 具体代码和配置
配置:
代码语言:go复制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层:
代码语言:go复制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层实现类:
代码语言:go复制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
}
实体类:
代码语言:go复制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)
}
配置文件:
代码语言:text复制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语言中结构体类似序列化的问题:
解决—结构体实现接口:
代码语言: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的查询并没有增加缓存,当然也是为了方便演示。
今天的分享就到这里。
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表