一份数据到底应该用什么数据类型存Redis 并不是只有简单的key-value。我们经常说的 Redis 五大基本数据类型包括String、Hash、List、Set、Sorted SetZSet。严格来说现代 Redis 支持的数据类型早已经不止这五种还有Stream、Bitmap、HyperLogLog、Geospatial 等不过这五种依然是最核心的一组。Redis 官方目前也把 String、Hash、List、Set、Sorted Set 等作为通用数据类型进行介绍。每种类型到底适合存什么为什么适合以及它们底层大概是怎么实现的。1. 先看五种类型最大的区别类型数据特点常见场景String一个 key 对应一个值缓存、计数器、TokenHash一个 key 对应多个 field-value对象、用户信息List有序、可重复队列、时间线Set无序、不可重复去重、标签、共同好友ZSet不重复并且按照 score 排序排行榜、优先级队列如果只想快速选择可以先记一个简单值 ↓ String 一个对象有很多字段 ↓ Hash 需要保存顺序可以重复 ↓ List 不允许重复不关心顺序 ↓ Set 不允许重复还需要排序 ↓ ZSet下面分别来看。2. String 是最基本的数据类型String是 Redis 最基础、也是最常用的数据类型。例如SET usernamezhangsanGET username结果zhangsan从逻辑上看就是username ↓ zhangsanRedis 官方把 String 定义为一个字节序列因此它不仅可以存普通字符串也可以存数字、序列化后的对象甚至二进制数据。所以不要把 Redis 的String理解成 Java 中单纯的String它更接近一段二进制安全的数据。3. String 最适合什么场景最常见的是缓存。例如key user:1001 value {id:1001,name:张三,age:20}可以SET user:1001{id:1001,name:张三,age:20}查询GET user:1001这也是最常见的缓存模式数据库中的对象 ↓ 序列化成 JSON ↓ Redis String例如 Java 项目中User Object ↓ Jackson ↓ JSON ↓ Redis4. String 还可以做计数器String虽然叫字符串但是 Redis 可以把其中的整数内容当作数字进行操作。例如SET article:100:view0INCR article:100:view INCR article:100:view INCR article:100:view结果3所以非常适合文章阅读量点赞数接口调用次数库存计数等场景。例如INCRBY article:100:view10就可以直接增加 10。Redis 的INCR、DECR等操作本身就是针对 String 类型提供的原子计数能力。5. String 底层一定就是字符串吗不是。Redis 对外暴露的是String但内部可能使用不同编码。例如当前 Redis 的 String 可能使用intembstrraw等编码。例如SET age20Redis 发现20实际上是一个整数就可能使用更加节省内存的int编码。而较短字符串和较长字符串也可能采用不同内部编码。可以通过OBJECT ENCODING age查看具体编码。所以需要区分Redis 数据类型是给用户看的逻辑类型encoding 是 Redis 内部实际采用的数据结构或编码方式。6. Hash 是什么假设我们要缓存一个用户id 1001 name 张三 age 20 email xxxqq.com如果使用 String可以直接存成 JSONSET user:1001{id:1001,name:张三,age:20}但是还有另一种方式HashHash 的结构可以理解成user:1001 ├── id → 1001 ├── name → 张三 ├── age → 20 └── email → xxxqq.com也就是Key ↓ field → value field → value field → valueRedis 官方将 Hash 描述为一组field-value对非常适合表示简单对象。7. Hash 怎么使用可以HSET user:1001 name张三age20emailxxxqq.com读取单个字段HGET user:1001 name结果张三读取整个对象HGETALL user:1001还可以单独修改HSET user:1001 age21这就是 Hash 一个非常明显的优势可以针对对象中的某一个字段进行修改而不需要把整个对象重新写一遍。8. String 存 JSON 和 Hash 存对象怎么选这是实际开发中经常遇到的问题。假设User id name age email avatar使用 Stringuser:1001 ↓ 一整个 JSON使用 Hashuser:1001 ↓ id → 1001 name → 张三 age → 20 email → xxx如果你的业务通常每次都把整个对象取出来。那么 String JSON 往往简单直接。例如GET user:1001 ↓ 反序列化 ↓ User如果经常只读取或修改对象中的某几个字段。那么 Hash 会更加自然。例如只修改年龄HSET user:1001 age21不需要GET 整个 JSON ↓ 反序列化 ↓ 修改 age ↓ 重新序列化 ↓ SET 整个 JSON所以没有绝对的Hash 一定比 String 好。应该根据访问模式选择。9. Hash 底层是什么Hash 内部也不是永远使用同一种数据结构。当前 Redis 中小型 Hash 可以使用更加节省内存的listpack当不再适合紧凑编码时会转换为普通的hashtable。Redis 7.0 以后小型 Hash 的紧凑表示主要使用listpack而不是早期版本常讲的ziplist。Hash 会根据数据规模和版本选择不同内部编码现代 Redis 中常见的是 listpack 和 hashtable。10. List 是什么List是有顺序、允许重复的字符串序列。例如A B C D插入RPUSH message A RPUSH message B RPUSH message C现在message A → B → C可以LRANGE message0-1得到整个 List。11. List 为什么适合做队列因为 List 支持LPUSHRPUSHLPOPRPOP也就是可以从两端插入和删除。例如普通队列左边进入 ↓ A B C D ↓ 右边出去可以使用LPUSH queue task1 LPUSH queue task2 RPOP queue也可以反过来。因此 List 很适合实现FIFO先进先出队列。同时也可以实现LIFO栈。12. List 还有阻塞操作Redis List 还提供BLPOPBRPOP等阻塞命令。假设消费者执行BRPOP task_queue0如果当前没有任务不需要不断 while 循环查询 Redis。客户端可以阻塞等待。当生产者LPUSH task_queue task1消费者就可以获取任务。所以 Redis List 可以实现一个非常简单的生产者 ↓ Redis List ↓ 消费者但是要注意List 能做简单队列不代表它就等于完整的消息队列。如果业务需要消费确认、Consumer Group、消息重放等能力通常应该考虑 RedisStream或专门的 MQ而不是只使用 List。13. List 底层就是链表吗从逻辑上你可以把 List 理解成链表结构。现代 Redis List 通常使用quicklist而 quicklist 的节点内部又可以使用listpack等紧凑结构。官方当前OBJECT ENCODING文档也明确指出老的linkedlist编码已经不再使用而 List 可以采用quicklist等编码。可以简单理解quicklist Node ↓ listpack ↕ Node ↓ listpack ↕ Node ↓ listpack这样既兼顾两端插入删除效率又能减少大量链表节点带来的内存开销。因此面试不要再机械背Redis List 底层就是双向链表。更准确List 的逻辑行为类似双端链表现代 Redis 内部主要使用 quicklist并结合 listpack 等紧凑结构。14. Set 是什么Set最大的特点就是无序 不重复。例如SADD user:1001:tagsjavaSADD user:1001:tags redis SADD user:1001:tags mysql再次SADD user:1001:tagsjava不会出现两个java所以Set java redis mysql天然具有去重能力。Redis 官方也把 Set 定义为无序、元素唯一的字符串集合。15. Set 最经典的场景去重假设统计某篇文章有哪些用户点赞可以SADD article:100:likes user1 SADD article:100:likes user2 SADD article:100:likes user3如果 user1 又点赞一次SADD article:100:likes user1Set 不会重复保存。然后SCARD article:100:likes就可以获得点赞用户数量。判断某人是否点赞SISMEMBER article:100:likes user116. Set 更强的是集合运算Set 支持交集并集差集例如用户 A 关注的人 1 2 3 4 用户 B 关注的人 3 4 5 6求共同关注SINTER user:A:follow user:B:follow得到3 4求并集SUNION user:A:follow user:B:follow求差集SDIFF user:A:follow user:B:follow所以共同好友共同关注兴趣标签用户去重都是 Set 很典型的使用场景。Redis 官方也把 membership、intersection、union、difference 作为 Set 的核心能力。17. Set 底层是什么Set 也会根据数据情况选择不同内部编码。例如当前 Redis 可以使用intsetlistpackhashtable等方式。如果是比较小、并且元素都是整数的 SetRedis 可以使用非常紧凑的intset。数据不再满足条件以后可以转换成其他编码。所以Redis 的逻辑数据类型和底层实现不是一一固定对应的。这也是学习 Redis 数据结构时非常重要的一点。18. ZSet 是什么ZSet全称Sorted Set也就是有序集合。它和 Set 一样member 不能重复。但是每一个 member 都会对应一个scoreRedis 根据 score 对元素进行排序。例如ZADD game:rank100zhangsan ZADD game:rank80lisi ZADD game:rank120wangwu可以理解成lisi → 80 zhangsan → 100 wangwu → 120Redis 会根据 score 自动维护顺序。19. ZSet 最经典的场景排行榜例如游戏积分排行榜。增加玩家积分ZINCRBY game:rank10zhangsan查看排名ZREVRANGE game:rank09WITHSCORES就可以获取分数最高的前 10 名。如果是ZRANGE game:rank09WITHSCORES则默认按照 score 从低到高。所以排行榜积分榜热度排名优先级队列都非常适合使用 ZSet。Redis 官方也直接把排行榜、按 score/rank 范围查询等作为 Sorted Set 的典型用途。20. ZSet 和 Set 到底差在哪里SetA B C只关心元素是否存在。ZSetA → 10 B → 50 C → 30除了元素不能重复以外还增加了score所以可以根据 score 自动排序简单记Set 去重ZSet 去重 排序21. ZSet 为什么查询排名这么快因为 Redis 并不是每次查询排行榜时临时把所有数据拿出来重新排序。ZSet 本身就是一个持续维护有序状态的数据结构。当前普通 ZSet 的内部实现使用同时包含skip list和hash table的结构因此既可以高效根据 member 查找 score又可以按照 score 进行范围和排名操作。可以简单理解Hash Table ↓ 快速根据 member 查找 Skip List ↓ 按照 score 保持有序所以 ZSet 才非常适合排行榜 ↓ Top N ↓ 按分数范围查找22. 什么是 Skip ListSkip List中文叫跳表。假设普通链表1 → 2 → 3 → 4 → 5 → 6 → 7 → 8如果想找8可能需要一个一个往后走。跳表会增加多层索引Level 2 1 --------→ 5 --------→ 8 Level 1 1 ----→ 3 ----→ 5 ----→ 7 → 8 Level 0 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8查找时可以高层快速跳跃 ↓ 逐渐下降 ↓ 定位目标所以平均情况下可以实现比较高效的查找插入删除以及范围遍历。这也是 ZSet 很适合排行榜的原因之一。23. ZSet 底层永远是跳表吗也不是。和 Hash、Set 一样Redis 会考虑内存占用。小型 ZSet 可以使用listpack当不再适合紧凑编码时再使用普通的skiplist编码。Redis 当前官方的OBJECT ENCODING文档也是这样区分的。所以不要背ZSet 底层就是跳表。更准确应该说ZSet 根据数据规模可能采用不同 encoding普通大型 ZSet 使用 skiplist 编码而该实现会结合跳表和哈希表。24. 五种类型应该怎么选现在把最重要的选择逻辑放在一起。只需要一个 value ↓ String 一个对象包含很多字段 并且经常单独操作字段 ↓ Hash 需要顺序 并且允许重复 ↓ List 不允许重复 不关心排序 ↓ Set 不允许重复 还需要按照某个分数排序 ↓ ZSet例如用户对象user:1001 name age email可以考虑Hash。文章详情完整 JSONarticle:100 ↓ JSON可以考虑String。任务列表task1 task2 task3可以考虑List。点赞用户user1 user2 user3可以考虑Set。排行榜user1 → 100 user2 → 200 user3 → 150可以考虑ZSet。25. String 和 Hash 最容易混如果每次基本都是整个对象一起读写使用String JSON往往比较简单。如果经常只修改其中某几个字段可以考虑Hash。例如用户积分name avatar score level如果经常只修改 scoreHash 可以直接HINCRBY user:1001 score10而 String JSON 可能需要GET 整体 ↓ 解析 JSON ↓ 修改 score ↓ 重新序列化 ↓ SET 整体因此数据类型的选择本质上取决于你的数据访问方式。26. List、Set、ZSet 最容易混可以从两个问题判断。第一个允许重复吗List允许。Set不允许。ZSetmember 不允许重复。第二个是否需要排序Set不维护业务排序。ZSet按照 score 排序。List按照元素插入形成的序列保存。所以聊天消息顺序 ↓ List / Stream 用户标签 ↓ Set 排行榜 ↓ ZSet这里尤其要注意如果你真正需要的是可靠消息系统不要因为 List 能先进先出就直接认为 List 是最佳消息队列方案。Redis 现在还有专门的Stream数据类型支持 append-only 事件流以及 Consumer Group 等能力。27. 为什么 Redis 要设计这么多数据类型假设 Redis 只有 String。理论上我们也可以把任何东西都序列化成 JSONList ↓ JSON 数组 Set ↓ JSON 数组 排行榜 ↓ JSON但问题是每修改一点东西可能就需要把整个 JSON 取出来重新处理。例如排行榜100 万用户如果全部存成 JSON然后每次查询 Top 10读取所有数据 ↓ 反序列化 ↓ 排序 ↓ 取前 10显然不合理。而 ZSetZREVRANGE rank09直接就能获取结果。所以 Redis 的核心思想并不是只提供一个特别快的 HashMap。而是直接提供针对常见业务场景设计的数据结构和原子操作。这也是 Redis 官方经常把自己称作data structure server/store的原因。28. Redis 类型和底层 encoding 不要混为一谈例如你执行TYPE key看到的可能是string hash list set zset这是逻辑数据类型。而OBJECT ENCODING key看到的可能是int embstr raw listpack hashtable quicklist intset skiplist这是底层 encoding。所以关系可以理解成Redis 逻辑类型 ↓ 根据当前数据特征 ↓ 选择合适的内部 encoding例如Hash ↓ 小数据 ↓ listpack Hash ↓ 数据变大 ↓ hashtable这种转换对用户通常是透明的。Redis 官方也明确说明当紧凑编码不再适用时对象可以自动转换到通用编码。