golang预取式乐观缓存策略插件库pocache的使用
golang预取式乐观缓存策略插件库pocache的使用
Pocache (poh-cash (/poʊ kæʃ/)
),即预取式乐观缓存,是一个轻量级的应用内缓存包。它引入了预取式缓存更新机制,在并发环境中通过减少冗余数据库调用来优化性能,同时保持数据新鲜。它使用Hashicorp的Go LRU包作为默认存储。
这是解决著名的惊群问题的另一种优雅方案,保护你的数据库!
关键特性
- 预取式缓存更新:自动更新即将过期的缓存条目
- 阈值窗口:可配置的在缓存过期前触发更新的时间窗口
- 提供过期缓存:可选配置以提供过期缓存并在后台刷新
- 防抖更新:通过对相同键的并发更新请求进行防抖来防止过多的I/O调用
- 自定义存储:可自定义底层存储以扩展/替换应用内缓存或使用外部缓存数据库
为什么使用Pocache?
在高并发环境(如Web服务器)中,多个请求会同时尝试访问相同的缓存条目。如果没有查询调用抑制/防抖机制,应用会多次查询底层数据库直到缓存刷新。在尝试解决惊群问题时,大多数应用会提供过期缓存直到更新完成。
Pocache通过结合防抖机制和阈值窗口内的乐观更新来解决这些场景,保持缓存始终最新,永远不需要提供过期缓存!
工作原理
给定缓存过期时间和阈值窗口,当在阈值窗口内访问值时,Pocache会触发预取式缓存更新。
示例:
- 缓存过期时间:10分钟
- 阈值窗口:1分钟
|______________________ ____threshold window__________ ______________|
0 min 9 mins 10 mins
Add key here Get key within window Key expires
当在阈值窗口内(9-10分钟之间)获取键时,Pocache会为该键启动后台更新(预取式)。这确保了新鲜数据的可用性,预期未来使用(乐观式)。
自定义存储
Pocache为底层存储定义了以下接口。只要实现了这个简单接口,你就可以配置你选择的存储,并作为配置提供。
type store[K comparable, T any] interface {
Add(key K, value *Payload[T]) (evicted bool)
Get(key K) (value *Payload[T], found bool)
Remove(key K) (present bool)
}
下面是一个设置自定义存储的示例(不用于生产环境):
type mystore[Key comparable, T any] struct{
data sync.Map
}
func (ms *mystore[K,T]) Add(key K, value *Payload[T]) (evicted bool) {
ms.data.Store(key, value)
}
func (ms *mystore[K,T]) Get(key K) (value *Payload[T], found bool) {
v, found := ms.data.Load(key)
if !found {
return nil, found
}
value, _ := v.(*Payload[T])
return value, true
}
func (ms *mystore[K,T]) Remove(key K) (present bool) {
_, found := ms.data.Load(key)
ms.data.Delete(key)
return found
}
func foo() {
cache, err := pocache.New(pocache.Config[string, string]{
Store: mystore{data: sync.Map{}}
})
}
完整示例
package main
import (
"context"
"fmt"
"time"
"github.com/naughtygopher/pocache"
)
type Item struct {
ID string
Name string
Description string
}
func newItem(key string) *Item {
return &Item{
ID: fmt.Sprintf("%d", time.Now().Nanosecond()),
Name: "name::" + key,
Description: "description::" + key,
}
}
func updater(ctx context.Context, key string) (*Item, error) {
return newItem(key), nil
}
func onErr(err error) {
panic(fmt.Sprintf("this should never have happened!: %+v", err))
}
func main() {
cache, err := pocache.New(pocache.Config[string, *Item]{
// LRUCacheSize 是要在缓存中维护的键数量(可选,默认1000)
LRUCacheSize: 100000,
// QLength 是更新和删除队列的长度(可选,默认1000)
QLength: 1000,
// CacheAge 是缓存将被维护的时间,除了LRU淘汰
// 这是为了在键没有基于LRU被淘汰时不维护过时数据
// (可选,默认1分钟)
CacheAge: time.Hour,
// Threshold 是在到期前的时间,此时键被认为有资格被更新
// (可选,默认1秒)
Threshold: time.Minute * 5,
// ServeStale 不会在缓存过期时返回错误。它会返回过期值,
// 并触发更新。这对于可以接受过期值且数据一致性不是
// 至关重要的用例很有用。
// (可选,默认false)
ServeStale: false,
// UpdaterTimeout 是调用更新器函数时的上下文超时
// (可选,默认1秒)
UpdaterTimeout: time.Second * 15,
// Updater 是可选的,但没有它就是一个基本的LRU缓存
Updater: updater,
// ErrWatcher 在尝试更新缓存时出现任何错误时调用(可选)
ErrWatcher: onErr,
})
if err != nil {
panic(err)
}
const key = "hello"
item := newItem(key)
e := cache.Add(key, item)
fmt.Println("evicted:", e)
ee := cache.BulkAdd([]pocache.Tuple[string, *Item]{
{Key: key + "2", Value: newItem(key + "2")},
})
fmt.Println("evicted list:", ee)
ii := cache.Get(key)
if ii.Found {
fmt.Println("value:", ii.V)
}
ii = cache.Get(key + "2")
if ii.Found {
fmt.Println("value:", ii.V)
}
}
Gopher吉祥物
这里使用的Gopher是用Gopherize.me创建的。Pocache帮助你阻止惊群的发生。
更多关于golang预取式乐观缓存策略插件库pocache的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang预取式乐观缓存策略插件库pocache的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Go预取式乐观缓存策略插件库pocache使用指南
概述
pocache是一个Go语言实现的预取式乐观缓存策略插件库,它通过预取机制和乐观锁策略来提高缓存命中率,减少缓存穿透问题。下面我将详细介绍其使用方法和示例代码。
安装
go get github.com/pocache/pocache
核心概念
- 预取机制:在缓存项过期前自动异步刷新
- 乐观锁策略:多个请求可以同时获取旧值,只有一个请求会执行实际加载
- 缓存穿透保护:对空值也进行缓存
基本使用示例
package main
import (
"context"
"fmt"
"time"
"github.com/pocache/pocache"
)
func main() {
// 创建缓存实例
cache := pocache.New(
pocache.WithExpiration(5*time.Minute), // 缓存过期时间
pocache.WithPreloadTime(1*time.Minute), // 提前1分钟预加载
pocache.WithRefreshConcurrency(10), // 最大并发刷新数
)
// 定义数据加载函数
loader := func(ctx context.Context, key string) (interface{}, error) {
// 模拟从数据库或其他数据源加载数据
fmt.Printf("Loading data for key: %s\n", key)
return fmt.Sprintf("value_for_%s", key), nil
}
// 获取缓存值
val, err := cache.Get(context.Background(), "test_key", loader)
if err != nil {
panic(err)
}
fmt.Println("Got value:", val)
}
高级特性
1. 批量获取
keys := []string{"key1", "key2", "key3"}
loader := func(ctx context.Context, key string) (interface{}, error) {
return fmt.Sprintf("value_for_%s", key), nil
}
results, err := cache.MGet(context.Background(), keys, loader)
if err != nil {
panic(err)
}
for key, val := range results {
fmt.Printf("%s: %v\n", key, val)
}
2. 自定义缓存存储
// 实现Storage接口
type customStorage struct{}
func (s *customStorage) Get(key string) (interface{}, bool) {
// 自定义获取逻辑
}
func (s *customStorage) Set(key string, value interface{}, expiration time.Duration) {
// 自定义设置逻辑
}
func (s *customStorage) Delete(key string) {
// 自定义删除逻辑
}
// 使用自定义存储
cache := pocache.New(
pocache.WithStorage(&customStorage{}),
)
3. 指标监控
// 实现Metrics接口
type customMetrics struct{}
func (m *customMetrics) IncHit() {
// 缓存命中
}
func (m *customMetrics) IncMiss() {
// 缓存未命中
}
func (m *customMetrics) IncRefresh() {
// 缓存刷新
}
// 使用自定义指标
cache := pocache.New(
pocache.WithMetrics(&customMetrics{}),
)
最佳实践
- 合理设置预加载时间:根据数据变化频率设置,高频变化数据预加载时间应更短
- 处理加载错误:在loader函数中妥善处理错误,避免缓存污染
- 监控缓存命中率:通过Metrics接口监控缓存效果
- 考虑内存限制:大数据集应考虑使用WithMaxSize选项
性能优化示例
cache := pocache.New(
pocache.WithExpiration(10*time.Minute),
pocache.WithPreloadTime(2*time.Minute),
pocache.WithRefreshConcurrency(100), // 高并发场景可增大
pocache.WithShards(32), // 分片减少锁竞争
pocache.WithMaxSize(10000), // 限制最大缓存项
pocache.WithMetrics(&prometheusMetrics{}), // 使用Prometheus监控
)
注意事项
- loader函数应该是幂等的,因为可能会被并发调用
- 对于热点数据,可以适当缩短预加载时间
- 缓存对象应该是不可变的,否则可能导致并发问题
pocache通过其预取和乐观锁机制,能够有效提高缓存命中率,减少缓存穿透,是构建高性能Go应用的理想缓存解决方案。