- 作者:老汪软件技巧
- 发表时间:2024-08-17 15:35
- 浏览量:
一、golang 世界的多级缓存框架现状
golang 开源框架的世界里有很多NB的分布式缓存框架,也有很多NB的本地内存缓存框架,但是没有NB的多级缓存框架!如果你体验过阿里巴巴开源的 JetCache 缓存框架,你就会觉得其他缓存框架都不够好用。为了解决缓存高性能、高可用面临的问题,也为了提高研发效率和质量,我们决定自研 golang 版本的 JetCache 缓存框架。
Featureeko/gocachebytedance/gopkggo-redis/cachemgtv-tech/jetcache-go
Star数量
2.4k
1.7k
741
139
多级缓存
缓存旁路(loadable)
泛型支持
单飞模式
缓存更新监听器(缓存一致)
异步刷新
指标统计
Y(简单)
缓存空对象
批量查询(MGet)
稀疏列表缓存
二、JetCache-go 的魅力2.1 功能概览
2.2 缓存旁路模式(Cache-Aside Pattern)配置
redis:
test-redis:
addrs:
- 1.1.1.1:6442
- 1.1.1.2:7440
- 1.1.1.3:7441
type: cluster
readTimeout: 100ms
writeTimeout: 100ms
cache:
graph:
cacheType: both # 可选 both、remote、local 三种模式
localType: tinyLFU # 可选 tinyLFU、freeCache 两种本地缓存类型
codec: msgpack # 可选 json、sonic、msgpack 三种序列化方式
redisName: test-redis
refreshDuration: 1m # 自动刷新缓存周期
stopRefreshAfterLastAccess: 5m # 缓存 key 多久没有访问后停止自动刷新
localExpire: 1m # 本地缓存过期时长
remoteExpire: 1h # 默认的远程缓存过期时长
part:
cacheType: local # 可选 both、remote、local 三种模式
localType: freeCache # 可选 tinyLFU、freeCache 两种本地缓存类型
codec: sonic # 可选 json、sonic、msgpack 三种序列化方式
redisName: test-redis
refreshDuration: 1m # 自动刷新缓存周期
stopRefreshAfterLastAccess: 5m # 缓存 key 多久没有访问后停止自动刷新
localExpire: 1m # 本地缓存过期时长
remoteExpire: 1h # 默认的远程缓存过期时长
我们内部脚手架封装好了通过配置即可初始化缓存实例。
使用
Once接口查询,并开启缓存自动刷新。
// GetSubGraphCache 查询知识图谱缓存
func (s *Service) GetSubGraphCache(ctx context.Context, req *api.SubGraphReq) (resp *api.SubGraphResp, err error) {
if err := req.Validate(); err != nil {
return nil, err
}
key := model.KeySubGraph.Key(req.VertexId, req.ArtifactId, req.Depth)
err = s.dao.CacheGraph.Once(ctx, key,
cache.TTL(model.KeySubGraph.Expire()),
cache.Refresh(true),
cache.Value(&resp),
cache.Do(func(ctx context.Context) (any, error) {
return s.getSubGraph(ctx, req)
}))
return
}
// 查询知识图谱
func (s *Service) getSubGraph(ctx context.Context, req *api.SubGraphReq) (resp *api.SubGraphResp, err error) {
// 逻辑实现
}
MGet(稀疏列表缓存)泛型接口查询
// MGetPartCache 批量获取视频缓存
func (d *Dao) MGetPartCache(ctx context.Context, partIds []int64) map[int64]*model.Part {
return d.CachePart.MGet(ctx, model.KeyPart.Key(), partIds,
func(ctx context.Context, ids []int64) (map[int64]*model.Part, error) {
return d.Odin.MGetPart(ctx, ids)
})
}
// MGetPart 批量获取分集信息
func (c *Odin) MGetPart(ctx context.Context, partIds []int64) (map[int64]*model.Part, error) {
// 查询逻辑
}
通过缓存旁路模式,只需要简单的对业务方法进行代理,就能够叠加多级缓存的各种特效。
三、JetCache-go 是如何解决缓存中的经典问题的3.1 分布式-缓存穿透
关键词:强调缓存和数据库都没有数据+并发访问
应对策略:
jetcache-go采取轻量级的 方式来解决这个问题:
func (c *jetCache) setNotFound(ctx context.Context, key string, skipLocal bool) error {
if c.local != nil && !skipLocal {
c.local.Set(key, notFoundPlaceholder)
}
// 略...
ttl := c.notFoundExpiry + time.Duration(c.safeRand.Int63n(int64(c.offset)))
return c.remote.SetEX(ctx, key, notFoundPlaceholder, ttl)
}
func (c *jetCache) getBytes(ctx context.Context, key string, skipLocal bool) ([]byte, error) {
if !skipLocal && c.local != nil {
b, ok := c.local.Get(key)
if ok {
c.statsHandler.IncrHit()
c.statsHandler.IncrLocalHit()
if bytes.Compare(b, notFoundPlaceholder) == 0 {
return nil, c.errNotFound
}
return b, nil
}
c.statsHandler.IncrLocalMiss()
}
// 略...
b := util.Bytes(s)
if bytes.Compare(b, notFoundPlaceholder) == 0 {
return nil, c.errNotFound
}
// 略...
return b, nil
}
3.2 分布式-缓存击穿
关键词:强调单个热点Key过期+并发访问
应对策略:
jetcache-go提供 、、、等机制实现击穿保护。
3.3 分布式-防缓存击穿利器-singleflight
在go标准库中(/x/sync/singleflight) 提供了可重复的函数调用抑制机制。通过给每次函数调用分配一个key,相同key的函数并发调用时,只会被执行一次,返回相同的结果。其本质是对函数调用的结果进行复用。
jetcache-go的 Once、MGet 接口均采用singlefilght机制实现。
3.4 分布式-防缓存击穿利器-auto refresh
mycache := cache.New(cache.WithName("any"),
cache.WithRemote(remote.NewGoRedisV8Adaptor(ring)),
cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)),
cache.WithErrNotFound(errRecordNotFound),
cache.WithRefreshDuration(time.Minute),
cache.WithStopRefreshAfterLastAccess(time.Hour))
)
if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true),
cache.Do(func(ctx context.Context) (any, error) {
return mockDBGetObject(1)
})); err != nil {
panic(err)
}
3.5 分布式-缓存雪崩
关键词:强调批量Key过期+并发访问
应对策略:
jetcache-go提供、、等机制来避免缓存雪崩。
设置缓存数据的随机过期时间:
func (c *TinyLFU) Set(key string, b []byte) {
ttl := c.ttl
if c.offset > 0 {
ttl += time.Duration(c.rand.Int63n(int64(c.offset)))
}
c.cache.SetWithTTL(key, b, 1, ttl)
// wait for value to pass through buffers
c.cache.Wait()
}
func (c *jetCache) setNotFound(ctx context.Context, key string, skipLocal bool) error {
if c.local != nil && !skipLocal {
c.local.Set(key, notFoundPlaceholder)
}
// 略...
ttl := c.notFoundExpiry + time.Duration(c.safeRand.Int63n(int64(c.offset)))
return c.remote.SetEX(ctx, key, notFoundPlaceholder, ttl)
}
3.6 分布式-大Key问题
关键词:强调Key数据量过大/Key中成员的数量过多
大Key是指Key本身的数据量过大或者Key中的成员数过多。大Key容易导致Redis实例的带宽利用率被占满、集群架构下分片内存不均衡、对Key进行删除操作造成长时间阻塞等问题。
应对策略:
jetcache-go支持,实现了多种序列化方案。默认是Msgpack+Snappy压缩,也支持自定义实现。
3.7 分布式-热Key问题
关键词:强调Key的请求频频过高
热Key是指预期外的访问量陡增,如突然出现的爆款商品、访问量暴涨的热点新闻、直播间某主播搞活动带来的大量刷屏点赞、游戏中某区域发生多个工会之间的战斗涉及大量玩家等。导致占用大量CPU资源、节点连接数被耗尽,影响其他请求并导致整体性能降低。
应对策略:
Jetcache-go 支持,也支持针对缓存数据利用率较高的场景开启自动刷新缓存,达到的效果。
四、JetCache-go 有哪些高级特性4.1 支持多种序列化方式codec方式说明优势
原生json
golang自带的序列化工具
兼容性强
msgpack
msgpack+snappy压缩(内容>64字节)
性能较强、容量小
sonic
字节开源的高性能json序列化工具
性能强
你也可以自定义实现codec的接口,注册进来。
4.2 支持多种本地缓存、远程缓存名称类型优缺点
TinyLFU
LOCAL
优点:缓存命中率高缺点:如果缓存条目过多,GC负担会比较严重
FreeCache
LOCAL
优点:Zero GC高性能 缺点:缓存Key/Value大小限制,需要预申请虚拟内存
redis/go-redis(v8)
REMOTE
非常先进的go Redis客户端(star 19.6k)
你也可以自定义缓存实现:
Local接口
type Local interface {
// Set stores the given data with the specified key.
Set(key string, data []byte)
// Get retrieves the data associated with the specified key.
// It returns the data and a boolean indicating whether the key was found.
Get(key string) ([]byte, bool)
// Del deletes the data associated with the specified key.
Del(key string)
}
Remote接口
type Remote interface {
// SetEX sets the expiration value for a key.
SetEX(ctx context.Context, key string, value any, expire time.Duration) error
// SetNX sets the value of a key if it does not already exist.
SetNX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error)
// SetXX sets the value of a key if it already exists.
SetXX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error)
// Get retrieves the value of a key. It returns errNotFound (e.g., redis.Nil) when the key does not exist.
Get(ctx context.Context, key string) (val string, err error)
// Del deletes the cached value associated with a key.
Del(ctx context.Context, key string) (val int64, err error)
// MGet retrieves the values of multiple keys.
MGet(ctx context.Context, keys ...string) (map[string]any, error)
// MSet sets multiple key-value pairs in the cache.
MSet(ctx context.Context, value map[string]any, expire time.Duration) error
// Nil returns an error indicating that the key does not exist.
Nil() error
}
4.3 多级缓存自由组合
多级缓存是 jetcache-go 最具特色的能力,让我们可以非常方便快速的构建缓存积木,从而用最低成本提供最高的性能。
4.4 缓存自动刷新
缓存刷新配置及使用示例:
mycache := cache.New(cache.WithName("any"),
// ...
// cache.WithRefreshDuration sets the asynchronous refresh interval
cache.WithRefreshDuration(time.Minute),
// cache.WithStopRefreshAfterLastAccess sets the time to cancel the refresh task after the cache key is not accessed
cache.WithStopRefreshAfterLastAccess(time.Hour))
// `Once` interface starts automatic refresh by `cache.Refresh(true)`
err := mycache.Once(ctx, key, cache.Value(obj), cache.Refresh(true), cache.Do(func(ctx context.Context) (any, error) {
return mockDBGetObject(1)
}))
缓存自动刷新原理
4.5 稀疏列表缓存实现(MGet)
普通查询逻辑
MGet查询逻辑
MGet 通过 golang的泛型机制 + Load 函数,非常友好的多级缓存批量查询ID对应的实体。如果缓存是redis或者多级缓存最后一级是redis,查询时采用Pipeline实现读写操作,提升性能。查询未命中本地缓存,需要去查询Redis和DB时,会对Key排序,并采用单飞模式(singleflight)调用。需要说明是,针对异常场景(IO异常、序列化异常等),我们设计思路是尽可能提供有损服务,防止穿透。
4.6 缓存更新监听器-解决缓存一致性问题
缓存更新监听器,支持拓展缓存更新后所有GO进程的本地缓存失效,实现本地缓存一致性,避免数据波动。JetCache-Go抽象了5种事件:Set、SetByOnce、SetByRefresh、SetByMGet、Delete。
缓存监听广播流程
通过 redis 的pub/sub实现缓存广播示例:
当然,你也可以选择通过其他消息队列组件去实现,事件和方法我们都提供了。
4.7 缓存高可用 - 自动降级
4.8 缓存指标统计
支持实现stats.Handler接口并注册到Cache组件来自定义收集指标,例如使用Prometheus 采集指标。
我们默认实现了通过日志打印统计指标,如下所示:
2023/09/11 16:42:30.695294 statslogger.go:178: [INFO] jetcache-go stats last 1m0s.
cache | qpm| hit_ratio| hit| miss| query| query_fail
------------+------------+------------+------------+------------+------------+------------
bench | 216440123| 100.00%| 216439867| 256| 256| 0|
bench_local | 216440123| 100.00%| 216434970| 5153| -| -|
bench_remote| 5153| 95.03%| 4897| 256| -| -|
------------+------------+------------+------------+------------+------------+------------
实现stats.Handler接口并将指标采集到Prometheus。
统计维度为 Cache 实例对应的名称,建议��考上文的 graph、part 设置不同的缓存实例,就会有不同的缓存统计指标。
五、总结
JetCache-go 实现了核心功能跟阿里巴巴开源的 JetCache 缓存框架对齐,并在公司内部大规模推广,也接入了很多重要业务。最显著的效果是:提高了研发效率,也提升了服务的性和稳定性。
JetCache-go 虽然取得了阶段性的成绩,但是,仍然存在一些不足:
我们会持续的迭代,也希望更多的小伙伴一起参与共建!