redis数据类型及使用场景
一、数据类型与场景总览
| 数据类型 | 底层结构 | 核心特性 | 典型场景 | 性能边界 |
|---|
| String | SDS / int / embstr | 二进制安全,最大 512MB | 缓存、计数、分布式锁、Session | O(1) |
| Hash | ziplist / hashtable | 字段级操作,节省内存 | 对象缓存、购物车、配置存储 | O(1) 字段访问 |
| List | quicklist | 双向链表,有序 | 消息队列、时间线、栈/队列 | O(1) 头尾,O(N) 中间 |
| Set | intset / hashtable | 唯一性,集合运算 | 标签、关注关系、抽奖、去重 | O(1) 增删查 |
| ZSet | skiplist + dict | 有序,按分数排名 | 排行榜、延迟队列、滑动窗口限流 | O(log N) |
| Bitmap | raw string | 位操作,极省空间 | 签到、在线状态、布隆过滤器 | O(1) 位操作 |
| HyperLogLog | sparse / dense | 基数估算,固定 12KB | UV 统计、海量去重计数 | O(1),误差 0.81% |
| Geo | sorted set | 经纬度存储,距离计算 | 附近的人、位置服务 | O(log N) 添加 |
| Stream | radix tree + listpack | 持久化消息队列,消费者组 | 事件溯源、消息队列、日志收集 | O(log N) 添加 |
| JSON | ReJSON 模块 | 路径查询,原子更新 | 文档存储、配置管理、嵌套对象 | 依赖路径深度 |
二、String:最基础也最万能
2.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:对象缓存(KV 缓存) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SET user:1001 '{"id":1001,"name":"Alice","level":5}' EX 3600 │ │ GET user:1001 │ │ │ │ 适用:用户信息、商品详情、配置项等读多写少的完整对象 │ │ 注意:Value 较大时(> 10KB),考虑 Hash 字段拆分或压缩 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:计数器(原子操作) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ INCR article:1001:views # 文章阅读量 +1 │ │ INCRBY user:1001:credits 10 # 用户积分 +10 │ │ DECR stock:sku:8888 # 库存扣减 │ │ │ │ 原子性保证:INCR/DECR 是单命令,无需事务 │ │ 注意:计数上限 2^63-1,溢出后报错 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:分布式锁(Redlock 算法) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SET lock:order:1001 my-thread-id NX EX 30 │ │ # NX = 仅不存在时才设置(互斥) │ │ # EX 30 = 30 秒过期,防止死锁 │ │ │ │ 释放锁: │ │ if redis.call("get", KEYS[1]) == ARGV[1] then │ │ return redis.call("del", KEYS[1]) │ │ else return 0 end │ │ │ │ 陷阱:时钟漂移、主从切换导致锁丢失(Redisson 已解决) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 4:Session 存储 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SET session:abc123 '{"userId":1001,"loginTime":...}' EX 1800 │ │ │ │ 替代 Tomcat 本地 Session,支持分布式部署 │ │ 配合 Spring Session 自动集成 │ │ │ └─────────────────────────────────────────────────────────────────┘
2.2 实战陷阱
| 陷阱 | 说明 | 解决 |
|---|
| 大 Key | 单个 String > 10MB | 拆分为 Hash 字段,或压缩(Snappy/LZ4) |
| 热 Key | 单个 Key 被百万 QPS 访问 | 本地缓存 + 随机后缀打散(如config:1~config:10) |
| 批量操作 | 循环 GET 1000 次 | 改用 MGET,或 Pipeline |
三、Hash:对象字段级操作
3.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:用户信息对象(替代 JSON String) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ String 方案(不好): │ │ SET user:1001 '{"name":"Alice","age":25,"city":"Beijing"}' │ │ 修改年龄:GET → 解析 JSON → 修改 → 序列化 → SET(全量覆盖) │ │ │ │ Hash 方案(推荐): │ │ HSET user:1001 name Alice age 25 city Beijing │ │ 修改年龄:HSET user:1001 age 26 # 仅修改字段,O(1) │ │ 获取城市:HGET user:1001 city │ │ │ │ 内存优势:ziplist 编码时,字段少且短比 String 省 50%+ 内存 │ │ 编码转换:字段数 > 512 或字段值 > 64B 时,ziplist → hashtable │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:购物车(字段 = SKU,值 = 数量) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ HSET cart:user:1001 sku:8888 2 │ │ HSET cart:user:1001 sku:9999 1 │ │ HGETALL cart:user:1001 # 获取整个购物车 │ │ HINCRBY cart:user:1001 sku:8888 1 # 加购 │ │ HDEL cart:user:1001 sku:8888 # 删除商品 │ │ │ │ 过期控制:Hash 不支持整体过期,需配合顶层 Key 或使用 String JSON │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:配置项分组存储 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ HSET config:db host localhost port 3306 user admin │ │ HSET config:cache ttl 3600 maxSize 10000 │ │ │ │ 优势:配置按组管理,批量读取(HGETALL)或单字段更新 │ │ │ └─────────────────────────────────────────────────────────────────┘
3.2 Hash vs String 选型决策
┌─────────────────────────────────────────────────────────────────┐ │ 决策树:对象缓存选 Hash 还是 String? │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 对象字段是否经常单独更新? │ │ │ │ │ YES → 用 Hash(HSET/HGET 字段级操作) │ │ │ │ │ NO → 对象是否很大(> 10KB)? │ │ │ │ │ YES → 用 String + 压缩(Snappy) │ │ │ │ │ NO → 是否需要整体过期? │ │ │ │ │ YES → String(SET EX 原子过期) │ │ │ │ │ NO → Hash(内存更省) │ │ │ └─────────────────────────────────────────────────────────────────┘
四、List:有序队列
4.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:消息队列(轻量级,非高可靠场景) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 生产者:LPUSH queue:email '{"to":"user@x.com","content":"..."}' │ │ 消费者:BRPOP queue:email 30 # 阻塞等待 30 秒 │ │ │ │ 特点: │ │ - 单消费者:LPUSH + BRPOP 简单可靠 │ │ - 多消费者:BRPOP 竞争消费,每条消息只被一个消费者处理 │ │ - 无 ACK 机制:消费者崩溃消息丢失(对比 Stream 的消费者组) │ │ │ │ 适用:日志收集、异步任务、通知推送(允许少量丢失) │ │ 不适用:金融交易、订单处理(需 Stream 或专业 MQ) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:时间线/Feed 流(最新 N 条) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ LPUSH timeline:user:1001 '{"id":123,"text":"hello"}' │ │ LTRIM timeline:user:1001 0 99 # 只保留最新 100 条 │ │ LRANGE timeline:user:1001 0 19 # 获取首页 20 条 │ │ │ │ 特点:O(1) 头尾操作,LTRIM 原子裁剪 │ │ 对比:ZSet 可实现按时间分数排序,但 List 更轻量 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:栈(Stack)和队列(Queue) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 栈(后进先出):LPUSH + LPOP │ │ 队列(先进先出):LPUSH + BRPOP │ │ 双端队列:LPUSH/RPUSH + LPOP/RPOP │ │ │ └─────────────────────────────────────────────────────────────────┘
4.2 List 的陷阱
| 陷阱 | 说明 | 解决 |
|---|
| LINDEX/LSET 中间操作 | O(N) 遍历,大 List 性能灾难 | 避免,或用 ZSet |
| 无 ACK 机制 | 消费者崩溃消息丢失 | 重要业务用 Stream |
| 阻塞连接占用 | BRPOP 长时间阻塞消耗连接池 | 控制超时,或使用 Stream |
五、Set:唯一集合
5.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:标签系统(Tag) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SADD article:1001:tags java redis distributed-system │ │ SADD article:1002:tags java spring microservice │ │ │ │ 查找共同标签:SINTER article:1001:tags article:1002:tags │ │ → {"java"} │ │ │ │ 查找文章 1001 的所有标签:SMEMBERS article:1001:tags │ │ │ │ 查找有 "redis" 标签的所有文章(反向索引): │ │ SADD tag:redis article:1001 article:1005 article:1010 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:关注关系(社交图谱) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ user:1001 关注了 user:1002, 1003, 1004 │ │ SADD following:user:1001 1002 1003 1004 │ │ │ │ user:1002 的粉丝:SADD followers:user:1002 1001 1005 │ │ │ │ 共同关注:SINTER following:user:1001 following:user:1002 │ │ 可能认识:SDIFF following:user:1002 following:user:1001 │ │ (user:1002 关注但 user:1001 没关注的) │ │ │ │ 注意:大数据量时(百万粉丝),Set 内存消耗大,考虑: │ │ - 分片:followers:user:1002:0, followers:user:1002:1... │ │ - 或改用 ZSet(带时间分数,支持范围查询) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:抽奖/随机选取(SRANDMEMBER/SPOP) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SADD lottery:pool user1 user2 user3 ... user10000 │ │ SRANDMEMBER lottery:pool 3 # 随机抽 3 个(不删除) │ │ SPOP lottery:pool 3 # 随机抽 3 个(从池子移除) │ │ │ │ 适用:抽奖、随机推荐、A/B 测试分组 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 4:UV 去重(配合 HyperLogLog 的精确版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ SADD uv:2024-01-01 user1 user2 user3 ... │ │ SCARD uv:2024-01-01 # 精确 UV 数 │ │ │ │ 内存问题:百万 UV = 百万个字符串,内存爆炸 │ │ 优化:用 HyperLogLog 估算(12KB 固定内存,误差 0.81%) │ │ │ └─────────────────────────────────────────────────────────────────┘
六、ZSet:有序集合(最强大类型)
6.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:排行榜(Leaderboard) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ZADD leaderboard 1500 "player:Alice" │ │ ZADD leaderboard 2300 "player:Bob" │ │ ZADD leaderboard 1800 "player:Carol" │ │ │ │ 获取前 10 名:ZREVRANGE leaderboard 0 9 WITHSCORES │ │ → Bob(2300), Carol(1800), Alice(1500)... │ │ │ │ 获取 Alice 的排名:ZREVRANK leaderboard "player:Alice" │ │ → 2(从 0 开始) │ │ │ │ 获取 Alice 的分数:ZSCORE leaderboard "player:Alice" │ │ → 1500 │ │ │ │ 更新分数:ZINCRBY leaderboard 100 "player:Alice" │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:延迟队列(Delayed Queue) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 分数 = 执行时间戳(毫秒) │ │ ZADD delay:queue 1705312800000 "task:sendEmail:1001" │ │ ZADD delay:queue 1705316400000 "task:refund:2002" │ │ │ │ 定时轮询(每秒): │ │ ZRANGEBYSCORE delay:queue 0 1705312805000 LIMIT 0 1 │ │ # 获取当前时间前到期的任务 │ │ │ │ 取出执行:ZPOPMIN delay:queue 1 # 原子弹出最小分数 │ │ │ │ 对比 List 的 BRPOP:ZSet 支持任意时间精度,List 只能 FIFO │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:滑动窗口限流(Sliding Window Rate Limit) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 记录用户请求时间戳: │ │ ZADD ratelimit:user:1001 1705312800000 "req:1" │ │ ZADD ratelimit:user:1001 1705312800100 "req:2" │ │ ZADD ratelimit:user:1001 1705312800200 "req:3" │ │ │ │ 清理过期窗口(60 秒前): │ │ ZREMRANGEBYSCORE ratelimit:user:1001 0 1705312740000 │ │ │ │ 检查窗口内请求数: │ │ ZCARD ratelimit:user:1001 # < 100 则允许,否则拒绝 │ │ │ │ 优势:精确滑动窗口,而非粗糙的固定窗口 │ │ 注意:高并发时 ZADD + ZREMRANGEBYSCORE + ZCARD 非原子,需 Lua 脚本 │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 4:地理位置(Geo,底层是 ZSet) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ GEOADD locations 116.40 39.90 "Beijing" │ │ GEOADD locations 121.47 31.23 "Shanghai" │ │ GEOADD locations 113.26 23.13 "Guangzhou" │ │ │ │ 获取北京坐标:GEOPOS locations "Beijing" │ │ │ │ 计算北京到上海距离:GEODIST locations "Beijing" "Shanghai" km │ │ │ │ 查找北京 500km 内的城市: │ │ GEORADIUS locations 116.40 39.90 500 km WITHDIST WITHCOORD │ │ → 北京(0km), 天津(110km)... │ │ │ │ 原理:GeoHash 编码为 52 位整数作为 ZSet 分数,范围查询即地理范围 │ │ │ └─────────────────────────────────────────────────────────────────┘
七、Bitmap:位图(极致空间效率)
7.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景 1:用户签到(365 天仅需 46 字节/用户) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 第 1 天签到:SETBIT sign:user:1001 0 1 │ │ 第 2 天签到:SETBIT sign:user:1001 1 1 │ │ 第 365 天签到:SETBIT sign:user:1001 364 1 │ │ │ │ 检查第 100 天是否签到:GETBIT sign:user:1001 99 │ │ 统计本月签到天数:BITCOUNT sign:user:1001 0 30 │ │ │ │ 内存:1 亿用户 × 365 天 / 8 = 4.56GB(对比 MySQL 存储省 100 倍) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 2:在线状态(亿级用户) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 用户 1001 上线:SETBIT online:20240101 1001 1 │ │ 用户 1001 下线:SETBIT online:20240101 1001 0 │ │ │ │ 统计今日在线人数:BITCOUNT online:20240101 │ │ 检查用户是否在线:GETBIT online:20240101 1001 │ │ │ │ 注意:用户 ID 必须连续或映射为连续整数,否则稀疏 Bitmap 浪费空间 │ │ 稀疏优化:Redis 4.0+ 引入 BITFIELD 或改用 Roaring Bitmap(外部) │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ 场景 3:布隆过滤器(Bloom Filter) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 原理:多个 Hash 函数映射到 Bitmap 位,判断"可能存在"或"肯定不存在" │ │ │ │ 添加元素: │ │ for hash in hashFunctions(element): │ │ SETBIT bloom:filter hash 1 │ │ │ │ 检查存在: │ │ for hash in hashFunctions(element): │ │ if GETBIT bloom:filter hash == 0: return False │ │ return True # 可能存在(有误判),或肯定存在 │ │ │ │ 误判率:位数组越大、Hash 函数越多,误判率越低 │ │ 适用:缓存穿透防护、URL 去重、垃圾邮件过滤 │ │ Redis 4.0+ 原生模块:RedisBloom │ │ │ └─────────────────────────────────────────────────────────────────┘
八、HyperLogLog:基数估算
8.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景:海量 UV 统计(误差可接受) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 问题:1 亿独立访客,用 Set 存储 → 内存 > 1GB │ │ │ │ HyperLogLog 方案: │ │ PFADD uv:2024-01-01 user1 user2 user3 ... user100000000 │ │ PFCOUNT uv:2024-01-01 │ │ → 100023456(真实值 100000000,误差 0.023%) │ │ │ │ 内存:固定 12KB,与数据量无关! │ │ │ │ 合并多天的 UV(去重总 UV): │ │ PFMERGE uv:total uv:2024-01-01 uv:2024-01-02 uv:2024-01-03 │ │ PFCOUNT uv:total │ │ │ │ 适用:网站 UV、广告曝光去重、搜索关键词去重 │ │ 不适用:精确计数(如订单金额统计) │ │ │ └─────────────────────────────────────────────────────────────────┘
九、Stream:持久化消息队列
9.1 典型场景
┌─────────────────────────────────────────────────────────────────┐ │ 场景:事件溯源 + 消费者组(替代 List 做可靠队列) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 生产者: │ │ XADD orders * userId 1001 amount 199.99 status created │ │ → 返回消息 ID:1705312800000-0 │ │ │ │ 消费者组(多个消费者并行处理,不重复消费): │ │ XGROUP CREATE orders payment-group $ MKSTREAM │ │ │ │ 消费者 1: │ │ XREADGROUP GROUP payment-group consumer-1 COUNT 1 STREAMS orders >│ │ → 读取新消息,处理支付 │ │ XACK orders payment-group 1705312800000-0 # 确认已处理 │ │ │ │ 消费者 2: │ │ XREADGROUP GROUP payment-group consumer-2 COUNT 1 STREAMS orders >│ │ → 读取下一条新消息 │ │ │ │ 未确认消息检查(处理超时重试): │ │ XPENDING orders payment-group │ │ → 查看哪些消息已分配但未 ACK │ │ │ │ 对比 Kafka: │ │ - Stream:轻量,无需外部依赖,适合中小规模 │ │ - Kafka:高吞吐、持久化、分区副本,适合大规模 │ │ │ └─────────────────────────────────────────────────────────────────┘
十、选型决策树
┌─────────────────────────────────────────────────────────────────┐ │ Redis 数据类型选型决策树 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 需要消息队列 + 消费者组 + ACK? │ │ │ │ │ YES → Stream │ │ │ │ │ NO → 需要按分数排序/范围查询? │ │ │ │ │ YES → ZSet(排行榜、延迟队列、滑动窗口) │ │ │ │ │ NO → 需要地理位置查询? │ │ │ │ │ YES → Geo(底层 ZSet) │ │ │ │ │ NO → 需要集合运算(交并差)? │ │ │ │ │ YES → Set(标签、关系、抽奖) │ │ │ │ │ NO → 需要字段级更新? │ │ │ │ │ YES → Hash(对象缓存、购物车) │ │ │ │ │ NO → 需要精确去重计数? │ │ │ │ │ YES → Set(小数据) │ │ │ HyperLogLog(大数据)│ │ │ │ │ NO → 需要位操作? │ │ │ │ │ YES → Bitmap │ │ │ │ │ NO → 需要队列语义?│ │ │ │ │ YES → List│ │ │ │ │ NO → String│ │ │ └─────────────────────────────────────────────────────────────────┘
十一、一句话总结
String是万能缓存,Hash做对象字段级操作,List做轻量队列,Set做关系与去重,ZSet是时间/分数驱动的核心引擎,Bitmap/HyperLogLog用极致空间换统计能力,Stream是原生持久化消息队列。架构师选型时,先问"是否需要排序",再问"是否需要精确",最后才考虑"是否足够简单"。