Gin+GORM在访问数据库时,有多种方案实现先读取缓存、未命中再读取数据库
实现方案
GORM层缓存方案
实现机制: 使用钩子实现在SQL查询周期执行AfterCreate、BeforeQuery逻辑,实现查询前查缓存、创建后更新缓存
GIN层缓存方案
对比:
在GORM层实现查询缓存
优势:
- 数据层面的封装:将缓存逻辑封装在数据层意味着所有通过GORM进行的数据库操作都会自然地利用缓存,不依赖于特定的web框架。
- 代码复用性:如果应用在多个地方使用相同的数据访问逻辑,那么这种方法可以确保缓存逻辑的一致性和复用性。
劣势:
- 复杂的缓存逻辑:在ORM层处理缓存可能需要处理更复杂的场景,如对象关联和复杂查询。
- 可能的性能开销:如果缓存逻辑本身很复杂,可能会引入额外的性能开销。
- 减少灵活性:将缓存逻辑绑定在ORM层可能减少了在不同场景下调整缓存策略的灵活性。
在Gin中间件实现查询缓存
优势:
- 请求级别的缓存:可以根据不同的API请求进行精细化的缓存控制。
- 灵活性和控制:在web框架层面实现缓存允许你根据请求的不同参数或路径来调整缓存逻辑。
- 易于实现:通常在web框架中实现缓存逻辑比在ORM层更直接、更容易实现。
劣势:
- 绑定于特定框架:这种方法将缓存逻辑绑定在了特定的web框架(例如Gin)上,减少了代码的可移植性。
- 缓存粒度问题:可能需要更细致地控制缓存,例如根据不同的用户或请求参数来缓存,这在中间件层面可能比较复杂。
最终考虑,还是在Gin层实现:
根据接口/入参等构建查询Key,若Key存在直接返回结果;若不存在走接口逻辑
构建缓存层
定义接口
infrastructure/data_layer/interface.go
type DataLayerInterface interface {
Init(config *config.CacheConfig) error
SetCostComparisonOverviewResultCache(kvs util.CostComparisonOverviewResultKV) error
GetCostComparisonOverviewResultCache(key string) ([]handler.CostComparisonOverviewResponse, error)
}
实现接口方法
infrastructure/data_layer/redis.go
func (r *RedisLayer) SetCostComparisonOverviewResultCache(kvs util.CostComparisonOverviewResultKV) error {
defer commonutil.TimeCost()()
p := r.client.NewPipeline()
ctx := context.Background()
s, err := json.Marshal(kvs.Value)
if err != nil {
global.LogEntry.Errorf("序列化失败 %v", kvs.Key)
return err
} else {
p.Set(ctx, kvs.Key, string(s), time.Hour*24*30000)
_, err := p.Exec(ctx)
if err != nil {
global.LogEntry.Errorf("Key:%v 写入缓存失败:%v", kvs.Key, err)
return err
}
}
return nil
}
func (r *RedisLayer) GetCostComparisonOverviewResultCache(key string) ([]handler.CostComparisonOverviewResponse, error) {
defer commonutil.TimeCost()()
var (
dataItems = []handler.CostComparisonOverviewResponse{}
rr = RedisLayer{}
v = rr.Init()
)
vars, err := v.Get(key)
if err != nil {
global.LogEntry.Errorf("Redis缓存查询异常:%v %v", vars, err.Error())
return nil,err
} else {
items := []handler.CostComparisonOverviewResponse{}
err := json.Unmarshal([]byte(vars), &items)
if err != nil {
global.LogEntry.Errorf("Redis数据反序列化失败:%v %v", vars, err)
return nil,err
}
dataItems = append(dataItems, items...)
}
global.LogEntry.Infof("Reids缓存查询结果:总计数据:%v", len(dataItems))
return dataItems, nil
}
使用缓存
infrastructure/cache/cache.go
func (c *Gorm2Cache) SetCostComparisonOverviewResultCache(key string, vars []handler.CostComparisonOverviewResponse) error {
kvs := util.CostComparisonOverviewResultKV{
Key: key,
Value: vars,
}
return c.cache.SetCostComparisonOverviewResultCache(kvs)
}
func (c *Gorm2Cache) GetCostComparisonOverviewResultCache(key string ) ([]handler.CostComparisonOverviewResponse, error) {
return c.cache.GetCostComparisonOverviewResultCache(key)
}
Gin中间件构建
中间件实现
这里在业务逻辑实现缓存不存在,则更新缓存
middleware/cache.go
const (
CostComparisonByDAORedisKey = "cost-comparisonByDAO"
)
/*
1. 判断缓存层是否存在Key,如果存在则取出缓存并返回数据;c.Abort
2. 如果缓存中Key不存在,则c.Next()继续查询数据库
*/
func CacheMiddleware() gin.HandlerFunc {
var (
cacheConfig = &config.CacheConfig{}
)
gorm2Cache, _ := cache2.NewGorm2Cache(cacheConfig)
return func(c *gin.Context) {
key := GenCacheQueryKeyV3(c)
req := request.HGin{C: c}
fPath := c.Request.URL.Path
isExists, err := gorm2Cache.KeyExist(key)
if err != nil {
global.LogEntry.Warnf("CacheMiddleware 获取缓存KEY: %v异常 %v", key, err)
}
if !isExists { // 缓存不存在, 查询sql ,写入redis缓存
c.Next()
} else {
// 取出缓存
switch path.Base(fPath) {
case CostComparisonByDAORedisKey:
data, _ := gorm2Cache.GetCostComparisonOverviewResultCache(key)
v := handler.Res{
Total: len(data),
Record: data,
}
req.SuccessResponse(handler.Page(v,c))
}
c.Abort()
}
}
}
使用中间件
中间件只绑定特定URL
router/bill/cost_comparison.go
unc RegisterCostComparisonRouter(rp *gin.RouterGroup) {
rp.GET("/cost-comparisonByDAO", middleware.CacheMiddleware(), v3.GetCostComparisonByService)
}
第一次访问「访问数据库」
第二次访问「使用缓存」
优势:
相比在逻辑里实现缓存miss判断,在中间件里实现更统一、易于控制
hit := rQuery.GCache(c, true)
if hit {
return rQuery.BuildResponse(c, true)
}
} else {
fmt.Printf("不走缓存查询:%v %v \n", c.Query("cost_unit"), c.Query("product_code"))
rQuery.GDB(c, true)
}