golang预取式乐观缓存策略插件库pocache的使用

golang预取式乐观缓存策略插件库pocache的使用

pocache gopher

Pocache (poh-cash (/poʊ kæʃ/)),即预取式乐观缓存,是一个轻量级的应用内缓存包。它引入了预取式缓存更新机制,在并发环境中通过减少冗余数据库调用来优化性能,同时保持数据新鲜。它使用Hashicorp的Go LRU包作为默认存储。

这是解决著名的惊群问题的另一种优雅方案,保护你的数据库!

关键特性

  1. 预取式缓存更新:自动更新即将过期的缓存条目
  2. 阈值窗口:可配置的在缓存过期前触发更新的时间窗口
  3. 提供过期缓存:可选配置以提供过期缓存并在后台刷新
  4. 防抖更新:通过对相同键的并发更新请求进行防抖来防止过多的I/O调用
  5. 自定义存储:可自定义底层存储以扩展/替换应用内缓存或使用外部缓存数据库

为什么使用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

1 回复

更多关于golang预取式乐观缓存策略插件库pocache的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Go预取式乐观缓存策略插件库pocache使用指南

概述

pocache是一个Go语言实现的预取式乐观缓存策略插件库,它通过预取机制和乐观锁策略来提高缓存命中率,减少缓存穿透问题。下面我将详细介绍其使用方法和示例代码。

安装

go get github.com/pocache/pocache

核心概念

  1. 预取机制:在缓存项过期前自动异步刷新
  2. 乐观锁策略:多个请求可以同时获取旧值,只有一个请求会执行实际加载
  3. 缓存穿透保护:对空值也进行缓存

基本使用示例

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{}),
)

最佳实践

  1. 合理设置预加载时间:根据数据变化频率设置,高频变化数据预加载时间应更短
  2. 处理加载错误:在loader函数中妥善处理错误,避免缓存污染
  3. 监控缓存命中率:通过Metrics接口监控缓存效果
  4. 考虑内存限制:大数据集应考虑使用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监控
)

注意事项

  1. loader函数应该是幂等的,因为可能会被并发调用
  2. 对于热点数据,可以适当缩短预加载时间
  3. 缓存对象应该是不可变的,否则可能导致并发问题

pocache通过其预取和乐观锁机制,能够有效提高缓存命中率,减少缓存穿透,是构建高性能Go应用的理想缓存解决方案。

回到顶部