竹简文档
缓存

HashCache

基于哈希数据结构的二维键值对缓存接口

HashCache

HashCache 定义了基于哈希(Hash)数据结构的缓存操作接口,用于管理二维键值对数据。

接口定义

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

泛型参数

字段

类型

注意: 字段键类型 F 必须是可比较的(comparable),因为需要作为 map 的键。

数据结构

哈希结构为:key → field → value

user:123 → {
    "name": "张三",
    "email": "zhangsan@example.com",
    "age": "25"
}

方法说明

Get

获取指定字段的值。

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

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • field - 字段键

返回值:

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

Set

设置单个字段的值。v1.1.0 起尾部新增 opts ...SetOption,可在单次调用中覆盖默认 TTL 或附加写入条件。

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

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • field - 字段键
  • value - 指向值的指针
  • opts - 写操作选项(可选)。常用:
    • xCache.WithTTL(ttl) — 覆盖本次写入的过期时间
    • xCache.WithNX() / WithXX() — 仅当 key 不存在 / 已存在时写入
    • xCache.WithNoSlide() — 追加字段但不延长 key 的整体 TTL(哈希场景常用,避免追加操作意外续期)

返回值:

  • error - 错误信息

示例:

// 沿用默认 TTL
_ = hc.Set(ctx, "config:1", "theme", &theme)

// 追加字段但不滑动 TTL(不延长 key 整体过期时间)
_ = hc.Set(ctx, "config:1", "lang", &lang, xCache.WithNoSlide())

GetAll

获取哈希表中的所有字段和值。

GetAll(ctx context.Context, key K) (map[F]V, error)

参数:

  • ctx - context.Context 上下文
  • key - 哈希键

返回值:

  • map[F]V - 所有字段和值的映射
  • error - 错误信息

GetAllStruct

获取哈希表中的所有字段和值,直接映射到指定结构体。

GetAllStruct(ctx context.Context, key K) (S, error)

参数:

  • ctx - context.Context 上下文
  • key - 哈希键

返回值:

  • S - 结构体映射结果
  • error - 错误信息

SetAll

批量设置多个字段的值。v1.1.0 起尾部新增 opts ...SetOption,可在单次调用中覆盖默认 TTL 或附加写入条件。

