在上一篇 缓存设计的好,服务根本不会倒 介绍了 db 层缓存,回顾一下,db 层缓存次要设计能够总结为:
- 缓存只删除不更新
- 行记录始终只存储一份,即主键对应行记录
- 惟一索引仅缓存主键值,不间接缓存行记录(参考 mysql 索引思维)
- 防缓存穿透设计,默认一分钟,避免缓存击穿和雪崩
- 不缓存多行记录
前言
在大型业务零碎中,通过对长久层增加缓存,对于大多数单行记录查问,置信缓存可能帮长久层加重很大的拜访压力,但在理论业务中,数据读取不仅仅只是单行记录,面对大量多行记录的查问,这对长久层也会造成不小的拜访压力,除此之外,像秒杀零碎、选课零碎这种高并发的场景,单纯靠长久层的缓存是不事实的,本文咱们来介绍 go-zero 实际中的缓存设计之biz cache。
实用场景举例
- 选课零碎
- 内容社交零碎
- 秒杀
像这些零碎,咱们能够在业务层再减少一层缓存来存储系统中的要害信息,如选课零碎中学生选课信息,课程残余名额;内容社交零碎中某一段时间之间的内容信息等。
接下来,咱们以内容社交零碎来进行举例说明。
在内容社交零碎中,咱们个别是先查问一批内容列表,而后点击某条内容查看详情,
在没有增加 biz 缓存前,内容信息的查问流程图应该为:
从上图以及上一篇文章 缓存设计的好,服务根本不会倒 中咱们能够晓得,内容列表的获取是没方法依赖缓存的,
如果咱们在业务层增加一层缓存用来存储列表中的要害信息(甚至残缺信息),那么多行记录的拜访不再是一个问题,这就是 biz redis 要做的事件。接下来咱们来看一下设计方案,假如内容零碎中单行记录蕴含以下字段
字段名称 | 字段类型 | 备注 |
---|---|---|
id | string | 内容 id |
title | string | 题目 |
content | string | 具体内容 |
createTime | time.Time | 创立工夫 |
咱们的指标是获取一批内容列表,而尽量避免内容列表走 db 造成拜访压力,首先咱们采纳 redis 的 sort set 数据结构来存储,根须要存储的字段信息量,有两种 redis 存储计划:
- 缓存部分信息
对其关键字段信息(如:id 等)依照肯定规定压缩,并存储,score 咱们用 createTime
毫秒值(工夫值相等这里不探讨),这种存储计划的益处是节约 redis 存储空间,
那另一方面,毛病就是须要对列表具体内容进行二次回查(但这次回查是会利用到长久层的行记录缓存的)
- 缓存残缺信息
对公布的所有内容依照肯定规定压缩后均进行存储,同样 score 咱们还是用 createTime
毫秒值,这种存储计划的益处是业务的增、删、查、改均走 reids,而 db 层这时候
就能够不必思考行记录缓存了,长久层仅提供数据备份和复原应用,从另一方面来看,其毛病也很显著,须要的存储空间、配置要求更高,费用也会随之增大。
示例代码:
type Content struct {
Id string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreateTime time.Time `json:"create_time"`
}
const bizContentCacheKey = `biz#content#cache`
// AddContent 提供内容存储
func AddContent(r redis.Redis, c *Content) error {v := compress(c)
_, err := r.Zadd(bizContentCacheKey, c.CreateTime.UnixNano()/1e6, v)
return err
}
// DelContent 提供内容删除
func DelContent(r redis.Redis, c *Content) error {v := compress(c)
_, err := r.Zrem(bizContentCacheKey, v)
return err
}
// 内容压缩
func compress(c *Content) string {
// todo: do it yourself
var ret string
return ret
}
// 内容解压
func uncompress(v string) *Content {
// todo: do it yourself
var ret Content
return &ret
}
// ListByRangeTime 提供依据时间段进行数据查问
func ListByRangeTime(r redis.Redis, start, end time.Time) ([]*Content, error) {kvs, err := r.ZrangebyscoreWithScores(bizContentCacheKey, start.UnixNano()/1e6, end.UnixNano()/1e6)
if err != nil {return nil, err}
var list []*Content
for _, kv := range kvs {data := uncompress(kv.Key)
list = append(list, data)
}
return list, nil
}
在以上例子中,redis 是没有设置过期工夫的,咱们将增、删、改、查操作均同步到 redis,咱们认为内容社交零碎的列表拜访申请是比拟高的状况下才做这样的方案设计,
除此之外,还有一些数据拜访,没有像内容设计零碎这么频繁的拜访,可能是某一时间段内访问量从天而降的减少,之后可能很长一段时间才会再拜访一次,以此距离,或者说不会再拜访了,面对这种场景,咱们又该如何思考缓存的设计呢?在 go-zero 内容实际中,有两种计划能够解决这种问题:
- 减少内存缓存:通过内存缓存来存储以后可能突发访问量比拟大的数据,罕用的存储计划采纳 map 数据结构来存储,map 数据存储实现比较简单,但缓存过期解决则须要减少定时器来解决,另一宗计划是通过 go-zero 库中的 Cache,其是专门用于内存缓存治理。
- 采纳 biz redis,并设置正当的过期工夫
总结
以上两个场景能够蕴含大部分的多行记录缓存,对于多行记录查问量不大的场景,临时没必要间接把 biz redis 放进去,能够先尝试让 db 来承当,开发人员能够依据长久层监控及服务监控来掂量何时须要引入 biz cache。
我的项目地址
https://github.com/tal-tech/go-zero
欢送应用 go-zero 并 star 反对咱们!
go-zero 系列文章见『微服务实际』公众号