别再用String存所有东西了,你可能浪费了80%的内存
一、从一次生产事故说起
某天早高峰,用户反馈App首页加载极慢。排查发现,运营在后台配置了一组缓存数据,开发图方便直接用String存了个大JSON,value达到1.2MB。get/set操作在高并发下触发大量网络IO阻塞,Redis线程池被打满。
教训:不懂数据类型的适用场景,就是在给系统埋坑。
二、五大核心类型 + 三大新贵
1. String ------ 最熟悉的陌生人
底层编码:int、embstr、raw
-
int:8字节长整型,直接用long存储
-
embstr:<=44字节,连续内存分配,一次创建/释放
-
raw:>44字节,SDS(简单动态字符串)+ 独立内存
硬核细节:
c
// SDS结构(redis 3.2+)
struct sdshdr8 {
uint8_t len; // 已使用字节数
uint8_t alloc; // 分配总字节数
unsigned char flags; // 低3位表示类型
char buf[];
};
实战命令:
bash
# 计数器场景(限流、计数)
INCR user:123:pageview
INCRBY user:123:points 50
# 分布式锁(SET NX EX 原子操作)
SET lock:order:1001 "uuid" NX EX 10
# 批量操作减少RTT
MSET user:1:name "张三" user:1:age "25"
MGET user:1:name user:1:age
避坑指南:
-
禁止
KEYS *,用SCAN替代 -
大key(>10KB)考虑拆分或换Hash
-
大量过期key同时过期,添加随机偏移
2. Hash ------ 对象的天然容器
底层编码:ziplist(7.0+改listpack)→ hashtable
升级条件:
-
任一field长度 > hash-max-ziplist-value(默认64)
-
field数量 > hash-max-ziplist-entries(默认512)
内存对比 :存1000个用户字段,String需1000个key消耗约1MB,Hash仅需1个key + 1000个field消耗约0.5MB,内存省一半。
实战命令:
bash
# 用户信息存储
HSET user:1001 name "张三" age 25 city "北京"
HGETALL user:1001 # 注意:大对象慎用
HINCRBY user:1001 exp 10 # 原子增加经验值
# 获取部分字段
HMGET user:1001 name age
生产建议:
-
控制field数量在1000内,避免HGETALL阻塞
-
批量操作使用
HMSET/HMGET -
统计用
HLEN不要HKEYS
3. List ------ 轻量级消息队列
底层编码:quicklist(ziplist + 双向链表)
Redis 3.2开始采用quicklist,每个节点是一个ziplist,平衡了内存和性能。
核心命令:
bash
# 栈:LPUSH + LPOP
# 队列:LPUSH + RPOP
# 阻塞队列:LPUSH + BRPOP
# 消息队列示例
LPUSH msg_queue "{'user':1001,'action':'like'}"
RPOP msg_queue # 非阻塞
BRPOP msg_queue 5 # 阻塞5秒,适合消费者模式
使用场景:
-
最新消息列表(如朋友圈时间线)
-
生产者-消费者模式(注意空轮询)
-
简单日志收集
坑点:
-
LINDEX操作索引越大越慢(需遍历) -
LLEN频繁调用会影响性能 -
阻塞队列若消费者崩溃,消息可能丢失(可用Stream替代)
4. Set ------ 去重利器
底层编码:intset → hashtable
-
intset :所有元素是整数且数量小于
set-max-intset-entries(默认512),有序整数数组,二分查找 -
hashtable:条件不满足时升级,内存开销较大
强大功能:
bash
# 共同关注(社交场景)
SINTER user:1001:follows user:1002:follows
# 随机推荐(抽奖、随机展示)
SRANDMEMBER lottery 3 # 随机取3个不重复
SPOP lottery 1 # 随机弹出
# 标签系统
SADD article:1001:tags "Redis" "数据库" "缓存"
SISMEMBER article:1001:tags "Redis" # 判断是否有标签
性能数据:
-
添加/删除/查找:O(1)
-
并集/交集/差集:O(N)(N大时注意阻塞)
-
推荐使用
SUNIONSTORE将结果存到临时key
5. ZSet ------ 排行榜标配
底层编码:listpack → skiplist + hashtable
跳表(skiplist)实现有序存储,哈希表实现快速按值查找分数。
核心命令:
bash
# 游戏排行榜
ZADD leaderboard 100 "player:1001"
ZADD leaderboard 95 "player:1002"
ZINCRBY leaderboard 5 "player:1001" # 增加分数
# 取前三(WITHSCORES带分数)
ZREVRANGE leaderboard 0 2 WITHSCORES
# 取排名(0-based)
ZREVRANK leaderboard "player:1001"
# 区间操作
ZRANGEBYSCORE leaderboard 90 100 WITHSCORES # 取分数在90-100的玩家
典型场景:
-
延迟队列:用时间戳做score
-
滑动窗口限流:用户访问时间戳存入ZSet,统计区间内数量
内存优化:
-
默认ZSet内存较大,可改用
ZREMRANGEBYRANK定期清理 -
score尽量用整数,浮点数会额外开销
6. 新贵:Stream(5.0+)
专为消息队列设计,媲美Kafka的消费组功能。
bash
# 生产消息
XADD mystream * sensor-id 1234 temperature 19.8
# 消费组(类似Kafka)
XGROUP CREATE mystream mygroup 0 # 创建消费组
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream > # 读取新消息
场景:可靠消息队列、事件溯源、日志收集。
7. 新贵:Bitmaps(本质是String)
bash
# 日活统计(每个bit代表一个用户)
SETBIT login:20250101 1001 1 # 用户1001登录
BITCOUNT login:20250101 # 统计日活
# 连续签到7天
BITOP AND week_active login:0101 login:0102 ... login:0107
存储1亿用户在线状态仅需12.5MB。
8. 新贵:HyperLogLog(6.0+完善)
基数统计,误差0.81%,内存固定12KB。
bash
PFADD uv:20250101 "user:1001" "user:1002"
PFCOUNT uv:20250101 # 去重计数
PFMERGE uv:week uv:0101 uv:0102 ... # 合并周数据
场景:UV统计、搜索词独立计数。
三、选型决策树
text
需要计数? → String(INCR)
需要存储对象? → Hash
需要有序? → ZSet
需要去重? → Set
需要消息队列? → List(简单) / Stream(可靠)
需要统计UV? → HyperLogLog
需要布隆过滤? → RedisBloom(需单独安装)
四、生产黄金法则
-
拒绝BigKey:String > 10KB,Hash/Set/ZSet元素 > 5000需拆分
-
控制复杂度 :慎用
KEYS *、HGETALL、SMEMBERS等O(N)命令 -
设置过期时间:尤其是临时数据
-
Pipeline批量操作:减少RTT
-
读写分离:从节点承担读压力
-
监控内存 :
INFO memory+ RedisInsight可视化
五、性能压舱石数据
| 操作 | 复杂度 | 10w数据耗时 | 建议 |
|---|---|---|---|
| String GET/SET | O(1) | ~0.1ms | 放心用 |
| Hash HGETALL | O(N) | ~15ms | <100字段 |
| List LINDEX | O(N) | ~5ms | 慎用 |
| Set SADD | O(1) | ~0.1ms | 没问题 |
| ZSet ZRANGE | O(logN+M) | ~1ms | M<100 |
| Set SINTER | O(N) | ~20ms | 异步或用Store版本 |
六、总结
Redis不是简单的KV存储,每个数据类型都经过精妙设计。理解底层编码和适用边界,才能发挥最大价值。
下次再有人用String存JSON数组,请把这篇文章甩给他。