Bootstrap

GoFrame 缓存组件

GoFrame 缓存组件

GoFrame 缓存组件采用了接口化设计,通过 Adapter 接口实现,支持灵活的自定义实现和扩展。

接口设计

Adapter 接口定义

任何实现了 Adapter 接口的对象均可注册到缓存管理对象中。接口定义请参考:

https://pkg.go.dev/github.com/gogf/gf/v2/os/gcache#Adapter

接口操作方法

注册接口实现
// SetAdapter changes the adapter for this cache.
// Be very note that, this setting function is not concurrent-safe, which means you should not call
// this setting function concurrently in multiple goroutines.
func (c *Cache) SetAdapter(adapter Adapter)

具体示例请参考下方 Redis 缓存适配器章节。

获取接口实现
// GetAdapter returns the adapter that is set in current Cache.
func (c *Cache) GetAdapter() Adapter

内存缓存实现

缓存组件默认提供了一个高速的内存缓存实现,CPU 性能损耗在 ns 纳秒级别,操作效率非常高效。

基本使用

package main

import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    var (
        ctx   = gctx.New()
        cache = gcache.New()
    )

    // 设置缓存,不过期
    err := cache.Set(ctx, "k1", "v1", 0)
    if err != nil {
        panic(err)
    }

    // 获取缓存值
    value, err := cache.Get(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(value)

    // 获取缓存大小
    size, err := cache.Size(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(size)

    // 缓存中是否存在指定键名
    b, err := cache.Contains(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(b)

    // 删除并返回被删除的键值
    removedValue, err := cache.Remove(ctx, "k1")
    if err != nil {
        panic(err)
    }
    fmt.Println(removedValue)

    // 关闭缓存对象,让GC回收资源
    if err = cache.Close(ctx); err != nil {
        panic(err)
    }
}

输出结果:

1
true
v1

过期控制

package main

import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
    "time"
)

func main() {
    var ctx = gctx.New()
    
    // 当键名不存在时写入,设置过期时间1秒
    _, err := gcache.SetIfNotExist(ctx, "k1", "v1", time.Second)
    if err != nil {
        panic(err)
    }

    // 打印当前的键名列表
    keys, err := gcache.Keys(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(keys)

    // 打印当前的键值列表
    values, err := gcache.Values(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(values)

    // 获取指定键值,如果不存在时写入,并返回键值
    value, err := gcache.GetOrSet(ctx, "k2", "v2", 0)
    if err != nil {
        panic(err)
    }
    fmt.Println(value)

    // 打印当前的键值对
    data1, err := gcache.Data(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(data1)

    // 等待1秒,以便k1:v1自动过期
    time.Sleep(time.Second)

    // 再次打印当前的键值对,k1:v1已经过期
    data2, err := gcache.Data(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(data2)
}

输出结果:

[k1]
[v1]
v2
map[k1:v1 k2:v2]
map[k2:v2]

GetOrSetFunc 方法说明

GetOrSetFunc 方法用于获取缓存值,当缓存不存在时执行指定的 f func(context.Context) (interface{}, error) 方法,并将结果缓存后返回。

注意事项:

  • GetOrSetFunc 的缓存方法参数 f 在缓存的锁机制外执行
  • f 内部可以嵌套调用 GetOrSetFunc
  • 如果 f 执行耗时较长,高并发时可能被多次执行
  • GetOrSetFuncLock 在锁机制内执行,保证只执行一次,但会占用锁的时间

示例代码:

package main

import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
    "time"
)

func main() {
    var (
        ch    = make(chan struct{}, 0)
        ctx   = gctx.New()
        key   = `key`
        value = `value`
    )
    
    // 启动10个协程并发测试
    for i := 0; i < 10; i++ {
        go func(index int) {
            <-ch
            _, err := gcache.GetOrSetFuncLock(ctx, key, func(ctx context.Context) (interface{}, error) {
                fmt.Println(index, "entered")
                return value, nil
            }, 0)
            if err != nil {
                panic(err)
            }
        }(i)
    }
    close(ch)
    time.Sleep(time.Second)
}

输出结果(随机值):

9 entered

LRU 缓存淘汰

package main

import (
    "fmt"
    "github.com/gogf/gf/v2/os/gcache"
    "github.com/gogf/gf/v2/os/gctx"
    "time"
)

func main() {
    var (
        ctx   = gctx.New()
        cache = gcache.New(2) // 设置LRU淘汰数量为2
    )

    // 添加10个元素,不过期
    for i := 0; i < 10; i++ {
        if err := cache.Set(ctx, i, i, 0); err != nil {
            panic(err)
        }
    }

    // 查看当前缓存状态
    size, err := cache.Size(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(size)

    keys, err := cache.Keys(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(keys)

    // 读取键名1,确保其优先保留
    value, err := cache.Get(ctx, 1)
    if err != nil {
        panic(err)
    }
    fmt.Println(value)

    // 等待自动淘汰(默认1秒检查一次)
    time.Sleep(3 * time.Second)
    
    // 查看淘汰后的缓存状态
    size, err = cache.Size(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(size)

    keys, err = cache.Keys(ctx)
    if err != nil {
        panic(err)
    }
    fmt.Println(keys)
}

输出结果:

[2 3 5 6 7 0 1 4 8 9]
1
2
[1 9]

Redis 缓存适配器

GoFrame 提供了 Redis 缓存适配器实现,特别适用于:

  • 多节点数据一致性场景
  • Session 共享
  • 数据库查询缓存

使用示例

func ExampleCache_SetAdapter() {
    var (
        err         error
        ctx         = gctx.New()
        cache       = gcache.New()
        redisConfig = &gredis.Config{
            Address: "127.0.0.1:6379",
            Db:      9,
        }
        cacheKey   = `key`
        cacheValue = `value`
    )

    // 创建Redis客户端
    redis, err := gredis.New(redisConfig)
    if err != nil {
        panic(err)
    }

    // 创建并设置Redis适配器
    cache.SetAdapter(gcache.NewAdapterRedis(redis))

    // 使用缓存对象设置和获取
    err = cache.Set(ctx, cacheKey, cacheValue, time.Second)
    if err != nil {
        panic(err)
    }
    fmt.Println(cache.MustGet(ctx, cacheKey).String())

    // 使用Redis客户端直接获取
    fmt.Println(redis.MustDo(ctx, "GET", cacheKey).String())
}

输出结果:

value
value

注意事项

1. Clear/Size 等方法的特殊性
  • 相同的 gredis.Config 会连接到相同的 Redis DB
  • Redis 没有数据分组功能
  • 多个 gcache.Cache 对象共享同一个 Redis DB
  • ClearSize 等操作会影响整个 Redis DB
  • 行为与内存缓存不同,请谨慎使用
2. Redis DB 使用建议

建议为不同的业务缓存类型配置不同的 Redis DB,避免与其他业务数据混用。

性能测试

测试环境

  • CPU: Intel® Core™ i5-4460 CPU @ 3.20GHz
  • 内存: 8GB
  • 系统: Ubuntu 16.04 amd64

测试结果

john@john-B85M:~/Workspace/Go/GOPATH/src/github.com/gogf/gf/v2/os/gcache$ go test *.go -bench=".*" -benchmem
goos: linux
goarch: amd64
Benchmark_CacheSet-4                       2000000        897 ns/op      249 B/op        4 allocs/op
Benchmark_CacheGet-4                       5000000        202 ns/op       49 B/op        1 allocs/op
Benchmark_CacheRemove-4                   50000000       35.7 ns/op        0 B/op        0 allocs/op
Benchmark_CacheLruSet-4                    2000000        880 ns/op      399 B/op        4 allocs/op
Benchmark_CacheLruGet-4                    3000000        212 ns/op       33 B/op        1 allocs/op
Benchmark_CacheLruRemove-4                50000000       35.9 ns/op        0 B/op        0 allocs/op
Benchmark_InterfaceMapWithLockSet-4        3000000        477 ns/op       73 B/op        2 allocs/op
Benchmark_InterfaceMapWithLockGet-4       10000000        149 ns/op        0 B/op        0 allocs/op
Benchmark_InterfaceMapWithLockRemove-4    50000000       39.8 ns/op        0 B/op        0 allocs/op
Benchmark_IntMapWithLockWithLockSet-4      5000000        304 ns/op       53 B/op        0 allocs/op
Benchmark_IntMapWithLockGet-4             20000000        164 ns/op        0 B/op        0 allocs/op
Benchmark_IntMapWithLockRemove-4          50000000       33.1 ns/op        0 B/op        0 allocs/op
PASS
ok   command-line-arguments 47.503s

;