Redis 面试笔记
1 数据类型与底层数据结构
Redis 有五种对象类型:
| 类型常量 | 对象名称 | 底层数据结构 |
|---|---|---|
| REDIS_STRING | 字符串对象 | INT / EMBSTR / RAW(简单动态字符串 SDS) |
| REDIS_LIST | 列表对象 | LINKEDLIST(双端链表)/ ZIPLIST(压缩列表)/ quicklist |
| REDIS_HASH | 哈希对象 | HT(字典)/ ZIPLIST |
| REDIS_SET | 集合对象 | HT / INTSET(整数集合) |
| REDIS_ZSET | 有序集合对象 | ZIPLIST / SKIPLIST(跳跃表+字典) |
有序集合 ZSET 的实现
- 元素数量小于 128 且所有 member 长度小于 64 字节时使用 ziplist(压缩列表) 编码,上限可通过
zset-max-ziplist-entries和zset-max-ziplist-value修改; - 否则使用 hash table + 跳跃表 + 双端链表实现;
- 为何不用平衡树:内存占用少、支持范围查找、实现相对容易。
后续 Redis 版本把 ziplist 优化为 listpack,作为 quicklist 的节点。
排行榜中分数相同、按时间排序怎么做
- 把 score 拆成高 32 位和低 32 位,高 32 位存分数、低 32 位存时间;
- 将 score 作为小数整数部分、时间戳作为小数部分,即
score.timestamp的格式作为分数。
2 五种对象的编码转换
- 字符串对象:int(保存整数)→ embstr(短字符串,一次性分配一块连续内存)→ raw(长字符串);
- 列表对象:ziplist → linkedlist(触发条件:列表元素超过配置阈值或单个元素过大);
- 哈希对象:ziplist → hashtable;
- 集合对象:intset → hashtable;
- 有序集合:ziplist → skiplist。
3 为什么 Redis 快
- 完全基于内存:绝大部分请求是纯粹的内存操作,类似 HashMap 的查找和操作时间复杂度 O(1);
- 数据结构简单,是专门设计的;
- 单线程:避免了上下文切换和竞争条件,不存在加锁/死锁性能消耗;
- 多路 I/O 复用,非阻塞 IO:利用 evport(solaris) → epoll(linux) → poll(os x/freebsd) → select 同时监察多个流(网络连接)的 I/O 事件,空闲时阻塞当前线程,有 I/O 事件时唤醒并轮询(epoll 只轮询真正触发的流),”多路”指多个网络连接,”复用”指复用同一个线程;
- 自己构建了 VM 机制,避免系统调用函数的内核态/用户态切换损耗。
4 持久化:RDB 与 AOF
- RDB:指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。父进程 fork 出子进程,子进程写临时 RDB 文件(bgsave),父进程继续服务;
- AOF:以 Redis 协议格式记录所有写操作命令,追加到文件末尾;启动时通过重新执行这些命令还原数据集。支持后台 rewrite(BGREWRITEAOF),使 AOF 体积不超过保存数据集的实际大小;
- 混合:可同时使用 AOF 和 RDB,重启时优先使用 AOF 还原数据集,因为 AOF 保存的数据集通常更完整;
- 也可以关闭持久化,让数据只在运行时存在。
5 主从同步机制
- 支持主从同步、从从同步;
- 第一次同步:主节点做一次 bgsave,同时将后续修改操作记录到内存 buffer;完成后将 RDB 文件全量同步到从节点,从节点加载 RDB 镜像到内存;加载完成后通知主节点将期间的修改操作同步过来重放,完成同步;
- 后续通过增量同步(积累命令)保持数据一致。
6 性能问题与解决方案
- Master 最好不做持久化工作(RDB 内存快照和 AOF 日志文件),
save命令调度 rdbSave 会阻塞主线程,快照大时影响性能、间断性暂停服务; - 数据重要时由某个 Slave 开启 AOF 备份,策略设为每秒同步一次;
- 为保主从复制速度和连接稳定,Master 和 Slave 最好在同一局域网;
- 尽量避免在压力很大的主库上增加从库;
- 主从复制不要用图状结构,用单向链表结构更稳定:
Master <- Slave1 <- Slave2 <- Slave3...,方便解决单点故障,Master 挂了可立刻启用 Slave1 做 Master; - BGREWRITEAOF 重写会占大量 CPU 和内存,导致服务 load 过高、短暂暂停。
7 淘汰策略(LRU)
问题:MySQL 有 2000w 数据,Redis 只存 20w,如何保证都是热点数据 → 用 lru 算法淘汰。
Redis 提供 6 种淘汰策略:
| 策略 | 说明 |
|---|---|
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
| volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从数据集中任意选择数据淘汰 |
| no-eviction | 禁止驱逐数据(默认策略,内存满时写入报错) |
8 Redis 与 Memcache 的区别
| 维度 | Memcache | Redis |
|---|---|---|
| 存储方式 | 全存内存,断电挂掉,数据不能超过内存大小 | 可持久化到硬盘 |
| 数据类型 | 支持简单 | 复杂数据类型(String/List/Set/ZSet/Hash) |
| 底层模型 | 通用系统调用 | 自建 VM 机制,减少内核态/用户态切换 |
| value 大小 | 最大 1MB | 最大可达 1GB |
9 应用场景
- 会话缓存(Session Cache):Redis 提供持久化,比 Memcached 有优势;
- 全页缓存(FPC):重启实例有磁盘持久化,页面加载不会下降;
- 队列:list 和 set 操作使其成为很好的消息队列平台,类似语言本身对 list 的 push/pop;
- 排行榜/计数器:内存中对数字递增递减操作极佳,Sorted Set 让排行榜实现简单;
- 发布/订阅:社交网络连接、脚本触发器、聊天系统等。
10 缓存穿透 / 击穿 / 雪崩
缓存穿透
查询一个必然不存在的数据,缓存不命中时不写缓存,导致每次请求都打到存储层。流量大时 DB 可能挂掉,人可利用不存在的 key 攻击。
解决:
- 布隆过滤器:把所有可能存在的数据哈希到足够大的 bitmap 中,一定不存在的数据会被拦截,避免对底层存储的查询压力;
- 缓存空值:查询返回为空(数据不存在或系统故障)也缓存该空结果,但过期时间很短,最长不超过五分钟。
缓存雪崩
设置缓存时采用了相同过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决:
- 加锁或队列保证缓存的单线程(进程)写,避免失效时大量并发请求落到底层存储;
- 简单方案:把缓存失效时间分散开,在原失效时间基础上加一个随机值(如 1-5 分钟随机),降低过期时间重复率。
缓存击穿
某个热点 key 过期瞬间被超高并发访问,大并发请求在缓存过期时从 DB 加载都回设缓存,瞬间把 DB 压垮。区别:击穿针对某一个 key,雪崩针对很多 key。
解决:
- 互斥锁(mutex key):缓存失效时不立即 load DB,先用 SETNX/Memcache ADD 设置一个 mutex key,设置成功再进行 load DB 并回设缓存;否则重试整个 get 流程;
- “提前”使用互斥锁:value 内部设置一个比实际 timeout 更小的 timeout1,读到 timeout1 过期时立即延长 timeout1 并重新设置,再从数据库加载数据回设;
- “永远不过期”:
- 物理不过期:Redis 层面确实不设置过期时间,避免热点 key 过期问题;
- 逻辑过期:把过期时间存在 key 对应 value 里,快过期时由后台异步线程进行缓存构建。对性能非常友好,唯一不足是构建缓存时其他线程可能访问老数据。
11 布隆过滤器
- 定义:概率型数据结构,特点是可以高效地插入和查询,能告诉你”某样东西一定不存在或可能存在“;
- 原理:一个 bit 向量/数组,用多个不同的哈希函数生成多个哈希值,把对应的 bit 位置 1。查询时若 k 个 bit 位都为 1 只能说可能存在(可能有哈希碰撞);若存在为 0 的 bit 位,则一定不存在;
- 特点:比 List/Set/Map 更高效、占用空间更少,但结果是概率性的;不支持删除(删除会导致 false negative);
- 参数估算:假设 m 位空间、k 个哈希函数、n 个 item,false positive 概率 p≈(1−e^(−kn/m))^k,最优 k = (m/n)ln2;
- 应用场景:垃圾邮件地址过滤、爬虫 URL 去重、解决缓存穿透、Bigtable/HBase 查不存在的行或列减少磁盘 IO、Chrome 加速安全浏览。
业务实现(用户 3 天内不收到同样的 item)
构造三个 Bloom filter:Bf0 记录 day0/1/2,Bf1 记录 day1/2/3,Bf2 记录 day2/3/4;每个 Bf 有 3 天寿命,到期清零重新记录,彼此相差 1 天;每个 item 都要 ADD 入三个 Bf。这样任何时刻都至少有一个 Bf 记录了近 2 天的推送历史。
阅读 —
·
全站 —