Redis数据类型全解析:从底层原理到生产实战

别再用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(需单独安装)

四、生产黄金法则

  1. 拒绝BigKey:String > 10KB,Hash/Set/ZSet元素 > 5000需拆分

  2. 控制复杂度 :慎用KEYS *HGETALLSMEMBERS等O(N)命令

  3. 设置过期时间:尤其是临时数据

  4. Pipeline批量操作:减少RTT

  5. 读写分离:从节点承担读压力

  6. 监控内存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数组,请把这篇文章甩给他。

相关推荐
草莓熊Lotso1 小时前
【Linux网络】深入理解传输层 UDP 协议:从底层原理到实战应用
linux·运维·服务器·c语言·网络·c++·udp
国科安芯1 小时前
商业航天级抗辐照全双工RS-485/RS-422收发器ASM491S2Y的技术特性与应用研究
运维·网络·单片机·嵌入式硬件·安全·架构·安全性测试
酣大智1 小时前
BGP选路原则--Med(6)
运维·网络·路由器·bgp
C137的本贾尼1 小时前
InnoDB 的物理世界:表空间、段、区与页
数据库
JdSnE27zv1 小时前
EF Code First学习笔记:数据库创建
数据库·笔记·学习
huluang1 小时前
《密评之殇》
运维·云计算
hweiyu001 小时前
Linux命令:blkid
linux·运维·服务器
我是一颗柠檬1 小时前
【Redis】Redis性能优化Day14(2026年)
数据库·redis·性能优化
j_xxx404_1 小时前
Linux线程池硬核解析:从固定线程池、单例线程池到线程安全、死锁与锁模型|附源码
linux·运维·服务器·c++·安全·ai