竹简文档
缓存

ListCache

基于列表数据结构的有序可重复元素缓存接口

ListCache

ListCache 定义了基于列表(List)数据结构的缓存操作接口,用于管理有序且允许重复元素的列表数据。

接口定义

type ListCache[K any, V any] interface {
    // v1.1.0 起:入参由变参 values ...V 调整为切片 []V,以便在尾部保留 opts ...SetOption 变参位置
    Prepend(ctx context.Context, key K, values []V, opts ...SetOption) error
    Append(ctx context.Context, key K, values []V, opts ...SetOption) error
    Range(ctx context.Context, key K, start int64, end int64) ([]V, error)
    Index(ctx context.Context, key K, index int64) (*V, error)
    Len(ctx context.Context, key K) (int64, error)
    Pop(ctx context.Context, key K) (*V, error)
    PopLast(ctx context.Context, key K) (*V, error)
    Remove(ctx context.Context, key K, count int64, value V) error
    Delete(ctx context.Context, key K) error
}

泛型参数

字段

类型

数据特点

  • 有序性:元素按插入顺序排列
  • 可重复:允许存储相同的元素
  • 双端操作:支持头尾插入和弹出
message:queue → ["msg1", "msg2", "msg3", "msg2"]
                  ↑                          ↑
                 头部                       尾部

方法说明

Prepend

将一组值插入到列表头部(左侧)。v1.1.0 起入参由变参 values ...V 调整为切片 values []V,并在尾部新增 opts ...SetOption,以便在单次调用中覆盖默认 TTL 或附加写入条件。

