4. Redis 基本数据结构(上):String、List、Hash 的底层实现与常用命令

Redis 之所以能成为缓存领域的基石,核心在于其丰富且高效的数据结构。本章深入剖析最常用的三种基本类型:String、List 和 Hash,从底层编码到实战命令,帮助你在读写分离架构中做出精准的数据建模决策。

4.1 String:最基础的缓存单元

String 是 Redis 中最简单的类型,但也是吞吐量最高、应用最广的类型。其值可以是字符串、数字或二进制数据(如图片序列化后的字节流),最大容量为 512 MB。

4.1.1 底层编码

Redis 根据存储数据的长度和内容,动态选择三种编码方式:

编码方式 触发条件 数据结构 优势
int 值可解析为 64 位有符号整数 直接使用 long 存储 整数运算无需转换,极低内存开销
embstr 字符串长度 ≤ 44 字节 一次性分配 redisObject + sdshdr 减少内存碎片,读写效率高
raw 字符串长度 > 44 字节 分两次分配 redisObject 和 sdshdr 支持动态扩容,适合大文本
实战建议:在缓存短文本(如 Session Token、验证码)时,尽量控制在 44 字节以内,可触发 embstr 编码,减少内存分配次数,提升读写性能。

4.1.2 常用命令

# 基本读写
SET key value [NX|XX] [EX seconds] [PX milliseconds]
GET key
GETSET key value

# 批量操作(减少网络 RTT)
MSET key1 value1 key2 value2
MGET key1 key2

# 数值操作(原子性)
INCR key
INCRBY key increment
DECR key
DECRBY key decrement

# 字符串操作
APPEND key value
STRLEN key
GETRANGE key start end
SETRANGE key offset value
读写分离场景:在缓存层,String 常用于存储 热点数据(如用户信息 JSON 串)和 计数器(如 PV/UV)。写操作(SET/INCR)路由到主节点,读操作(GET/MGET)路由到从节点,可显著降低主节点负载。

4.2 List:有序队列的基石

List 按插入顺序存储字符串元素,底层支持双向链表和压缩列表两种编码,是实现消息队列、时间线(Timeline)和最新动态列表的理想选择。

4.2.1 底层编码

Redis 3.2 之后,List 底层统一采用 quicklist 结构,它是 ziplist(压缩列表)与 linkedlist(双向链表)的混合体:

  • ziplist:内存连续,节省空间,适合小数据量(默认每个节点不超过 8 KB)。
  • linkedlist:双向指针,插入删除高效,适合大数据量或频繁修改。
  • quicklist:将多个 ziplist 通过双向链表串联,兼顾内存紧凑与操作效率。
注意:当 List 元素个数较少或单个元素较小时,Redis 会使用 ziplist 编码;当元素数量超过 list-max-ziplist-size(默认 -2,即 8 KB)时,自动转换为 linkedlist。合理配置该参数可避免内存膨胀。

4.2.2 常用命令

# 插入操作
LPUSH key value [value ...]   # 左端插入
RPUSH key value [value ...]   # 右端插入
LINSERT key BEFORE|AFTER pivot value  # 在指定元素前后插入

# 弹出操作
LPOP key                      # 左端弹出
RPOP key                      # 右端弹出
BLPOP key [key ...] timeout   # 阻塞式左端弹出
BRPOP key [key ...] timeout   # 阻塞式右端弹出

# 范围查询
LRANGE key start stop         # 获取指定范围元素
LINDEX key index              # 获取指定索引元素
LLEN key                      # 获取列表长度

# 修剪操作
LTRIM key start stop          # 保留指定范围元素
读写分离场景:List 常用于 消息队列(生产者 LPUSH,消费者 BRPOP)。在读写分离架构中,生产者写操作(LPUSH/RPUSH)必须路由到主节点;消费者读操作(LRANGE/LLEN)可路由到从节点,但阻塞弹出(BLPOP/BRPOP)仍需在主节点执行,以保证原子性。

4.3 Hash:对象级缓存的利器

