在一个大的项目中, 使用了全缓存模型, 即, 所有数据都会经过cache.
简单分层: 应用->内存缓存->redis缓存->数据库
是一个典型的 多读写少 的场景, 并且数据量, 请求量非常大.
总结了一些使用经验, 供参考
简洁优雅
关于缓存更新, 可以阅读这篇文章: CoolShell: 缓存更新的套路
为什么选择 Cache Aside Pattern , 因为这个模式足够简单, 出现不一致的概率非常低, 对于大多数项目来说够用了.
而其他几种模式, 复杂度会高很多.
当并发很高的时候, 一个热点 key 失效, 会触发回数据库重查的逻辑, 此时会有大量请求落到数据库
需要做防缓存击穿的处理.
一般各种语言的库, 都有考虑到这一点, 例如 go-redis/cache
如果是golang并且自定义了cache, 可以使用 singleflight , 其他语言也可以找类似机制的库.
这个库很轻量
# define
type Cache struct {
name string
keyPrefix string
codec *cache.Cache
cli *redis.Client
defaultExpiration time.Duration
G singleflight.Group
}
# usage
// if missing, call retrieveFunc
data, err, _ := c.G.Do(key.Key(), func() (interface{}, error) {
return retrieveFunc(key)
})
如果一个key不存在, 在缓存中查不到, 在数据库中也查不到, 那么这个key的请求每次都会穿透到数据库
此时, 可以引入 bloomfilter 或者 cuckoofilter ;
但是, 更简单的做法是, 缓存空值; 当成一个普通的key处理(缓存失效/数据一致性处理等)
大部分场景下, 给每一个缓存 key 设置 TTL 是一个很好的习惯. 可以避免无用数据占用资源, 及时淘汰掉使用较少的数据.
但是, 设置 TTL 的时候, 建议加上一个范围内容的随机数, 避免缓存在同一时间失效, 造成缓存雪崩.
TTL = 900s + randint(0,10)
key = {namespace}:{version}:{type}:{uniqueKey}
在实际应用部署中, 由于可能跟其他应用共用一套缓存, 所以建议缓存的 key 加入前缀, 防止冲突(如果冲突, 非常难以debug)
另外, 需要加入一个 version , 在版本发布必要时变更, 以弃用缓存中已有的数据
MessagePack: It’s like JSON.but fast and small
优点:
缺点:
所以, 缓存数据量比较大, 并且对性能有要求的, 可以使用msgpack
如果 value 比较大, 那么在放入缓存前, 可以进行一次压缩, 获取后再解压
当然, 这个会产生额外的资源消耗(CPU), 以及会多一些耗时.
但是, 这个有利于减少网络传输中的包大小. 如果 读取 是非常高频的话, 那么代价还是值得的.
可以参考 go-redis/cache , 当值超过一定大小时使用 s2 compression 进行压缩
以redis为例, 批量操作
mget/mhget pipeline
可以根据 key-value 特征, 批量 key 的数量等, 简单压测下性能, 决定使用哪种方式. 正常情况下, key 数量较大的时候, pipeline 性能最好.
甚至, 代码实现可以根据 key 的数量, 自行决定使用 mget 还是 pipeline
大部分情况, 项目中会混用两种缓存.
如果对数据一致性要求比较高, 可以全部使用 Redis.
但是, 其实每一次 Redis 操作代价大于内存操作
某些数据, 例如模型, 主键之类的, 一旦确定, 是不会变更的.
此时, 可以考虑使用 内存缓存 替代.
如果是golang, 推荐使用 go-cache . 没有其他实现那么强大, 但是胜在不需要序列化/反序列化.
如果使用的 Redis6, 并且程序的driver支持, 那么可以直接利用 client-side-caching 特性获取最大的性能. 这个对程序透明, 无需在额外的逻辑处理.
但是, 当前(2022)有很多时候, 部署基建还是老版本Redis, 很多语言的driver也还没有支持, 可能复用不了
那么, 此时如果使用了 内存->redis 两级缓存, 如何确保数据一致性.
可以做的额外操作:
sorted-set 存储 5 分钟内变更的 key , 内存缓存TTL设置 5 分钟; 每次先获取变更 key 列表, 本地缓存进行时间戳对比(这个方案对于批量key操作性能提升很大, 相当于把 N 次redis操作, 变成 本地缓存+ 1 次 changedkeylist获取+M次redis操作)让运维根据实际应用场景, 自行切换使用.
成本不高的话, 也可以支持下 redis-cluster 配置
注意, 开启 pool 以获取更好的性能
另外, 也需要关注下如何开启prometheus/otel相关的配置, 以便某些情况下, 监测相关的指标
引入缓存后, 在进行问题调试的时候非常不变.
建议加入相关的调试标志, 例如 ?force=true
此时, 可以通过对比两次请求的差异, 确定是否是缓存问题
甚至, 可以加入 ?debug=true 以获取各个环节的上下文信息, 快速调试