2. 数据结构:数据类型与使用场景
❓ 面试官:Redis 常用的数据类型有哪些?你们在业务中是怎么使用的?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): Redis 有 5 种基础数据类型(String、List、Hash、Set、ZSet)和 3 种特殊数据类型(Bitmap、HyperLogLog、Geospatial)。我会根据具体业务场景的特征(比如要不要去重、要不要排序、是不是存对象)来选择合适的数据结构。
📝 详细使用场景解析:
String(字符串):
- 基础缓存:缓存 JSON 格式的用户信息、商品详情等。
- 计数器:利用
INCR命令实现高并发的点赞数、阅读量、库存扣减(原子操作,不会超卖)。 - 分布式锁:利用
SETNX(SET if Not eXists)实现。
Hash(哈希/字典):
- 对象存储:存用户对象(key=user:1, field=name, value=张三)。如果用 String 存 JSON,修改某个字段得全量取出来解析再塞回去;用 Hash 可以直接
HSET user:1 name 李四,精准修改,节省网络带宽。 - 购物车:key=购物车ID(用户ID),field=商品ID,value=购买数量。
- 对象存储:存用户对象(key=user:1, field=name, value=张三)。如果用 String 存 JSON,修改某个字段得全量取出来解析再塞回去;用 Hash 可以直接
List(列表):
- 消息队列:利用
LPUSH和RPOP(或阻塞版的BRPOP)实现简单的异步消息队列。 - 时间线/最新列表:比如用户的历史浏览记录、朋友圈最新动态列表。因为 List 保证插入顺序,且支持分页读取(
LRANGE)。
- 消息队列:利用
Set(集合):
- 去重场景:网站的独立 IP 访问量(UV)统计、每天签到打卡的用户。
- 交并差集(标签推荐):比如求两个人的共同好友(交集
SINTER)、"可能认识的人"(差集SDIFF)、商品标签筛选。
ZSet(有序集合):
- 排行榜:微博热搜榜、游戏积分排名。每个元素带一个分数值(Score),可以按分数自动排序。通过
ZREVRANGE轻松取前 10 名。 - 延时队列:将任务的执行时间戳作为 Score 存入 ZSet,后台线程轮询拿 Score 小于当前时间的任务去执行。
- 排行榜:微博热搜榜、游戏积分排名。每个元素带一个分数值(Score),可以按分数自动排序。通过
❓ 面试官:你刚刚提到了高级数据类型,能说说 Bitmap 和 HyperLogLog 吗?【中级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 这两个都是为了在海量数据下极大地节省内存而生的。Bitmap 用于处理只有 0 和 1 两种状态的布尔值场景(如签到);HyperLogLog 用于海量数据的基数统计(去重估算,如亿级 UV),内存占用极小。
📝 详细原理解析:
Bitmap(位图):
- 原理:它其实不是一种新的数据结构,底层就是 String。它通过按
bit(位)来存数据,每一位只能是 0 或 1。 - 优势:存 1 亿个用户的活跃状态(是/否),如果用普通数字存,可能需要几百 MB;用 Bitmap 存,1 亿个 bit 只需要约
12 MB! - 实战场景:用户全年连续签到打卡记录(key=用户ID:年份,offset=天数,0代表未签,1代表已签到)、统计某天活跃用户总数(
BITCOUNT命令)。
- 原理:它其实不是一种新的数据结构,底层就是 String。它通过按
HyperLogLog:
- 原理:一种概率算法。专门用来做基数统计(即集合中不重复元素的个数)。
- 优势:不管你塞入几百万还是几亿个不同元素,它最多只占用
12 KB内存! - 缺点:有约
0.81%的标准误差;且只能算出总量(PFCOUNT),不能取出具体塞入了哪些元素。 - 实战场景:统计大促期间淘宝首页的独立访客数(UV)。这种场景下,老板只关心大概是 1000 万还是 1010 万,不需要 100% 绝对精确的个位数,用 HLL 完美解决内存爆炸问题。
Stream(流):
- 定位:Redis 5+ 的消息流结构,支持消费组、持久化、ACK,比 Pub/Sub 更像轻量队列。
- 场景:服务内异步解耦、简单事件流;海量交易仍优先专业 MQ。
- 👉 对比 Pub/Sub 见:消息与事务
❓ 面试官:能说说 Redis 里面针对数据类型的底层优化吗?(如跳表、压缩列表)【中高级】
频率:🔥🔥🔥
💡 一句话总结(先抛结论): Redis 速度快不仅是因为内存,还因为它的数据结构会根据数据量的大小自动转换底层实现。最经典的包括:数据量小的时候用压缩列表(Ziplist)省内存,数据量大了换成跳表(SkipList)或哈希表提速度。
📝 详细原理解析:
ZSet 的底层结构:
- 当元素数量较少且值较短时,底层使用 Ziplist(压缩列表)。它是一块连续的内存空间,极端节省内存,但查找慢(O(N))。
- 当数据量变大时,底层会自动转为 SkipList(跳表) + Dict(字典)。
- 什么是跳表?:在普通单向链表的基础上,增加了多级索引层。查找时像跳格子一样从上层往下层找。它的时间复杂度是
O(log N),和平衡二叉树一样快,但实现比红黑树简单得多,且在高并发插入时只需要修改局部指针,不需要像红黑树那样做全局的旋转变色操作。
Hash 的底层结构:
- 早期版本小数据量用 Ziplist,大数据量用 HashTable。
- Redis 7.0 之后,Ziplist 被废弃,全面替换为 Listpack(紧凑列表),解决了 Ziplist 的连锁更新问题。
🌟 面试加分项(实战经验): 面试官如果问:"为什么 Redis 的 ZSet 不用红黑树或者 B+ 树,而用跳表?" 绝杀回答: "第一,因为 B+ 树是为磁盘 IO 设计的(降低树高),而 Redis 是纯内存数据库,不在乎层高;第二,相比红黑树,跳表在执行范围查询(ZSet 最常用的功能 ZRANGE)时更高效,跳表找到起始点后顺着底层链表遍历就行,而红黑树范围查询要用到中序遍历;第三,跳表代码实现更简单,占用内存通过调整概率也可以比红黑树更小。"