Hash 类似于 Java 的 HashMap 或 Python 的 dict,适合存储结构化对象(如用户资料、商品详情),支持对单个字段进行原子操作,避免序列化整个对象的开销。

4.3.1 底层编码

Hash 根据字段数量和值大小,选择两种编码:

编码方式 触发条件 数据结构 优势
ziplist 字段数 < 512 且每个字段值 < 64 字节 连续内存存储 key-value 对 内存占用极低,适合小对象
hashtable 超出 ziplist 阈值 字典 + 拉链法解决冲突 O(1) 读写,适合大对象
性能对比:对于包含 10 个字段的用户对象,使用 Hash 存储比使用 String 序列化整个 JSON 节省约 30% 内存,且更新单个字段时无需反序列化整个对象。

4.3.2 常用命令

# 基本读写
HSET key field value [field value ...]
HGET key field
HMSET key field value [field value ...]  # 已废弃,建议用 HSET
HMGET key field [field ...]

# 批量操作
HGETALL key                   # 获取所有字段和值(慎用于大 Hash)
HKEYS key                     # 获取所有字段名
HVALS key                     # 获取所有字段值
HLEN key                      # 获取字段数量

# 原子增减
HINCRBY key field increment
HINCRBYFLOAT key field increment

# 删除与存在性
HDEL key field [field ...]
HEXISTS key field

4.4 三种数据结构对比与选型

特性 String List Hash
存储模型 键值对 有序列表 字段-值映射
最大容量 512 MB 2³² - 1 个元素 2³² - 1 个字段
读写复杂度 O(1) O(n) 索引访问,O(1) 头尾操作 O(1) 单字段,O(n) 全量
内存效率 高(embstr 编码) 中(quicklist 平衡) 高(ziplist 编码)
适用场景 缓存、计数器、分布式锁 消息队列、最新动态、日志 对象缓存、用户资料、配置
读写分离建议 写主读从 写主读从(阻塞操作在主) 写主读从

4.5 底层编码转换流程图

下图展示了 String、List、Hash 三种数据结构在运行时根据数据特征自动转换底层编码的决策路径:

Redis 基本数据结构底层编码转换流程 数据写入 Redis String 类型 判断值类型 整数 → int 编码 ≤44 字节 → embstr >44 字节 → raw List 类型 判断数据量 小数据 → ziplist 大数据 → linkedlist 统一为 quicklist Hash 类型 判断字段数与大小 字段<512 且值<64B → ziplist 编码 否则 → hashtable 内存高效存储

4.6 读写分离架构下的命令路由策略

在缓存读写分离架构中,正确路由命令是保证数据一致性和性能的关键。下表总结了三种数据结构的命令路由建议:

数据结构 必须路由到主节点的命令 可路由到从节点的命令 注意事项
String SET, SETNX, INCR, DECR, APPEND GET, MGET, STRLEN, GETRANGE INCR 等原子操作必须在主节点执行
List LPUSH, RPUSH, LPOP, RPOP, LINSERT, LTRIM LRANGE, LINDEX, LLEN, LREM(只读场景) BLPOP/BRPOP 阻塞操作必须在主节点
Hash HSET, HDEL, HINCRBY, HMSET HGET, HMGET, HGETALL, HKEYS, HVALS, HLEN HGETALL 在大 Hash 中慎用,避免从节点阻塞
关键原则:所有 写操作(包括修改、删除、原子增减)必须路由到主节点;只读操作可以路由到从节点。但需注意,Redis 复制是异步的,从节点可能存在秒级延迟,对强一致性要求高的场景应优先读主节点。

4.7 本章小结

  • String 是最轻量的缓存单元,支持 int/embstr/raw 三种编码,适合计数器、Session 和简单 KV 缓存。
  • List 基于 quicklist 实现,兼顾内存与性能,适合消息队列和时间线场景。
  • Hash 支持 ziplist 和 hashtable 两种编码,是对象级缓存的理想选择,可避免序列化开销。
  • 在读写分离架构中,严格区分命令的读写属性,将写操作路由到主节点,读操作路由到从节点,是保证系统稳定性的基础。