【大白话说Java面试题 第166题】【07_Redis篇】第2题:说说 Redis 的常见数据类型及其应用场景? 第2题说说 Redis 的常见数据类型及其应用场景回答核心考点 Redis 的数据类型不是简单的5 种基础类型而是“5 种基础类型 4 种高级类型 多种底层编码”的完整体系。大厂面试中面试官不会满足于背诵类型名称和应用场景而是期望你深入理解每种数据类型的底层编码实现String 的 SDS/int/embstr/raw、Hash 的 listpack/dict、List 的 quicklist、Set 的 intset/dict、ZSet 的 listpack/skiplistdict、编码切换的触发条件元素数量/大小阈值、不同编码的时间/空间复杂度差异以及高级类型Bitmap、HyperLogLog、Geo、Stream的适用边界。面试官真正想判断的是你是否能从源码级理解 Redis 如何通过多种底层编码动态适配实现性能与内存的平衡以及能否在生产环境中根据数据特征选择最优的数据类型和编码。1. Redis 数据类型全景图Redis 的数据类型分为基础数据类型和高级数据类型两大类类别数据类型核心特性典型场景基础类型String二进制安全最大 512MB缓存、计数器、分布式锁、SessionHashfield-value 映射O(1) 读写对象缓存、购物车、配置存储List双向链表头尾操作 O(1)消息队列、时间线、最新消息Set无序唯一集合交集/并集/差集标签、共同关注、抽奖、去重ZSet按 score 排序member 唯一排行榜、延时队列、权重排序高级类型Bitmap位操作极省内存签到统计、用户在线状态、布隆过滤器HyperLogLog基数统计标准误差 0.81%UV 统计、去重计数允许少量误差Geo地理位置支持半径搜索附近的人、商家、车辆定位Stream日志型消息队列消费者组事件溯源、消息队列Redis 5.0[citation:0]2. String 类型最基础也最精妙2.1 应用场景扩展场景命令原理注意事项缓存SET key value EX 3600带过期时间的 KV 存储大 Value 会阻塞建议 10KB计数器INCR article:read:1001原子递增线程安全注意溢出64 位有符号整数分布式锁SET lock:order:1001 myId NX EX 30NX不存在才设置 EX过期必须设置过期时间防死锁限流INCR rate:ip:192.168.1.1EXPIRE固定窗口计数边界突刺问题改用滑动窗口Session 共享SET session:abc123 {userId:1} EX 1800多实例共享 Session配合 Spring Session 使用全局 IDINCR global:order:id原子递增生成唯一 ID非连续但全局唯一2.2 底层编码三种编码动态切换Redis String 的底层不是简单的 C 字符串而是SDSSimple Dynamic String并根据数据特征选择三种编码编码触发条件内存结构特点int值是 64 位有符号整数直接存储整数最省内存无需额外分配embstr字符串长度 ≤ 44 字节Redis 3.2SDS RedisObject 连续内存只需 1 次内存分配缓存友好raw字符串长度 44 字节SDS RedisObject 分离内存2 次内存分配可存储任意大数据// embstr 编码结构连续内存 ---------------------------------------------------------------- | type | encoding | lru | refcount | ptr | len | alloc | buf[] | ---------------------------------------------------------------- | RedisObject (16B) | SDS header (3B) | 字符串内容 | -------------------------------------------------------------------- // raw 编码结构分离内存 RedisObject ──→ 指针 ──→ SDS独立内存块embstr 的只读特性embstr 编码的字符串是只读的执行APPEND等修改命令时会先转换为 raw 编码再执行修改。[citation:1]3. Hash 类型对象缓存的首选3.1 应用场景扩展场景命令示例优势用户对象缓存HSET user:1001 name Alice age 25 city Beijing比 String JSON 更省内存可单独修改字段购物车HSET cart:user:1001 sku:1001 2以用户为 Key商品为 field数量为 value配置存储HSET config:app maxConnections 1000分组管理配置项文章统计HINCRBY article:1001 views 1原子递增单个字段Hash vs String 存储对象对比// ❌ String 方式每次修改需全量序列化/反序列化redisTemplate.opsForValue().set(user:1001,{name:Alice,age:25,city:Beijing});// ✅ Hash 方式可单独修改某个字段更省内存redisTemplate.opsForHash().putAll(user:1001,Map.of(name,Alice,age,25,city,Beijing));redisTemplate.opsForHash().put(user:1001,age,26);// 只改 age3.2 底层编码listpack vs dictRedis 7.0 前使用 ziplist7.0 后统一使用listpack压缩列表的改进版解决连锁更新问题编码触发条件时间复杂度空间特点listpack元素数 512 且每个 field/value 64 字节O(N) 查找但 N 很小连续内存极省空间dicthashtable不满足 listpack 条件O(1) 读写指针开销大但操作快渐进式 rehashHash 从 listpack 切换为 dict 时采用渐进式 rehash------不是一次性迁移所有数据而是每次增删查时迁移少量桶避免阻塞主线程。[citation:2]4. List 类型从 linkedlist 到 quicklist 的演进4.1 应用场景扩展场景命令模式注意事项最新消息列表LPUSH timeline:user:1001 msg1LRANGE 0 9头插 范围查询只保留最近 N 条定期修剪简单消息队列LPUSH queue:task {job}BRPOP queue:task 30生产者/消费者无 ACK消费者宕机消息丢失栈LPUSHLPOP同端进出后进先出队列LPUSHRPOP/RPUSHLPOP异端进出先进先出4.2 底层编码quicklist3.2Redis 3.2 前使用ziplist或linkedlist3.2 后统一使用quicklist------双向链表 listpack 的混合结构quicklist ├── node1 ──→ listpack [item1, item2, item3] ← 每个节点是一个 listpack ├── node2 ──→ listpack [item4, item5, item6] └── node3 ──→ listpack [item7, item8]设计优势双向链表保证头尾插入删除 O(1)每个节点内的 listpack 连续存储节省指针开销缓存友好通过list-max-ziplist-size控制每个 listpack 的节点数平衡内存与性能。[citation:3]5. Set 类型集合运算的利器5.1 应用场景扩展场景命令原理性能共同关注SINTER user:1001:follows user:1002:follows交集运算O(N*M)N 和 M 是小集合时很快可能认识的人SDIFF user:1001:follows user:1002:follows差集运算找出 A 关注但 B 没关注的人抽奖去重SPOP activity:1001:participants 3随机弹出 3 个不重复元素原子操作防止重复中奖标签系统SADD tag:java article:1001文章打标签通过SINTER tag:java tag:spring找交集文章UV 统计小规模SADD uv:2024-01-01 user:1001SCARD去重后计数数据量大时用 HyperLogLog5.2 底层编码intset vs dict| 编码 | 触发条件 | 特点 | 切换代价 || ---- | -------- | ---- | -------- ||intset| 所有元素都是整数且元素数 512 | 有序数组二分查找 O(logN)极省内存 | 插入非整数或超阈值时全量转换为 dict ||dict| 不满足 intset 条件 | 哈希表O(1) 操作 | 内存开销大但操作快 |intset 的升级机制当插入一个比当前编码大的整数时如从 16 位升级到 32 位intset 会整体升级------所有元素重新编码这是 O(N) 操作但通常 N 很小。[citation:4]6. ZSet 类型最复杂也最强大6.1 应用场景扩展场景命令核心特性实时排行榜ZADD leaderboard 100 AliceZREVRANGE leaderboard 0 9 WITHSCORES按分数倒序支持范围查询延时队列ZADD delay_queue 1704067200 task:1001ZRANGEBYSCORE delay_queue 0 1704067200score 为执行时间戳到期取出权重排序ZADD search:java 0.8 article:1001按相关性分数排序搜索结果滑动窗口限流ZADD rate:ip:192.168.1.1 1704067200000 1ZREMRANGEBYSCORE ... 0 (now-window)时间戳为 score清理过期请求6.2 底层编码listpack vs skiplist dictZSet 是 Redis 最复杂的数据类型采用双结构组合实现编码触发条件结构时间复杂度listpack元素数 128 且每个元素 64 字节连续内存按 score 排序O(N) 查找N 很小skiplist dict不满足 listpack 条件跳表按 score 排序 哈希表按 member 查找范围查询 O(logN)单查 O(1)为什么用跳表而不用红黑树/AVL 树对比维度跳表SkipList红黑树/AVL 树范围查询天然支持按层级遍历需中序遍历实现复杂实现复杂度简单代码量少复杂旋转平衡逻辑繁琐并发性修改时影响局部锁粒度小旋转可能涉及多个节点锁粒度大内存开销额外指针平均 1.33 个/节点颜色标记 父指针开销相近Redis 选择✅ 采用❌ 不采用跳表 dict 的双结构设计跳表zskiplist按 score 排序支持ZRANGE、ZRANGEBYSCORE等范围查询O(logN)哈希表dictmember → score 的映射支持ZSCORE等单点查询O(1)内存共享跳表节点和哈希表节点共享同一个 member 和 score 对象不重复存储。// Redis 源码中的 zset 结构typedefstructzset{zskiplist*zsl;// 跳跃表按 score 排序dict*dict;// 哈希表member → score 映射}zset;[citation:5]7. 高级数据类型4 种扩展能力7.1 Bitmap位图用 String 的位操作实现每个 bit 代表一个状态极省内存场景命令内存计算示例用户签到SETBIT sign:2024:01 0 11 亿用户 1 年 ≈ 12MBBITCOUNT sign:2024:01统计月签到天数在线状态SETBIT online 1001 11 亿用户 ≈ 12MBBITPOS online 1找第一个在线用户布隆过滤器SETBIT bloom:filter hash1 1误判率可控配合多个 hash 函数使用1 亿用户的在线状态用 Bitmap 只需 12MB用 Set 可能需要 100MB。7.2 HyperLogLog基数统计用于大规模去重计数标准误差 0.81%固定内存 12KB场景命令精度内存网站 UVPFADD uv:2024-01-01 user:10010.81%12KB无论多少元素搜索去重PFCOUNT uv:2024-01-01近似值固定 12KB原理基于概率算法用 12KB 的寄存器数组统计哈希值的最高位连续 0 的个数估算不重复元素数量。不存储具体元素只存储统计特征。7.3 Geo地理位置基于 ZSet 实现将经纬度编码为 geohash 作为 score场景命令精度限制附近的人GEOADD users 116.397 39.916 user:1001约 0.5% 误差两极附近误差大附近商家GEORADIUS shops 116.4 39.9 5 km WITHDIST球面距离计算不支持多边形范围7.4 Stream流Redis 5.0 引入的日志型消息队列支持消费者组特性命令对比 List/ PubSub持久化XADD stream:orders * orderId 1001List 和 PubSub 都不持久化消费者组XGROUP CREATE stream:orders group1 $支持多个消费者组独立消费ACK 机制XACK stream:orders group1 1704067200000-0确认消费支持重试消息 ID时间戳-序列号如1704067200000-0天然有序支持范围查询Stream vs KafkaStream 适合轻量级消息队列 10万 TPSKafka 适合大规模日志采集百万 TPS。[citation:6]8. 数据类型选型决策表业务场景推荐类型核心命令避坑指南简单 KV 缓存StringSET/GET/EXPIRE大 Value 阻塞建议 10KB对象字段频繁修改HashHSET/HGET/HINCRBY比 String JSON 更省内存最新消息/时间线ListLPUSH/LRANGE/LTRIM定期LTRIM防止无限增长消息队列简单ListLPUSH/BRPOP无 ACK消费者宕机丢失消息消息队列可靠StreamXADD/XREADGROUP/XACKRedis 5.0支持消费者组标签/共同关注SetSADD/SINTER/SUNION大集合交集慢考虑分片排行榜/TopNZSetZADD/ZREVRANGEscore 为 double精度有限延时队列ZSetZADD/ZRANGEBYSCORE/ZREMscore 为时间戳需轮询用户签到/在线状态BitmapSETBIT/BITCOUNT/BITOP1 亿用户仅需 12MBUV 统计允许误差HyperLogLogPFADD/PFCOUNT固定 12KB误差 0.81%附近搜索GeoGEOADD/GEORADIUS不支持多边形两极误差大实时广播Pub/SubPUBLISH/SUBSCRIBE消息不持久化离线丢失9. 面试官追问与高分回答模板追问 1“Redis 有哪些数据类型分别说说应用场景”低分回答“String 做缓存Hash 存对象List 做队列Set 去重ZSet 排行榜。”太浅没有触及底层编码和高级类型高分回答 “Redis 的数据类型分为基础类型和高级类型两大类 基础类型5 种 -String不只是字符串支持整数编码int、短字符串连续内存embstr、长字符串raw。场景缓存、计数器INCR、分布式锁SET NX EX、全局 ID 生成。 -Hashfield-value 结构底层小数据用 listpack连续内存、大数据用 dict哈希表。场景对象缓存比 String JSON 更省内存且可单独修改字段、购物车、配置存储。 -List底层是 quicklist双向链表 listpack头尾操作 O(1)。场景最新消息列表、简单消息队列LPUSHBRPOP、栈/队列。 -Set底层 intset整数有序数组或 dict。场景标签系统、共同关注SINTER、抽奖去重SPOP。 -ZSet最复杂底层 listpack 或跳表 哈希表双结构。场景排行榜、延时队列score 为时间戳、权重排序。 高级类型4 种 -Bitmap位操作1 亿用户签到仅需 12MB -HyperLogLog基数统计固定 12KB误差 0.81%适合 UV 统计 -Geo基于 ZSet 的地理位置支持半径搜索 -Stream日志型消息队列支持消费者组和 ACKRedis 5.0。”追问 2“ZSet 的底层实现是什么为什么用跳表而不是红黑树”高分回答 “ZSet 是 Redis 最复杂的数据类型采用双结构组合实现 -跳表zskiplist按 score 排序支持ZRANGE、ZRANGEBYSCORE等范围查询平均时间复杂度 O(logN) -哈希表dict存储 member → score 的映射支持ZSCORE等单点查询O(1) -内存共享跳表节点和哈希表节点共享同一个 member 和 score 对象不重复存储。 选择跳表而非红黑树的原因 1.范围查询更简单跳表按层级遍历即可实现范围查询红黑树需要复杂的中序遍历 2.实现简单跳表代码量远小于红黑树维护和调试成本低 3.并发友好跳表修改只影响局部节点锁粒度小红黑树的旋转操作可能涉及多个节点锁粒度大 4.性能相当跳表平均查找复杂度 O(logN)与红黑树同级且常数因子更小。 小数据时元素 128 且每个元素 64 字节ZSet 使用 listpack 编码更省内存。”追问 3“Hash 和 String 存储对象有什么区别什么时候用 Hash”高分回答 “Hash 和 String 存储对象的核心差异在内存占用和操作粒度 1.内存占用String 存 JSON 需要重复存储 key如user:1001在每个字段都出现Hash 的 key 只存一次field 和 value 紧凑存储。对于小对象field 少且 value 短Hash 的 listpack 编码比 String 的 raw 编码更省内存。 2.操作粒度String 修改一个字段需要全量读取 → 反序列化 → 修改 → 序列化 → 写回O(对象大小)Hash 可以直接HSET修改单个 fieldO(1)。 3.TTL 控制String 可以给整个对象设过期时间Hash 只能给整个 Key 设过期不能单独给 field 设 TTLRedis 原生不支持需借助其他方案。 选型建议 - 对象字段频繁修改 →Hash - 需要给单个字段设 TTL →String每个字段独立 Key或Hash 定时任务清理 - 对象很大 1MB→StringHash 的大 Value 会触发 dict 编码内存开销大。”追问 4“List 做消息队列有什么缺陷和 Stream 有什么区别”高分回答 “List 做消息队列有三个核心缺陷 1.无 ACK 机制消费者通过BRPOP取出消息后如果处理失败或宕机消息永久丢失无法重试 2.无消费者组多个消费者竞争消费时无法做负载均衡和故障转移 3.消息无 IDList 的元素没有唯一标识无法做幂等消费和断点续传。 Stream 的改进 1.持久化消息默认持久化支持XDEL删除或XTRIM裁剪 2.消费者组XGROUP CREATE创建消费者组组内多个消费者负载均衡支持XACK确认和XPENDING查看未确认消息 3.消息 ID时间戳-序列号格式天然有序支持范围查询和断点续传 4.ACK 重试未确认的消息保留在 Pending 列表可重新分配给其他消费者。 但 Stream 仍有局限单节点吞吐量有限 10万 TPS不支持消息分区Partition大规模场景仍需 Kafka/RabbitMQ。”追问 5“Redis 的 String 底层有三种编码什么情况下会切换切换的代价是什么”高分回答 “Redis String 的三种编码及切换条件 -int值是 64 位有符号整数时自动使用直接存储在 RedisObject 的 ptr 字段中无需额外内存分配最省内存 -embstr字符串长度 ≤ 44 字节Redis 3.2时使用RedisObject 和 SDS 连续存储只需 1 次内存分配CPU 缓存友好 -raw字符串长度 44 字节时使用RedisObject 和 SDS 分离存储需 2 次内存分配可存储任意大数据。 切换代价 1.int → embstr/raw对整数执行APPEND操作如APPEND key abc会转换为 embstr 或 raw 2.embstr → rawembstr 是只读的任何修改操作APPEND、SETRANGE都会先转换为 raw再执行修改。转换涉及重新分配内存和拷贝数据O(N) 时间复杂度 3.raw 不会回退一旦转换为 raw即使后续缩短到 44 字节以内也不会回退为 embstrRedis 不做反向优化避免频繁切换。 生产环境建议避免对 embstr 编码的 Key 频繁执行APPEND会导致反复内存重分配。”追问 6“如果有一个 1 亿用户的签到系统怎么设计”高分回答 “1 亿用户签到系统的核心设计是Bitmap 分片 1.数据结构用 Bitmap每个 bit 代表一个用户一天的签到状态。1 亿用户 1 亿 bit ≈12MB/天一年约 4.3GB。如果用 Set 存储1 亿个元素至少需要 100MB。 2.Key 设计sign:2024:01:01表示 2024 年 1 月 1 日的签到 Bitmap用户 ID 作为 bit 偏移量。 3.签到操作SETBIT sign:2024:01:01 userId 1O(1)。 4.统计查询 - 月签到天数BITCOUNT sign:2024:01需将 30 天的 Bitmap 做 OR 合并或每天独立统计后求和 - 连续签到BITOP AND合并最近 N 天的 Bitmap再BITCOUNT。 5.分片策略如果用户 ID 不是连续整数如 UUID需要建立ID → 连续偏移的映射表如 Hash 表或按用户 ID 范围分片每片 1000 万用户10 个 Bitmap。 6.持久化Bitmap 是 String 类型自动支持 RDB/AOF 持久化。 7.缓存策略热数据最近 7 天保留在 Redis冷数据更早归档到 HBase/ClickHouse。 如果还需要统计月活跃用户数去重可用 HyperLogLogPFADD mau:2024-01 user:1001固定 12KB误差 0.81%。”10. 方案选型速查表数据特征推荐类型编码优化内存估算小对象字段 10 个Hash保持 listpack 编码field/value 64B比 String JSON 省 30-50%大对象字段 100 个StringJSON或 分 Hash避免 Hash 触发 dict 编码大 Hash 的 dict 指针开销大整数集合元素 512Set保持 intset 编码比 dict 省 80% 内存排行榜元素 1000ZSet触发 skiplist dict无法避免跳表指针开销约 1.33 倍时间序列数据Stream控制 stream 长度XTRIM消息保留策略防止无限增长二进制状态签到/在线Bitmap用户 ID 连续最佳1 亿用户 12MB大规模去重计数HyperLogLog固定 12KB无需优化误差 0.81% 可接受地理位置搜索Geo基于 ZSet自动优化约 2 倍 ZSet 开销面试官想要的满分总结Redis 的数据类型设计体现了“面向场景优化”的工程哲学------同一种逻辑类型如 String根据数据特征选择不同编码int/embstr/raw在内存占用和操作性能之间动态平衡。理解 Redis 数据类型必须抓住三个关键点底层编码动态切换Hash 的 listpack → dict、ZSet 的 listpack → skiplistdict、Set 的 intset → dict切换触发条件元素数量/大小是面试高频考点双结构设计的精妙ZSet 的跳表 哈希表同时支持范围查询O(logN)和单点查找O(1)且共享内存不重复存储这是 Redis 源码中最精妙的设计之一高级类型的适用边界Bitmap 适合二进制状态极省内存、HyperLogLog 适合大规模去重固定 12KB、Stream 适合轻量级可靠消息队列支持 ACK 和消费者组、Geo 适合附近搜索基于 ZSet。选型原则小数据优先用紧凑编码listpack、intset、embstr大数据接受指针开销换性能dict、skiplist、raw需要范围查询 → ZSet跳表需要集合运算 → Setintset/dict需要消息队列 → Stream优先或 List简单场景1 亿级别的二进制状态 → Bitmap1 亿级别的去重计数 → HyperLogLog这是 Redis 高级类型的杀手级应用。觉得对您有帮助麻烦点点关注啦您的关注是我创作的最大动力~