竹简文档
缓存

KeyCache

基于字符串数据结构的键值对缓存接口

KeyCache

KeyCache 定义了基于字符串(String)数据结构的缓存操作接口,用于管理单一键值对数据。

接口定义

type KeyCache[K any, V any] interface {
    Get(ctx context.Context, key K) (*V, bool, error)
    // v1.1.0 起尾部新增 opts ...SetOption,可在单次调用覆盖默认 TTL 或附加写入条件
    Set(ctx context.Context, key K, value *V, opts ...SetOption) error
    Exists(ctx context.Context, key K) (bool, error)
    Delete(ctx context.Context, key K) error
}

泛型参数

字段

类型

方法说明

Get

根据键检索值。

Get(ctx context.Context, key K) (*V, bool, error)

参数:

  • ctx - context.Context 上下文
  • key - 缓存键

返回值:

  • *V - 指向值的指针(如果存在)
  • bool - 键是否存在
  • error - 错误信息

Set

将键值对存入缓存。v1.1.0 起尾部新增 opts ...SetOption,可在单次调用中覆盖默认 TTL 或附加写入条件。

Set(ctx context.Context, key K, value *V, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 缓存键
  • value - 指向值的指针(可以为 nil)
  • opts - 写操作选项(可选)。可用的选项有:
    • xCache.WithTTL(ttl) — 覆盖本次写入的过期时间(ttl <= 0 表示永久)
    • xCache.WithNX() — 仅当 key 不存在时写入
    • xCache.WithXX() — 仅当 key 已存在时写入
    • xCache.WithKeepTTL() — 保留原 TTL 不重设(等价 Redis SET KEEPTTL
    • xCache.WithNoSlide() — 对 KeyCache.Set 无意义,传入被忽略

返回值:

  • error - 错误信息

示例:

import xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
import "time"

// 默认(沿用 Manager 实例 TTL)
_ = kc.Set(ctx, "user:1", &u)

// 覆盖 TTL 为 5 分钟
_ = kc.Set(ctx, "user:1", &u, xCache.WithTTL(5*time.Minute))

// 仅当不存在时写入(简易分布式锁)
_ = kc.Set(ctx, "lock:order:1", &token, xCache.WithNX(), xCache.WithTTL(30*time.Second))

// 刷新值但保留原 TTL(避免锁被无意延长)
_ = kc.Set(ctx, "lock:order:1", &newToken, xCache.WithKeepTTL())

注意: 需要处理 valuenil 的场景。

Exists

检查指定键是否存在。

Exists(ctx context.Context, key K) (bool, error)

参数:

  • ctx - context.Context 上下文
  • key - 缓存键

返回值:

  • bool - 键是否存在
  • error - 错误信息

Delete

从缓存中移除指定的键。

Delete(ctx context.Context, key K) error

参数:

  • ctx - context.Context 上下文
  • key - 缓存键

返回值:

  • error - 错误信息

实现示例

KeyCache 的实现由 Manager 通过 KeyCacheOf[K, V] 工厂方法分发到对应后端,业务侧无需手写 Get/Set/Exists/Delete。下方示例展示如何从上下文获取 Manager、构造泛型 kc,并调用其方法。

用户缓存实现

cache/user.go
import (
    "context"
    "time"

    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
    xCtx "github.com/bamboo-services/bamboo-base-go/defined/context"
    xCtxUtil "github.com/bamboo-services/bamboo-base-go/common/utility/context"
)

type User struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

// UserCache 是基于 KeyCacheOf 的泛型别名
// K = string(业务键,如用户 ID)
// V = User(值类型,框架内部按指针传递)
type UserCache = xCache.KeyCache[string, User]

// NewUserCache 从上下文获取 Manager 并创建泛型 KeyCache
func NewUserCache(ctx context.Context) UserCache {
    // 推荐方式:通过 xCtxUtil 从上下文获取 Manager(框架保留键 CacheManagerKey)
    manager := xCtxUtil.MustGet[*xCache.Manager](ctx, xCtx.CacheManagerKey)

    // 由 Manager 根据 Type() 分发到对应后端的 KeyCache 实现
    return xCache.KeyCacheOf[string, User](manager)
}

若处于非 HTTP 上下文(如初始化阶段、定时任务),无法从请求上下文获取 Manager,可改为构造期注入 *xCache.Manager,见下方使用缓存

基本用法

handler/user.go
import (
    "context"
    "time"

    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
    "github.com/gin-gonic/gin"
)

func GetUserHandler(c *gin.Context) {
    ctx := c.Request.Context()
    kc := NewUserCache(ctx)

    user := &User{ID: "1", Username: "筱锋", Email: "x@x.com"}

    // 基础用法:沿用默认 TTL(Manager 实例 TTL)
    _ = kc.Set(ctx, "user:1", user)

    // 覆盖 TTL 为 5 分钟
    _ = kc.Set(ctx, "user:1", user, xCache.WithTTL(5*time.Minute))

    // 分布式锁:NX + 短 TTL(仅当 key 不存在时写入)
    token := "token-abc"
    _ = kc.Set(ctx, "lock:order:1", &token, xCache.WithNX(), xCache.WithTTL(30*time.Second))

    // 保留原 TTL 刷新值(避免锁被无意延长)
    newToken := "token-def"
    _ = kc.Set(ctx, "lock:order:1", &newToken, xCache.WithKeepTTL())

    // 读取:返回 *User、是否命中、错误
    u, exists, err := kc.Get(ctx, "user:1")
    if err != nil || !exists {
        // 处理未命中或错误
    }
    _ = u

    // 检查存在
    ok, _ := kc.Exists(ctx, "user:1")

    // 删除
    _ = kc.Delete(ctx, "user:1")
    _ = ok
}

使用缓存

业务服务通常在构造期注入 *xCache.Manager,在方法内部按需通过 KeyCacheOf 创建对应泛型 kc,避免在请求路径上重复查表:

service/user.go
import (
    "context"

    xCache "github.com/bamboo-services/bamboo-base-go/major/cache"
)

type UserService struct {
    // 注入 Manager(而非底层 *redis.Client 或 *xCache.Cache)
    manager *xCache.Manager
}

func NewUserService(manager *xCache.Manager) *UserService {
    return &UserService{manager: manager}
}

// GetUser 获取用户(带缓存)
func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 按需创建泛型 kc(KeyCacheOf 内部有缓存,重复调用开销很小)
    kc := xCache.KeyCacheOf[string, User](s.manager)

    // 先从缓存获取
    user, exists, err := kc.Get(ctx, userID)
    if err != nil {
        return nil, err
    }
    if exists {
        return user, nil
    }

    // 缓存未命中,从数据库查询
    user, err = s.getUserFromDB(userID)
    if err != nil {
        return nil, err
    }

    // 写入缓存(沿用 Manager 默认 TTL)
    _ = kc.Set(ctx, userID, user)

    return user, nil
}

// DeleteUser 删除用户并清除缓存
func (s *UserService) DeleteUser(ctx context.Context, userID string) error {
    // 删除数据库记录
    if err := s.deleteUserFromDB(userID); err != nil {
        return err
    }

    // 删除缓存
    kc := xCache.KeyCacheOf[string, User](s.manager)
    return kc.Delete(ctx, userID)
}

使用场景

用户信息缓存

type UserCache interface {
    xCache.KeyCache[string, User]
}

适用于:

  • 用户基本信息
  • 用户权限信息
  • 用户配置

Token 缓存

type TokenCache interface {
    xCache.KeyCache[string, TokenInfo]
}

适用于:

  • JWT Token
  • 刷新 Token
  • 临时访问凭证

配置缓存

type ConfigCache interface {
    xCache.KeyCache[string, Config]
}

适用于:

  • 系统配置
  • 功能开关
  • 动态参数

最佳实践

1. 统一键命名

KeyCacheOf 不强制业务前缀,建议在调用 kc.Set/Get/Delete 时统一使用业务前缀区分不同类型缓存:

const userKeyPrefix = "user:"

// 写入时统一使用前缀
_ = kc.Set(ctx, userKeyPrefix+"1", &user)

// 读取时保持一致
user, exists, err := kc.Get(ctx, userKeyPrefix+"1")

更复杂的键编码(如带租户、版本号)可通过 xCache.WithKeyEncoder 在 Manager 层统一注入,避免业务侧散落拼键逻辑。

2. 处理 nil 值

KeyCache.Set 的入参为 *V,业务侧建议在写入前显式判空,避免把无效数据写入缓存:

if user == nil {
    return nil
}

_ = kc.Set(ctx, userID, user)

3. 错误处理

kc.Get 通过三元返回值区分"未命中"与"真实错误",调用方应据此决定是否回源:

user, exists, err := kc.Get(ctx, userID)

// 错误需返回,切勿当作未命中处理
if err != nil {
    return nil, err
}

// 未命中不是错误,回源数据库
if !exists {
    user, err = s.getUserFromDB(userID)
    // ...
}

4. 序列化选择

根据场景选择合适的序列化方式:

// JSON - 可读性好,兼容性强
data, _ := json.Marshal(user)

// MessagePack - 性能更好,体积更小
data, _ := msgpack.Marshal(user)

// Protocol Buffers - 强类型,跨语言
data, _ := proto.Marshal(user)

性能优化

批量操作

KeyCache 接口本身不支持批量操作。若需要一次性写入大量键值,可通过 manager.Redis() 直接访问底层 *redis.Client 进行 Pipeline 操作:

底层扩展用法:直接访问 Redis 客户端进行 Pipeline 操作。仅 Redis 后端可用;Memory 后端下 manager.Redis() 返回 nil,调用前应判空或确保部署后端为 Redis。

func (s *UserService) SetBatch(ctx context.Context, users map[string]*User) error {
    // 通过 manager.Redis() 获取底层 *redis.Client(仅 Redis 后端非 nil)
    rdb := s.manager.Redis()
    if rdb == nil {
        // Memory 后端或未启用缓存,回退到逐条 kc.Set 或直接返回
        return errors.New("batch pipeline requires redis backend")
    }

    pipe := rdb.Pipeline()

    for userID, user := range users {
        key := "user:" + userID
        data, _ := json.Marshal(user)
        // 复用 Manager 默认 TTL,保持与 kc.Set 一致的过期语义
        pipe.Set(ctx, key, data, s.manager.TTL())
    }

    _, err := pipe.Exec(ctx)
    return err
}

缓存预热

在系统启动时预加载热点数据。预热阶段通常不在 HTTP 请求中,应使用 context.Background()(或注入了 Manager 的初始化上下文)而非 *gin.Context

func (s *UserService) WarmupCache(ctx context.Context) error {
    // 获取热点用户列表
    hotUsers, err := s.getHotUsers()
    if err != nil {
        return err
    }

    // 由 Manager 创建泛型 KeyCache,复用与请求路径一致的缓存语义
    // ctx 推荐传入 context.Background() 或 Register 返回的初始化上下文
    kc := xCache.KeyCacheOf[string, User](s.manager)

    // 批量写入缓存
    for _, user := range hotUsers {
        _ = kc.Set(ctx, user.ID, user)
    }

    return nil
}

若预热需要绕过序列化、追求极致吞吐,可改用批量操作中的 manager.Redis() + Pipeline 方案,但会失去后端透明性。

注意事项

  1. TTL 管理:合理设置过期时间,避免缓存雪崩
  2. 内存占用:大对象考虑压缩或分片存储
  3. 并发安全:Redis 操作本身是原子的,但业务逻辑需要注意
  4. 缓存一致性:更新数据库后及时更新或删除缓存

On this page