懂 Redis 的数据结构原理(比如 String 是动态字符串、Hash 是压缩列表 / 哈希表),就知道存 “用户信息” 用 Hash 比 String 更省内存;
一、为什么用 Hash 存用户信息更省内存?
场景对比
假设存储用户信息:uid: 1001, name: "张三", age: 25, city: "北京" 方案A:用 String 存储
# 存为多个独立的 String SET user:1001:uid 1001 SET user:1001:name "张三" SET user:1001:age 25 SET user:1001:city "北京"# 或存为 JSON 格式的 String SET user:1001 '{"uid":1001,"name":"张三","age":25,"city":"北京"}'
方案B:用 Hash 存储
HSET user:1001 uid 1001 name "张三" age 25 city "北京"
二、底层数据结构原理对比
1. String 的存储结构(SDS - Simple Dynamic String)
struct sdshdr {int len; // 已使用的字节数int free; // 未使用的字节数char buf[]; // 字节数组 };
- 每个 String 都需要独立的 SDS 头(len + free,至少 8 字节)
- 每个 key 都需要独立的 Redis 对象头(redisObject,16 字节)
typedef struct redisObject {unsigned type:4; // 数据类型(4位)unsigned encoding:4; // 编码方式(4位)unsigned lru:LRU_BITS; // LRU时间(24位)int refcount; // 引用计数(4字节)void *ptr; // 指向实际数据的指针(8字节) } robj;
多个 String 存储的问题:
key1 -> redisObject(16B) + SDS头(8B) + 数据 key2 -> redisObject(16B) + SDS头(8B) + 数据 ... 每个key都有重复的对象头开销!
2. Hash 的存储结构(ZIPLIST 或 HT)
a) ZIPLIST(压缩列表) - 小数据量的默认选择
- 当 Hash 满足条件时(Redis 7.x 默认配置):
- 元素数量 ≤ 512
- 所有 value 长度 ≤ 64 字节
- 会使用压缩列表存储
// 压缩列表结构:紧凑的连续内存块 [zlbytes][zltail][zllen]|[entry1][entry2]...[entryN][zlend]
优点:
- 只有 1 个 redisObject 头
- 字段和值在内存中紧密排列
- 没有指针开销,内存利用率极高
b) HT(哈希表) - 大数据量时自动转换
- 当元素过多或 value 过大时,自动转为哈希表
- 虽然比 ZIPLIST 开销大,但仍然比多个 String 节省内存,因为:
- 只有一个主 key
- 字段名是共享的(复用 SDS)
三、内存占用对比(示例计算)
假设存储 10000 个用户,每个用户 4 个字段: String 方案:
总内存 ≈ 10000 × 4 × (redisObject 16B + SDS头 8B + 数据)≈ 10000 × 4 × 24B = 960,000B(不包含数据内容)≈ 937.5KB 的纯元数据开销!
Hash 方案(ZIPLIST):
总内存 ≈ 10000 × (1个redisObject 16B + ZIPLIST开销)≈ 10000 × 20B = 200,000B≈ 195KB 的元数据开销
节省约 75% 的内存!
四、实战验证
# 1. 测试 String 方案内存占用 127.0.0.1:6379> FLUSHALL 127.0.0.1:6379> SET user:1001:uid 1001 127.0.0.1:6379> SET user:1001:name "张三" 127.0.0.1:6379> SET user:1001:age 25 127.0.0.1:6379> SET user:1001:city "北京" 127.0.0.1:6379> MEMORY USAGE user:1001:uid (integer) 56 # 每个key占用~56字节# 2. 测试 Hash 方案内存占用 127.0.0.1:6379> FLUSHALL 127.0.0.1:6379> HSET user:1001 uid 1001 name "张三" age 25 city "北京" 127.0.0.1:6379> MEMORY USAGE user:1001 (integer) 120 # 整个Hash只占~120字节
五、其他优势
1. 原子操作更方便
# Hash 支持原子操作 HINCRBY user:1001 age 1 # 年龄+1 HSET user:1001 city "上海" # 修改城市 HGETALL user:1001 # 获取全部# String 需要事务或Lua脚本保证原子性 MULTI SET user:1001:city "上海" EXEC
2. 网络开销更小
# 获取用户所有信息 # Hash:1次网络往返 HGETALL user:1001# String:4次网络往返 或 管道 MGET user:1001:uid user:1001:name user:1001:age user:1001:city
3. 序列化/反序列化更高效
- String(JSON) 方案需要序列化/反序列化整个对象
- Hash 可以按需获取部分字段
六、什么时候用 String 更好?
虽然 Hash 通常更优,但 String 在以下场景更适合:
- 需要设置过期时间:Hash 只能整体过期,String 可以分别控制
- 需要 incr/decr 操作:String 的原子计数器更简单
- 超大 value:单个字段 value 特别大时
- 需要利用 String 的其他特性:如 bitmap、JSON 嵌套查询等
七、最佳实践建议
- 优先使用 Hash 存储对象类型数据
- 控制 Hash 的 field 数量,避免从 ZIPLIST 转成 HT
- 小对象用 ZIPLIST,大对象考虑分拆
- 监控内存:
redis-cli --bigkeys或MEMORY USAGE - 合理配置:
# redis.conf hash-max-ziplist-entries 512 # 元素数量限制 hash-max-ziplist-value 64 # 单个value大小限制
总结
这个知识点体现了:
- 知其然更要知其所以然:不只是会用命令,还要懂底层实现
- 性能优化源于细节:数据结构的选择直接影响内存、网络、CPU
- Redis 哲学:用合适的数据结构做合适的事