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 不重设(等价 RedisSET 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())注意: 需要处理 value 为 nil 的场景。
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,并调用其方法。
用户缓存实现
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,见下方使用缓存。
基本用法
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,避免在请求路径上重复查表:
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 方案,但会失去后端透明性。
注意事项
- TTL 管理:合理设置过期时间,避免缓存雪崩
- 内存占用:大对象考虑压缩或分片存储
- 并发安全:Redis 操作本身是原子的,但业务逻辑需要注意
- 缓存一致性:更新数据库后及时更新或删除缓存