Prepend(ctx context.Context, key K, values []V, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 列表键
  • values - 要插入的值切片
  • opts - 写操作选项(可选)。常用:
    • xCache.WithTTL(ttl) — 覆盖本次写入的过期时间
    • xCache.WithNX() / WithXX() — 仅当 key 不存在 / 已存在时写入
    • xCache.WithNoSlide() — 插入数据但不延长 key 的整体 TTL

返回值:

  • error - 错误信息

破坏性变更:升级时需将 lc.Prepend(ctx, key, "a", "b") 改写为 lc.Prepend(ctx, key, []string{"a", "b"})

示例:

// 沿用默认 TTL
_ = lc.Prepend(ctx, "queue:1", []string{"task-1", "task-2"})

// 仅当 key 不存在时写入(首次建队)
_ = lc.Prepend(ctx, "queue:1", []string{"task-1"}, xCache.WithNX())

注意: 多个值按顺序插入,最后一个值会在最前面。

Append

将一组值追加到列表尾部(右侧)。v1.1.0 起入参由变参 values ...V 调整为切片 values []V,并在尾部新增 opts ...SetOption,可在单次调用中覆盖默认 TTL 或附加写入条件。

Append(ctx context.Context, key K, values []V, opts ...SetOption) error

参数:

  • ctx - context.Context 上下文
  • key - 列表键
  • values - 要追加的值切片
  • opts - 写操作选项(可选),同 Prepend

返回值:

  • error - 错误信息

示例:

// 沿用默认 TTL
_ = lc.Append(ctx, "queue:1", []string{"task-3", "task-4"})

// 追加但不滑动 TTL(不延长 key 整体过期时间)
_ = lc.Append(ctx, "queue:1", []string{"task-5"}, xCache.WithNoSlide())

Range

按索引范围获取列表元素。

Range(ctx context.Context, key K, start int64, end int64) ([]V, error)

参数:

  • ctx - context.Context 上下文
  • key - 列表键
  • start - 起始索引(支持负数)
  • end - 结束索引(支持负数)

返回值:

  • []V - 元素切片
  • error - 错误信息

索引说明:

  • 0 表示第一个元素
  • -1 表示最后一个元素
  • -2 表示倒数第二个元素

Index

获取指定索引位置的元素。

Index(ctx context.Context, key K, index int64) (*V, error)

参数:

  • ctx - context.Context 上下文
  • key - 列表键
  • index - 索引位置(支持负数)

返回值:

  • *V - 指向元素的指针
  • error - 错误信息

Len

获取列表的长度。

Len(ctx context.Context, key K) (int64, error)

参数:

  • ctx - context.Context 上下文
  • key - 列表键

返回值:

  • int64 - 列表长度
  • error - 错误信息

Pop

从列表头部弹出一个元素并返回。

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

参数:

  • ctx - context.Context 上下文
  • key - 列表键

返回值:

  • *V - 指向弹出元素的指针
  • error - 错误信息

PopLast

从列表尾部弹出一个元素并返回。

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

参数:

  • ctx - context.Context 上下文
  • key - 列表键

返回值:

  • *V - 指向弹出元素的指针
  • error - 错误信息

Remove

从列表中移除指定数量的匹配元素。

Remove(ctx context.Context, key K, count int64, value V) error

参数:

  • ctx - context.Context 上下文
  • key - 列表键
  • count - 移除数量
    • count > 0:从头部开始移除
    • count < 0:从尾部开始移除
    • count = 0:移除所有匹配项
  • value - 要移除的值

返回值:

  • error - 错误信息

Delete

删除整个列表。

Delete(ctx context.Context, key K) error

参数:

  • ctx - context.Context 上下文
  • key - 列表键

返回值:

  • error - 错误信息

实现示例

ListCache 不需要手写底层实现:通过 Manager 创建泛型实例 lc 即可直接调用所有方法。下面以 K=stringV=string 的消息队列为例。

创建泛型 ListCache 实例

cache/message_queue.go
import (
    "context"

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

// MessageQueueCache 持有一个 string/string 的 ListCache 泛型实例
type MessageQueueCache struct {
    lc xCache.ListCache[string, string]
}

// NewMessageQueueCache 从上下文中的 CacheManager 构造泛型 ListCache
func NewMessageQueueCache(ctx context.Context) *MessageQueueCache {
    // 从请求上下文中获取 Manager
    manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
    // 基于泛型创建 ListCache 实例
    lc := xCache.ListCacheOf[string, string](manager)
    return &MessageQueueCache{lc: lc}
}

// Prepend 在队列头部插入消息(切片形式,可附加 SetOption)
func (c *MessageQueueCache) Prepend(ctx context.Context, queueName string, messages []string) error {
    return c.lc.Prepend(ctx, "queue:"+queueName, messages)
}

// Append 在队列尾部追加消息(切片形式,可附加 SetOption)
func (c *MessageQueueCache) Append(ctx context.Context, queueName string, messages []string) error {
    return c.lc.Append(ctx, "queue:"+queueName, messages)
}

// Range 获取指定范围的消息
func (c *MessageQueueCache) Range(ctx context.Context, queueName string, start, end int64) ([]string, error) {
    return c.lc.Range(ctx, "queue:"+queueName, start, end)
}

// Index 获取指定位置的消息
func (c *MessageQueueCache) Index(ctx context.Context, queueName string, index int64) (*string, error) {
    return c.lc.Index(ctx, "queue:"+queueName, index)
}

// Len 获取队列长度
func (c *MessageQueueCache) Len(ctx context.Context, queueName string) (int64, error) {
    return c.lc.Len(ctx, "queue:"+queueName)
}

// Pop 从队列头部弹出消息
func (c *MessageQueueCache) Pop(ctx context.Context, queueName string) (*string, error) {
    return c.lc.Pop(ctx, "queue:"+queueName)
}

// PopLast 从队列尾部弹出消息
func (c *MessageQueueCache) PopLast(ctx context.Context, queueName string) (*string, error) {
    return c.lc.PopLast(ctx, "queue:"+queueName)
}

// Remove 移除指定消息
func (c *MessageQueueCache) Remove(ctx context.Context, queueName string, count int64, message string) error {
    return c.lc.Remove(ctx, "queue:"+queueName, count, message)
}

// Delete 删除整个队列
func (c *MessageQueueCache) Delete(ctx context.Context, queueName string) error {
    return c.lc.Delete(ctx, "queue:"+queueName)
}

带写入选项的调用

// 沿用默认 TTL
_ = c.lc.Prepend(ctx, "queue:1", []string{"task-1", "task-2"})

// 仅当 key 不存在时写入(首次建队)
_ = c.lc.Prepend(ctx, "queue:1", []string{"task-1"}, xCache.WithNX())

// 追加但不滑动 TTL(不延长 key 整体过期时间)
_ = c.lc.Append(ctx, "queue:1", []string{"task-5"}, xCache.WithNoSlide())

使用消息队列

service/message_queue.go
type MessageQueueService struct {
    cache *MessageQueueCache
}

func NewMessageQueueService(cache *MessageQueueCache) *MessageQueueService {
    return &MessageQueueService{cache: cache}
}

// Enqueue 入队(追加到尾部),单条消息包装为切片
func (s *MessageQueueService) Enqueue(ctx context.Context, queueName string, message string) error {
    return s.cache.Append(ctx, queueName, []string{message})
}

// Dequeue 出队(从头部弹出)
func (s *MessageQueueService) Dequeue(ctx context.Context, queueName string) (*string, error) {
    return s.cache.Pop(ctx, queueName)
}

// Peek 查看队列头部(不弹出)
func (s *MessageQueueService) Peek(ctx context.Context, queueName string) (*string, error) {
    return s.cache.Index(ctx, queueName, 0)
}

// Size 获取队列大小
func (s *MessageQueueService) Size(ctx context.Context, queueName string) (int64, error) {
    return s.cache.Len(ctx, queueName)
}

// GetRecent 获取最近的 N 条消息
func (s *MessageQueueService) GetRecent(ctx context.Context, queueName string, count int64) ([]string, error) {
    return s.cache.Range(ctx, queueName, -count, -1)
}

使用场景

消息队列

type MessageQueueCache interface {
    xCache.ListCache[string, string]
}

适用于:

  • 任务队列
  • 消息队列
  • 事件队列

操作历史

type HistoryCache interface {
    xCache.ListCache[string, string]
}

适用于:

  • 用户操作历史
  • 浏览历史
  • 搜索历史

排行榜

type LeaderboardCache interface {
    xCache.ListCache[string, string]
}

适用于:

  • 实时排行榜
  • 热门列表
  • 推荐列表

栈结构

type StackCache interface {
    xCache.ListCache[string, string]
}

适用于:

  • 撤销/重做功能
  • 状态栈
  • 调用栈

最佳实践

1. 队列模式

使用 Append + Pop 实现 FIFO 队列:

// 入队(切片形式批量追加)
_ = lc.Append(ctx, "queue:1", []string{"msg1", "msg2", "msg3"})

// 出队
msg, _ := lc.Pop(ctx, "queue:1")  // 返回 "msg1"

2. 栈模式

使用 Prepend + Pop 实现 LIFO 栈:

// 入栈(切片形式批量插入)
_ = lc.Prepend(ctx, "stack:1", []string{"item1", "item2", "item3"})

// 出栈
item, _ := lc.Pop(ctx, "stack:1")  // 返回 "item3"

3. 限制列表长度

使用 Append + xCache.WithNoSlide() 控制列表长度而不延长 TTL:

// 追加消息但不滑动 TTL(不延长 key 整体过期时间)
// 配合上游的固定 TTL 与定期 LTrim,可控制列表长度
_ = lc.Append(ctx, "queue:1", []string{message}, xCache.WithNoSlide())

4. 分页获取

使用 Range 实现分页:

func (s *MessageQueueService) GetPage(ctx context.Context, queueName string, page, pageSize int64) ([]string, error) {
    start := (page - 1) * pageSize
    end := start + pageSize - 1
    return s.cache.Range(ctx, queueName, start, end)
}

5. 批量操作

使用切片一次性批量插入,避免逐条调用:

// ✅ 批量追加(单次调用,使用切片)
_ = lc.Append(ctx, "queue:1", []string{"msg1", "msg2", "msg3"})

// ❌ 逐个追加(多次网络往返)
_ = lc.Append(ctx, "queue:1", []string{"msg1"})
_ = lc.Append(ctx, "queue:1", []string{"msg2"})
_ = lc.Append(ctx, "queue:1", []string{"msg3"})

高级操作

以下为底层扩展用法ListCache 接口未直接暴露的 Redis 原生命令(阻塞弹出、列表间移动、按位置插入等)可通过 manager.Redis() 获取底层 *redis.Client,再调用 go-redis 原生 API 实现。这类调用绕过了 Manager 的封装,需自行处理错误与 TTL。

// 从上下文获取 Manager,再取出底层 redis 客户端
manager := ctx.Value(xCtx.CacheManagerKey).(*xCache.Manager)
rdb := manager.Redis()

阻塞弹出

实现阻塞队列(等待元素):

// 阻塞弹出(等待 5 秒)
func BlockingPop(ctx context.Context, rdb *redis.Client, queueName string, timeout time.Duration) (*string, error) {
    key := "queue:" + queueName
    result, err := rdb.BLPop(ctx, timeout, key).Result()
    if err == redis.Nil {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    if len(result) < 2 {
        return nil, nil
    }
    return &result[1], nil
}

列表间移动

将元素从一个列表移动到另一个:

// 从源队列弹出并推入目标队列
func Move(ctx context.Context, rdb *redis.Client, srcQueue, dstQueue string) error {
    srcKey := "queue:" + srcQueue
    dstKey := "queue:" + dstQueue
    _, err := rdb.RPopLPush(ctx, srcKey, dstKey).Result()
    return err
}

插入到指定位置

在某个元素前后插入:

// 在 pivot 元素之前插入
func InsertBefore(ctx context.Context, rdb *redis.Client, queueName, pivot, value string) error {
    key := "queue:" + queueName
    return rdb.LInsertBefore(ctx, key, pivot, value).Err()
}

// 在 pivot 元素之后插入
func InsertAfter(ctx context.Context, rdb *redis.Client, queueName, pivot, value string) error {
    key := "queue:" + queueName
    return rdb.LInsertAfter(ctx, key, pivot, value).Err()
}

性能优化

使用 Pipeline

批量操作多个列表(底层扩展用法,通过 manager.Redis() 获取 rdb):

func EnqueueMultiple(ctx context.Context, rdb *redis.Client, messages map[string][]string) error {
    pipe := rdb.Pipeline()

    for queueName, msgs := range messages {
        key := "queue:" + queueName
        values := make([]interface{}, len(msgs))
        for i, msg := range msgs {
            values[i] = msg
        }
        pipe.RPush(ctx, key, values...)
    }

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

避免大列表

优先使用 lc.Append 配合 xCache.WithNoSlide() 控制写入,避免因每次追加滑动 TTL 而导致列表无限增长:

// 追加单条消息但不滑动 TTL,配合上游固定 TTL 与定期 LTrim 控制列表长度
_ = lc.Append(ctx, "queue:1", []string{message}, xCache.WithNoSlide())

如需硬性长度上限,可在追加后通过 manager.Redis() 执行 LTrim

const MaxQueueSize = 10000

func TrimQueue(ctx context.Context, rdb *redis.Client, queueName string) error {
    key := "queue:" + queueName
    // 保留最新的 MaxQueueSize 条
    return rdb.LTrim(ctx, key, -MaxQueueSize, -1).Err()
}

注意事项

  1. 索引范围Rangeend 是包含的,不同于 Go 切片
  2. 负数索引-1 表示最后一个元素,-2 表示倒数第二个
  3. 空列表:弹出空列表返回 nil,不是错误
  4. 内存占用:大列表会占用较多内存,考虑分片
  5. 性能:头部插入/删除比尾部慢,优先使用尾部操作
  6. 过期时间:整个列表过期,无法为单个元素设置 TTL

On this page