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=string、V=string 的消息队列为例。
创建泛型 ListCache 实例
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())使用消息队列
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()
}注意事项
- 索引范围:
Range的end是包含的,不同于 Go 切片 - 负数索引:
-1表示最后一个元素,-2表示倒数第二个 - 空列表:弹出空列表返回
nil,不是错误 - 内存占用:大列表会占用较多内存,考虑分片
- 性能:头部插入/删除比尾部慢,优先使用尾部操作
- 过期时间:整个列表过期,无法为单个元素设置 TTL