SetAll(ctx context.Context, key K, fields map[F]*V, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • fields - 字段和值的映射
  • opts - 写操作选项(可选),同 Set

返回值:

  • error - 错误信息

SetAllStruct

批量设置多个字段的值,使用指定结构体进行写入。v1.1.0 起尾部新增 opts ...SetOption,可在单次调用中覆盖默认 TTL 或附加写入条件。

SetAllStruct(ctx context.Context, key K, value S, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • value - 结构体数据
  • opts - 写操作选项(可选),同 Set

返回值:

  • error - 错误信息

Exists

检查指定字段是否存在。

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

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • field - 字段键

返回值:

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

Remove

从哈希表中移除指定的字段。

Remove(ctx context.Context, key K, fields ...F) error

参数:

  • ctx - context.Context 上下文
  • key - 哈希键
  • fields - 要移除的字段列表(可变参数)

返回值:

  • error - 错误信息

Delete

删除整个哈希表。

Delete(ctx context.Context, key K) error

参数:

  • ctx - context.Context 上下文
  • key - 哈希键

返回值:

  • error - 错误信息

实现示例

用户配置缓存

通过 Manager 创建泛型 HashCache,无需自行封装底层 Redis 操作。

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

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

type UserConfig struct {
    Theme    string `json:"theme"`
    Language string `json:"language"`
    Timezone string `json:"timezone"`
}

// NewUserConfigCache 从上下文中获取 Manager 并创建泛型哈希缓存
//
// 泛型参数:[K, F, V, S]
//   - K: 键类型,string
//   - F: 字段键类型,string
//   - V: 字段值类型,string
//   - S: 结构体类型,UserConfig
func NewUserConfigCache(ctx context.Context) xCache.HashCache[string, string, string, UserConfig] {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    return xCache.HashCacheOf[string, string, string, UserConfig](manager)
}

使用配置缓存

service/user_config.go
type UserConfigService struct {
    hc xCache.HashCache[string, string, string, UserConfig]
}

func NewUserConfigService(ctx context.Context) *UserConfigService {
    return &UserConfigService{
        hc: NewUserConfigCache(ctx),
    }
}

// GetTheme 获取用户主题
func (s *UserConfigService) GetTheme(ctx context.Context, userID string) (string, error) {
    theme, exists, err := s.hc.Get(ctx, userID, "theme")
    if err != nil {
        return "", err
    }
    if !exists || theme == nil {
        return "default", nil
    }
    return *theme, nil
}

// UpdateTheme 更新用户主题
func (s *UserConfigService) UpdateTheme(ctx context.Context, userID string, theme string) error {
    // 更新数据库
    if err := s.updateThemeInDB(userID, theme); err != nil {
        return err
    }

    // 更新缓存(沿用默认 TTL)
    return s.hc.Set(ctx, userID, "theme", &theme)
}

// AppendLanguage 追加语言字段,但不滑动 TTL(不延长 key 整体过期时间)
func (s *UserConfigService) AppendLanguage(ctx context.Context, userID string, lang string) error {
    return s.hc.Set(ctx, userID, "lang", &lang, xCache.WithNoSlide())
}

// GetAllConfig 获取所有配置
func (s *UserConfigService) GetAllConfig(ctx context.Context, userID string) (map[string]string, error) {
    config, err := s.hc.GetAll(ctx, userID)
    if err != nil {
        return nil, err
    }

    // 如果缓存为空,从数据库加载
    if len(config) == 0 {
        loaded, err := s.loadConfigFromDB(userID)
        if err != nil {
            return nil, err
        }

        // 写入缓存,覆盖默认 TTL 为 10 分钟
        fields := make(map[string]*string)
        for k, v := range loaded {
            val := v
            fields[k] = &val
        }
        _ = s.hc.SetAll(ctx, userID, fields, xCache.WithTTL(10*time.Minute))
        return loaded, nil
    }

    return config, nil
}

// GetConfigStruct 以结构体形式获取全部配置
func (s *UserConfigService) GetConfigStruct(ctx context.Context, userID string) (UserConfig, error) {
    cfg, err := s.hc.GetAllStruct(ctx, userID)
    if err != nil {
        return UserConfig{}, err
    }
    return cfg, nil
}

// SaveConfigStruct 以结构体形式批量写入配置,并设置 10 分钟 TTL
func (s *UserConfigService) SaveConfigStruct(ctx context.Context, userID string, cfg UserConfig) error {
    return s.hc.SetAllStruct(ctx, userID, cfg, xCache.WithTTL(10*time.Minute))
}

// ResetConfig 重置配置
func (s *UserConfigService) ResetConfig(ctx context.Context, userID string) error {
    // 删除数据库配置
    if err := s.deleteConfigFromDB(userID); err != nil {
        return err
    }

    // 删除缓存
    return s.hc.Delete(ctx, userID)
}

使用场景

用户配置

type UserConfigCache interface {
    xCache.HashCache[string, string, string, UserConfig]
}

适用于:

  • 用户偏好设置
  • 界面配置
  • 通知设置

商品详情

type ProductCache interface {
    xCache.HashCache[string, string, interface{}, Product]
}

适用于:

  • 商品价格
  • 库存数量
  • 商品属性

会话数据

type SessionCache interface {
    xCache.HashCache[string, string, string, Session]
}

适用于:

  • 用户会话
  • 临时数据
  • 表单状态

最佳实践

1. 字段命名规范

使用清晰的字段名:

// ✅ 好的命名
fields := map[string]*string{
    "theme":    &theme,
    "language": &language,
    "timezone": &timezone,
}

// ❌ 避免的命名
fields := map[string]*string{
    "t": &theme,
    "l": &language,
    "z": &timezone,
}

2. 批量操作优化

优先使用 GetAllSetAll

// ✅ 批量获取
config, _ := cache.GetAll(ctx, userID)

// ❌ 逐个获取
theme, _ := cache.Get(ctx, userID, "theme")
language, _ := cache.Get(ctx, userID, "language")
timezone, _ := cache.Get(ctx, userID, "timezone")

3. 部分更新

只更新变化的字段:

func (s *UserConfigService) UpdatePartial(ctx context.Context, userID string, updates map[string]string) error {
    fields := make(map[string]*string)
    for k, v := range updates {
        val := v
        fields[k] = &val
    }
    return s.hc.SetAll(ctx, userID, fields)
}

4. 字段删除

使用 Remove 而不是 Delete

// ✅ 删除特定字段
cache.Remove(ctx, userID, "theme", "language")

// ❌ 删除整个哈希表(除非确实需要)
cache.Delete(ctx, userID)

性能优化

使用 Pipeline

批量操作多个哈希表,通过 manager.Redis() 获取底层客户端:

func SetMultipleUsers(ctx context.Context, configs map[string]map[string]*string) error {
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    pipe := manager.Redis().Pipeline()

    for userID, fields := range configs {
        key := "user:config:" + userID
        data := make(map[string]interface{})
        for field, value := range fields {
            if value != nil {
                data[field] = *value
            }
        }
        pipe.HSet(ctx, key, data)
    }

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

字段数量控制

避免单个哈希表字段过多:

// ✅ 合理的字段数量(< 100)
user:config:123 → {
    "theme": "dark",
    "language": "zh-CN",
    "timezone": "Asia/Shanghai"
}

// ❌ 字段过多(> 1000)
user:data:123 → {
    "field1": "value1",
    "field2": "value2",
    // ... 1000+ fields
}

注意事项

  1. 字段类型限制:字段键必须是 comparable 类型
  2. 内存占用:大量字段会占用较多内存,考虑分片
  3. 原子性:单个字段操作是原子的,但多字段操作不保证原子性
  4. 过期时间:哈希表整体过期,无法为单个字段设置 TTL
  5. 序列化:复杂值类型需要序列化为字符串存储

On this page