Skip to content

2. 数据结构:数据类型与使用场景 ​


❓ 面试官:Redis 常用的数据类型有哪些?你们在业务中是怎么使用的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): Redis 有 5 种基础数据类型(String、List、Hash、Set、ZSet)和 3 种特殊数据类型(Bitmap、HyperLogLog、Geospatial)。我会根据具体业务场景的特征(比如要不要去重、要不要排序、是不是存对象)来选择合适的数据结构。

📝 详细使用场景解析:

  1. String(字符串):

    • 基础缓存:缓存 JSON 格式的用户信息、商品详情等。
    • 计数器:利用 INCR 命令实现高并发的点赞数、阅读量、库存扣减(原子操作,不会超卖)。
    • 分布式锁:利用 SETNX(SET if Not eXists)实现。
  2. Hash(哈希/字典):

    • 对象存储:存用户对象(key=user:1, field=name, value=张三)。如果用 String 存 JSON,修改某个字段得全量取出来解析再塞回去;用 Hash 可以直接 HSET user:1 name 李四,精准修改,节省网络带宽。
    • 购物车:key=购物车ID(用户ID),field=商品ID,value=购买数量。
  3. List(列表):

    • 消息队列:利用 LPUSH 和 RPOP(或阻塞版的 BRPOP)实现简单的异步消息队列。
    • 时间线/最新列表:比如用户的历史浏览记录、朋友圈最新动态列表。因为 List 保证插入顺序,且支持分页读取(LRANGE)。
  4. Set(集合):

    • 去重场景:网站的独立 IP 访问量(UV)统计、每天签到打卡的用户。
    • 交并差集(标签推荐):比如求两个人的共同好友(交集 SINTER)、"可能认识的人"(差集 SDIFF)、商品标签筛选。
  5. ZSet(有序集合):

    • 排行榜:微博热搜榜、游戏积分排名。每个元素带一个分数值(Score),可以按分数自动排序。通过 ZREVRANGE 轻松取前 10 名。
    • 延时队列:将任务的执行时间戳作为 Score 存入 ZSet,后台线程轮询拿 Score 小于当前时间的任务去执行。

❓ 面试官:你刚刚提到了高级数据类型,能说说 Bitmap 和 HyperLogLog 吗?【中级】 ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 这两个都是为了在海量数据下极大地节省内存而生的。Bitmap 用于处理只有 0 和 1 两种状态的布尔值场景(如签到);HyperLogLog 用于海量数据的基数统计(去重估算,如亿级 UV),内存占用极小。

📝 详细原理解析:

  1. Bitmap(位图):

    • 原理:它其实不是一种新的数据结构,底层就是 String。它通过按 bit(位)来存数据,每一位只能是 0 或 1。
    • 优势:存 1 亿个用户的活跃状态(是/否),如果用普通数字存,可能需要几百 MB;用 Bitmap 存,1 亿个 bit 只需要约 12 MB!
    • 实战场景:用户全年连续签到打卡记录(key=用户ID:年份,offset=天数,0代表未签,1代表已签到)、统计某天活跃用户总数(BITCOUNT 命令)。
  2. HyperLogLog:

    • 原理:一种概率算法。专门用来做基数统计(即集合中不重复元素的个数)。
    • 优势:不管你塞入几百万还是几亿个不同元素,它最多只占用 12 KB 内存!
    • 缺点:有约 0.81% 的标准误差;且只能算出总量(PFCOUNT),不能取出具体塞入了哪些元素。
    • 实战场景:统计大促期间淘宝首页的独立访客数(UV)。这种场景下,老板只关心大概是 1000 万还是 1010 万,不需要 100% 绝对精确的个位数,用 HLL 完美解决内存爆炸问题。
  3. Stream(流):

    • 定位:Redis 5+ 的消息流结构,支持消费组、持久化、ACK,比 Pub/Sub 更像轻量队列。
    • 场景:服务内异步解耦、简单事件流;海量交易仍优先专业 MQ。
    • 👉 对比 Pub/Sub 见:消息与事务

❓ 面试官:能说说 Redis 里面针对数据类型的底层优化吗?(如跳表、压缩列表)【中高级】 ​

频率:🔥🔥🔥

💡 一句话总结(先抛结论): Redis 速度快不仅是因为内存,还因为它的数据结构会根据数据量的大小自动转换底层实现。最经典的包括:数据量小的时候用压缩列表(Ziplist)省内存,数据量大了换成跳表(SkipList)或哈希表提速度。

📝 详细原理解析:

  1. ZSet 的底层结构:

    • 当元素数量较少且值较短时,底层使用 Ziplist(压缩列表)。它是一块连续的内存空间,极端节省内存,但查找慢(O(N))。
    • 当数据量变大时,底层会自动转为 SkipList(跳表) + Dict(字典)。
    • 什么是跳表?:在普通单向链表的基础上,增加了多级索引层。查找时像跳格子一样从上层往下层找。它的时间复杂度是 O(log N),和平衡二叉树一样快,但实现比红黑树简单得多,且在高并发插入时只需要修改局部指针,不需要像红黑树那样做全局的旋转变色操作。
  2. Hash 的底层结构:

    • 早期版本小数据量用 Ziplist,大数据量用 HashTable。
    • Redis 7.0 之后,Ziplist 被废弃,全面替换为 Listpack(紧凑列表),解决了 Ziplist 的连锁更新问题。

🌟 面试加分项(实战经验): 面试官如果问:"为什么 Redis 的 ZSet 不用红黑树或者 B+ 树,而用跳表?" 绝杀回答: "第一,因为 B+ 树是为磁盘 IO 设计的(降低树高),而 Redis 是纯内存数据库,不在乎层高;第二,相比红黑树,跳表在执行范围查询(ZSet 最常用的功能 ZRANGE)时更高效,跳表找到起始点后顺着底层链表遍历就行,而红黑树范围查询要用到中序遍历;第三,跳表代码实现更简单,占用内存通过调整概率也可以比红黑树更小。"