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 三种数据结构在运行时根据数据特征自动转换底层编码的决策路径:
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 两种编码,是对象级缓存的理想选择,可避免序列化开销。
- 在读写分离架构中,严格区分命令的读写属性,将写操作路由到主节点,读操作路由到从节点,是保证系统稳定性的基础。