Redis List(列表) · 3. 底层编码与内存至8. Go 客户端与最佳实践
本文按 Redis 8.2.10 核对编码与配置默认值;命令的引入版本另行标注。生产配置可能不同,可通过 CONFIG GET 查询。示例面向独立实验服务,运行结果和内存数字未在本次实测。
3. 底层编码与内存
8.2.10 的小 List 可以直接使用 listpack;超过节点容纳限制后转换成 quicklist,其节点保存 listpack 或大元素。不能把“所有 List 都是链表”或“只有 quicklist”作为当前编码结论。
list-max-listpack-size 默认 -2,表示每个紧凑节点目标上限约 8 KiB,正数表示元素数量限制;list-compress-depth 默认 0,关闭内部节点压缩。它们不是整个 List 的长度上限。
编码与内存怎么测
在独立实验实例准备教学键,再读实际状态;下列是测量方法,本次未运行或填写测量数字:
RPUSH learn:types:queue task-a task-b
OBJECT ENCODING learn:types:queue
MEMORY USAGE learn:types:queue固定键名、元素字节长度、Redis 版本和分配器,分别记录 1、10、100 个元素或不同值长度时的内存,再跨越编码阈值观察。单元素的增量开销可用相邻两次内存差估计,编码转换时不能套用线性平均;String/Bitmap 的横轴应使用字节数或最高位偏移。比较“一键多元素”和“多个独立键”时,把键名、对象、字典项和 TTL 管理开销一起计入。
MEMORY USAGE 默认对嵌套值采样,SAMPLES 0 会遍历所有嵌套值,仅在小型隔离实验中使用。其结果不等同于进程 RSS;大量键还会增加全局字典、分配器碎片和复制缓冲等成本,需结合 INFO memory。没有适用于所有版本的固定“每 key 几字节”。
依据:8.2.10 配置、对应类型实现、MEMORY USAGE。
4. 性能与阻塞风险
LRANGE 的深偏移和 LREM/LINSERT 都可能长时间扫描。List 没有 LSCAN;固定大小 LRANGE 可限制输出,但并发插入会移动下标,分页不保证稳定快照。BLPOP 等待挂起的是客户端;长时间的计算型命令才会占住服务器执行路径,不要混淆两种“阻塞”。
常规命令执行主要占用主执行路径,耗时计算会拖慢同实例其他请求;网络 I/O、后台持久化和释放内存有其他线程/进程参与。命令复杂度不包含所有网络、分配、编码转换与客户端成本。
5. 原子性与并发
LPUSH 和 LTRIM 分别原子,但组合不是一次操作;严格限长可用 MULTI/EXEC 或 Lua。LMOVE 原子移入处理中队列,仍需业务确认、超时和重投。处理记录若只有相同消息体,没有唯一任务 ID,重试与确认会难以区分。
MULTI/EXEC 保证排队命令连续执行,但执行时单条报错不会回滚其他已成功命令;WATCH 是乐观冲突检测,需要有界重试。Lua 也不会自动回滚运行错误前的写入,应先校验参数与类型,再修改状态。
6. 过期、持久化与生命周期
只有整键 TTL,没有单元素 TTL。聊天记录需 LTRIM、队列需背压和过期任务处理;给共享聊天键续 TTL 会影响整组记录。弹出最后一个元素后键被删除。长列表删除、快照和复制也需要容量预算。
大键会增加 RDB/AOF 重写、复制全量同步和恢复时的处理量,fork 写时复制还可能增加峰值内存。单次同步删除聚合对象可能阻塞;UNLINK 先摘除键,把主要回收工作移到后台,但总回收工作和内存压力仍存在,不能替代限长和拆分。
完成教学实验后,只清理自己创建的键,例如 UNLINK learn:types:queue。
7. 集群与扩展
LMOVE/BLMOVE 的源和目标以及多键 BLPOP 要满足同槽条件,例如 jobs:{room1}:ready 与 jobs:{room1}:processing。按房间分队列可分散热点,但跨房间全局先后顺序不再由单一 List 保证。消费读取应面向主节点,副本延迟会让长度和队列状态落后。
一个键只在一个分片中,hash tag 控制相关键进入同一槽,不会把一个大键切开。副本读取通常是最终一致观察;操作后立即读到本次写入的需求应选主节点或明确的协调策略。
8. Go 客户端与最佳实践
下面函数使用 github.com/redis/go-redis/v9,调用方提供 context.Context 和已配置认证、超时的 *redis.Client。
func ListExample(ctx context.Context, rdb *redis.Client, message string) ([]string, error) {
_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.LPush(ctx, "learn:go:chat", message)
pipe.LTrim(ctx, "learn:go:chat", 0, 199)
return nil
})
if err != nil { return nil, err }
return rdb.LRange(ctx, "learn:go:chat", 0, 19).Result()
}List 通常映射 []string。TxPipelined 将这里的两条写命令放入事务;后续 LRange 是另一次读取,可能看到其他请求的更新。检查事务与每条命令的错误,事务执行错误不会自动回滚。阻塞消费应使用有限等待和独立的连接预算。
普通 Pipeline 只是批量发送,不能充当事务;批次过大也会增加响应缓冲与延迟。对超时后的写入重试,需要根据幂等性判断,不能把“没收到响应”当成“没执行”。
继续阅读
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践