GPT学习
INFO
这里是gpt给我的学习计划。
第一周
MySQL 为什么要用索引?B+ 树索引为什么比红黑树、哈希更适合数据库?
你可以把一张数据库表想成一本很厚的书。
没有索引:你查一个人,就得从第一页翻到最后一页,叫全表扫描。 有索引:相当于先看目录,再直接跳到那一页,速度会快很多。
那为什么 MySQL 常用 B+ 树 呢?
因为数据库的数据很多,放不进内存,经常要从磁盘读。 而磁盘读取最怕的不是“算得慢”,而是“读的次数太多”。
B+ 树的特点是:
- 一层能存很多“路标”,树会很矮
- 树越矮,查一次数据要访问的磁盘页越少
- 叶子节点天然有序,特别适合 范围查询、排序、分页
所以数据库喜欢 B+ 树,本质上是:尽量减少磁盘 IO,同时兼顾范围查询。
补充
InnoDB 的主键索引是聚簇索引,数据本身就存放在主键索引的叶子节点;普通二级索引叶子节点存的是主键值,所以通过二级索引查数据时,往往还要再回主键索引查一次,这个过程叫回表。 而 B+ 树的非叶子节点只存索引键和指向子节点的指针,不存数据,这样可以在内存中存更多的索引键,减少树的高度,从而减少磁盘访问次数,提高查询效率。
追问 1:为什么不用红黑树?
因为红黑树是二叉树,每个节点只能分两个方向,树会很高。 数据库数据量大时,树高一上来,就意味着查一次可能要做很多次磁盘 IO,不适合数据库这种磁盘场景。 而 B+ 树是多叉树,树更矮,IO 次数更少。
追问 2:为什么不用哈希?
哈希适合 等值查询,比如 where id = 10。 但它有几个问题:
- 不支持范围查询,比如 id > 10
- 不支持排序
- 不能很好利用最左前缀这类特性
- 哈希冲突还会影响性能
所以数据库通用索引更偏向 B+ 树。
追问 3:什么是聚簇索引,什么是非聚簇索引?
在 InnoDB 里:
- 聚簇索引(主键索引):叶子节点直接存整行数据
- 二级索引(普通索引):叶子节点存的是主键值,不是整行数据
所以:
- 查主键,通常直接就拿到数据
- 查普通索引,可能先找到主键,再去主键索引拿整行,这叫回表
追问 4:什么情况下索引会失效?
高频答案先记这几个:
- 对索引列做函数、计算、类型转换
- 没有满足最左前缀原则
- like 以 % 开头
- or 使用不当:一边有索引,一边没有索引,可能导致全表扫描;两边是俩索引,可能只能用一个索引,另一个索引失效;两边都没有索引,肯定全表扫描了。
- 数据区分度太低,优化器觉得全表扫更划算
- 隐式类型转换,比如字符串列拿数字去查
追问 5:什么是最左前缀匹配?
联合索引比如 (a, b, c),能用上的前提通常是从最左边开始连续匹配。 也就是:
- a ✅
- a, b ✅
- a, b, c ✅
- b, c ❌
- a, c 一般只能部分利用
因为联合索引在底层是按 (a,b,c) 的顺序排好序的。
MySQL 事务是什么?ACID 是什么?四种隔离级别怎么理解?
事务是一组逻辑操作单元,要么全部成功提交,要么全部失败回滚。InnoDB 通过 ACID 保证事务特性:原子性保证要么全做要么全撤销,一致性保证事务前后数据处于合法状态,隔离性解决并发事务互相干扰的问题,持久性保证提交后的结果尽量可靠保存。InnoDB 支持四种隔离级别,默认是 REPEATABLE READ。READ UNCOMMITTED 可能出现脏读;READ COMMITTED 解决脏读,但因为每次读都是新快照,所以还可能有不可重复读和幻读;REPEATABLE READ 下同一事务的普通 SELECT 读取第一次读建立的快照,能避免不可重复读,InnoDB 还会结合 next-key lock 防止幻读;SERIALIZABLE 隔离性最强,但并发性能最差。
追问 1:MySQL 默认隔离级别是什么?
InnoDB 默认是 REPEATABLE READ。
追问 2:READ COMMITTED 和 REPEATABLE READ 最大区别是什么?
核心区别就是:
- READ COMMITTED:每次一致性读都是新快照
- REPEATABLE READ:同一事务里一致性读复用第一次读建立的快照
面试一句话:RC 是语句级快照,RR 更接近事务级快照。
追问 3:什么叫“快照读”?
普通 SELECT 在 READ COMMITTED 和 REPEATABLE READ 下默认是 consistent nonlocking read,也就是一致性非加锁读。
它不会给读到的数据加锁,而是通过多版本机制去看某个时间点的快照。MySQL 文档对 consistent read 的定义就是:用 multi-versioning 给查询呈现某个时间点的数据库快照。
追问 4:什么叫“当前读”?
这个你先有印象就行:
- 快照读:普通 SELECT
- 当前读:读最新版本并且可能加锁,比如: SELECT ... FOR UPDATE、SELECT ... FOR SHARE、UPDATE、DELETE
这些加锁读在事务提交或回滚时释放锁,而且需要关闭自动提交或显式开启事务。
Select ... For Update的用途
- 实现分布式锁:在事务里 SELECT ... FOR UPDATE 某行,如果能成功加锁,就说明抢到锁了;如果被别人锁住了,就会等锁释放或者超时。
- 乐观锁的补充:先用普通 SELECT 读数据,做一些业务逻辑判断后,再用 SELECT ... FOR UPDATE 验证数据没有被别人改过,最后再 UPDATE。
- 处理库存等并发修改的场景:先 SELECT ... FOR UPDATE 锁住相关记录,确保在修改库存时不会有其他事务同时修改,避免超卖。
追问 5:什么是 gap lock?什么是 next-key lock?
这是你后面学“锁”和“MVCC”一定会遇到的。
- gap lock:锁的是“索引记录之间的空隙”,主要作用是防止别人往这个区间插入新记录。MySQL 文档明确说 gap lock 的目的就是阻止插入。
- next-key lock:记录锁 + 前面的 gap lock,也就是“锁住这条记录,同时锁住它前面的间隙”。InnoDB 默认在 REPEATABLE READ 下对搜索和索引扫描使用 next-key locks,用来防止 phantom rows。
乐观锁和悲观锁分别是什么?version、stock > 0、for update 各算哪一种?
乐观锁和悲观锁的核心区别在于是否先加锁。悲观锁假设冲突很多,所以会先锁住数据再处理,MySQL 里典型做法是 select ... for update,它属于 locking read,会读取最新值并加锁。乐观锁假设冲突不多,所以读的时候不加锁,而是在更新时通过条件校验来判断数据有没有被别人改过,常见实现是版本号 version 或条件更新,比如 update ... where stock > 0 或 where version = ?。悲观锁更适合竞争激烈、必须保证中间过程不被打断的场景;乐观锁更适合读多写少、冲突不高、追求吞吐的场景。
MVCC 到底是什么?为什么普通 SELECT 不加锁也能读?
面试背这个: MVCC 是多版本并发控制,主要解决普通 SELECT 的一致性读问题;而 INSERT / UPDATE / DELETE 负责生产或推进这些版本。InnoDB 会为被修改的行保留旧版本信息,旧版本主要通过 undo log 保存;每行还会维护像 DB_TRX_ID、DB_ROLL_PTR 这样的隐藏字段。普通 SELECT 在 READ COMMITTED 和 REPEATABLE READ 下默认属于一致性非加锁读,数据库会根据事务的快照规则判断当前行的哪个版本对它可见,因此普通查询通常不需要加锁也能读到一致结果。READ COMMITTED 下每次一致性读都会读取新的快照,而 REPEATABLE READ 下同一事务内的普通查询会复用第一次读建立的快照。MVCC 主要服务于快照读,提高读写并发;对于 SELECT ... FOR UPDATE、UPDATE、DELETE 这类当前读或加锁语句,则还要结合行锁、gap lock、next-key lock 来保证并发正确性。
MVCC 让快照读成为可能;至于快照是“每次新建”还是“复用第一次的”,取决于隔离级别。
- RC(READ COMMITTED):每次 consistent read 都会读取一个新的快照
- RR(REPEATABLE READ):同一事务里的所有 consistent read 都复用第一次读建立的快照
MVCC = 多版本并发控制。 你可以把它理解成:数据库不会只保存“当前这一版数据”,还会保留旧版本的信息,这样不同事务就能在并发时各自看到自己该看到的版本。 InnoDB 官方就直接说它是一个 multi-version storage engine,会保存被修改行的旧版本信息来支持并发和回滚;这些旧版本信息保存在 undo tablespaces 的 rollback segment 里。
- 隐藏字段
InnoDB 在每行里会额外维护隐藏信息。官方文档列出了至少这两个关键字段:
DB_TRX_ID:最后一次修改这行的事务 ID DB_ROLL_PTR:指向 undo log 的指针
- undo log
undo log 里保存旧版本所需的信息。 如果当前版本不该给你看,数据库就可以沿着 undo 去还原更早的版本。官方明确说,InnoDB 使用 rollback segment 里的信息去构建 earlier versions of a row for a consistent read。
- Read View
你可以把它理解成:“我这个事务此刻观察世界的时间点规则”。 官方文档虽然在这几页里没有把 Read View 细节全展开,但明确说了:在 REPEATABLE READ 下,同一事务内的 consistent reads 读取的是第一次读建立的 snapshot;在 READ COMMITTED 下,每次 consistent read 都会读取自己的 fresh snapshot。
MVCC 是否能解决幻读?
MVCC 主要解决的是普通查询的一致性读问题。 在 InnoDB 默认的 REPEATABLE READ 下,普通 SELECT 依靠 snapshot 做到可重复读;而对于 locking reads、UPDATE、DELETE 这类会扫描索引范围的语句,InnoDB 会用 gap lock / next-key lock 来防止范围内出现新的插入,从而防止 phantom rows。官方文档说明了两件事:一是 REPEATABLE READ 下普通查询读第一次快照;二是默认会对搜索和索引扫描使用 next-key locks 来防止 phantom rows。
所以面试里更稳的说法是:普通查询靠 MVCC;涉及当前读和范围修改时,再结合 next-key lock 处理幻读。
追问 1:为什么 MVCC 能提高并发?
因为如果没有 MVCC, 普通读为了保证一致性,很多时候就得加锁;一旦读也大量加锁,读写就会互相卡住。
而 InnoDB 的 consistent read 默认不加锁,所以:
- 普通读不容易阻塞写
- 写也不一定非得把普通读全堵住
- 在很多读多写少的业务里,并发会好很多
这就是为什么大家总说:
MVCC 用空间换时间,用多版本换并发。这句话是工程总结,不是官方原句,但和官方机制描述是一致的:为了支持 consistent read,InnoDB 保留旧版本信息,并可通过 undo 构建早期版本。
对于RR
- 如果流程是 SELECT → 你自己 UPDATE 同一行 → SELECT,第二次 SELECT 会看到你自己改后的值。
- 如果中间是别人改了并提交,而你自己没改这行,那么你后面的普通 SELECT 仍然通常看到第一次快照里的旧值。
- 如果中间别人改了,而你后面的 UPDATE 又去操作了那行,那么这条 UPDATE 走的是更“当前”的加锁/修改语义,不完全受第一次快照约束;一旦它被你更新成功,后面的普通 SELECT 就能看到你刚更新后的结果。 官方把这种现象明确列为 RR 下的一种 anomaly。
MySQL 里的行锁、间隙锁、next-key lock,到底分别锁什么?
InnoDB 的行锁本质上是索引记录锁。记录锁锁的是某个索引记录;间隙锁锁的是索引记录之间的空隙,只用来阻止插入;next-key lock是记录锁加前面的间隙锁。RR 下 InnoDB 默认对搜索和索引扫描使用 next-key lock 来防止幻读;而 RC 下通常关闭 gap lock,所以更多是只锁记录,不锁 gap。对于唯一索引上的唯一条件,通常只需要记录锁;对于范围查询或非唯一条件,通常会锁扫描到的索引范围。
追问 1:为什么叫“行锁”,但实际上是索引记录锁?
InnoDB 的行锁是基于索引实现的,所以它锁的不是物理行,而是索引记录。官方文档明确说,InnoDB 的行锁是索引记录锁,锁住的是索引记录而不是物理行。也就是说,如果你对一个表没有索引,InnoDB 就只能退化成全表锁了。
追问 2:INSERT 又是什么锁?
这个也很高频。
官方说:INSERT 对插入进去的那一行,设置的是排他的 index-record lock,这不是 next-key lock,不会锁前面的 gap。但在真正插入前,会先加一种 insert intention gap lock,表示“我打算往这个 gap 里插入”
官方还特别说明:如果两个事务都往同一个 gap 里插,但插入的位置不同,它们不一定互相阻塞。
白话理解:就是先举手说“我要往这个空隙里塞一条记录。”,如果你和别人不是要塞到完全冲突的位置,可能可以并行。
追问 3:追问 1:SELECT ... FOR UPDATE 一定只锁最终结果行吗?
不一定。
官方说,UPDATE、DELETE、locking read 一般会对扫描到的索引记录加锁,不是只看最终 WHERE 过滤后剩下的那些行;因为 InnoDB 知道的是“扫描了哪些索引范围”,而不是精确保留你写的 WHERE 条件。
追问 4:为什么范围查询更容易造成锁冲突?
因为它锁的不是一个点,而是一个区间,甚至连区间里的 gap 都可能一起锁住。
追问 5:为什么唯一索引很重要?
因为唯一索引 + 唯一条件通常可以把锁精确到一条记录,而不是一整个范围。如果没有唯一索引,很多 UPDATE、DELETE、SELECT ... FOR UPDATE 就只能锁一个范围,导致更多的锁冲突和性能问题。
死锁是怎么产生的?为什么两个看起来正常的 UPDATE 也会死锁?
死锁就是两个或多个事务互相等待对方持有的锁,导致都无法继续执行。InnoDB 默认会自动检测死锁,并回滚其中一个事务。两个看起来正常的 UPDATE 也可能死锁,因为数据库执行时不是只看 SQL 文本,而是看加锁顺序和锁范围。
比如事务 A 先更新 id=1 再更新 id=2,事务 B 先更新 id=2 再更新 id=1,就可能形成环路等待。另外,UPDATE 和 locking read 往往会锁扫描到的索引记录,在 RR 下还可能是 next-key lock,所以范围更新、索引不佳、访问顺序不一致都会增加死锁概率。降低死锁的方法主要是统一资源访问顺序、缩短事务、用好索引、并在应用层做好事务重试。
Redis 为什么快?
Redis 快主要有几个原因。
- 第一,Redis 的数据主要放在内存里,避免了磁盘随机 IO,所以延迟很低。
- 第二,Redis 的核心命令处理采用 mostly single-threaded 的事件驱动模型,减少了多线程切换和锁竞争开销。
- 第三,Redis 的命令和数据结构都比较轻量,很多操作可以直接在内存中高效完成。
- 第四,Redis 支持 pipelining,可以减少网络往返开销;从 Redis 6 开始,部分网络 I/O 也引入了多线程来进一步提升吞吐。 需要注意的是,Redis 并不是绝对不会慢,如果出现大 key、慢命令、热 key、频繁持久化或 fork 开销,也会出现延迟抖动。
追问 1:Redis 是单线程的吗?
Redis 的核心命令处理是 mostly single-threaded 的事件驱动模型,绝大部分命令在主线程执行,避免了多线程切换和锁竞争开销。但从 Redis 6 开始,部分网络 I/O 引入了多线程来提升吞吐,主线程仍然负责命令处理和数据访问,所以可以说 Redis 是单线程的,但在网络 I/O 上有多线程优化。
追问 2:单线程为什么还能这么快?
因为 Redis 的典型场景是:
- 内存操作
- 单条命令短小
- 避免锁竞争
- 可以 pipeline 减少网络 RTT
追问 3:Redis 快是不是因为用了哈希表?
这只能算一部分。 很多 key 查找确实依赖高效字典结构,但 Redis 快的根本不只是“哈希表”,还包括内存访问、简单执行模型、网络优化、内置数据结构设计。
追问 4:什么情况下 Redis 可能会慢?
一些常见原因包括:
- 大 key:比如一个非常大的 hash、list、set,操作时需要处理大量
- 慢命令:比如 keys *、sort、zrange withscores 等需要扫描大量数据的命令
- 热 key:某个 key 被频繁访问,可能导致单线程处理压力大
- 持久化:RDB 快照或 AOF 重写时会 fork,
- fork 开销:如果数据量大,fork 可能会导致主线程短暂停顿,出现延迟抖动
- 内存压力:如果 Redis 内存使用接近上限,GC 或者内存碎片可能导致性能问题
- 网络问题:如果客户端和 Redis 之间网络不稳定,也可能导致请求延迟增加
- ...
Redis 持久化是什么?RDB 和 AOF 有什么区别?
Redis 持久化是把内存中的数据保存到磁盘,方便重启后恢复。常见方式有 RDB 和 AOF。RDB 是定期生成某个时间点的数据快照,优点是文件紧凑、恢复快、适合备份,缺点是宕机时可能丢最近几分钟的数据。AOF 是把每次写命令追加记录下来,重启时通过回放日志恢复数据,优点是更耐久,尤其在 appendfsync everysec 或 always 下数据更安全,缺点是文件通常更大、恢复通常比 RDB 慢。Redis 还支持 AOF rewrite 来压缩日志。线上如果特别重视数据安全,通常会同时开启 RDB 和 AOF;如果只是纯缓存,可能只开 RDB,甚至不开持久化。
追问 1:RDB 一定比 AOF 快吗?
一般面试里可以答:恢复通常更快,运行时开销通常也更小一些,但它的代价是耐久性更弱。Redis 官方明确说 RDB 对大数据集重启更快,而 AOF 在不同 fsync 策略下性能会有不同。
追问 2:AOF 一定不会丢数据吗?
不一定。 如果是 appendfsync everysec,官方明确说灾难情况下仍可能丢大约 1 秒的数据;如果是 appendfsync no,风险会更大。只有 always 更安全,但性能代价很高。
追问 3:为什么 AOF 文件会变小?
准确说不是“自动变小”,而是通过 rewrite 把很多冗余历史命令压成恢复当前状态所需的最小命令集。
追问 4:Redis 7 的 AOF 有什么变化?
可以答:Redis 7 开始用了 multi-part AOF,把 AOF 拆成 base file、incremental files,并用 manifest 管理。
缓存穿透
穿透就是:你查的这个数据本来就不存在。 所以流程会变成:
- 查 Redis,没有
- 查数据库,也没有
- 下次还有人查这个不存在的数据,又重复一遍
如果有人恶意一直查不存在的 id,这种请求就会直接穿过缓存,持续打到数据库。AWS 官方博客就把这种情况叫做 cache penetration problem,并建议用 negative caching 减少无意义的数据库查询。
怎么解决
最常见是这 3 个:
第一,缓存空值 / 负缓存。 数据库查出来不存在,也往缓存里写一个“空结果”,并设置较短 TTL。AWS 官方博客明确把 negative caching 作为解决 cache penetration 的方法之一。
第二,Bloom Filter。 在请求真正查缓存/数据库前,先用 Bloom filter 判断“这个 key 理论上是否可能存在”。Redis 官方文档说明,Bloom filter 能用极小内存判断某个元素是否在集合中,并且对于“不存在”的判断非常高效。
第三,参数校验。 比如非法 id、明显越界的用户编号、格式错误的 key,直接在业务层拦掉。 这个更多是工程常识,不是 Redis 专属能力。
面试回答
缓存穿透是指请求的数据本身不存在,导致每次请求都会同时 miss 缓存和数据库,请求直接穿过缓存层打到下游。常见解决方案是缓存空值、使用 Bloom Filter 预判 key 是否可能存在、以及在业务层做参数合法性校验。 AWS 官方架构博客直接提到了 negative caching;Redis 官方文档则提供了 Bloom filter 这种适合做存在性预判的概率型数据结构。
缓存击穿
击穿一般指:一个很热的 key,在某个瞬间过期了, 然后大量并发请求同时发现缓存 miss,一起去查数据库。
它不是“不存在”,而是:
- 这个 key 原本存在
- 而且很热门
- 但它刚好过期了
- 大量请求同时回源
Redis 官方博客把这种并发 miss 后同时触发同一份回源工作的现象称为 stampede。
怎么解决
第一,加互斥锁 / singleflight。 只让一个请求去查数据库并重建缓存,其他请求等待它。Redis 官方博客讲 cache stampede 时,核心思路之一就是用锁来避免多个并发请求重复做同一份回源工作。
第二,逻辑过期 / 后台刷新。 热点 key 不直接让它“物理消失”,而是返回旧值,同时异步刷新。这样能减少瞬时回源洪峰。Redis 生态文档里也有 stampede protection、后台刷新这类思路。
第三,热点 key 永不过期,靠主动更新。 这个是常见工程手段。对极热点数据,很多系统会避免让它自然过期,而是用消息通知、定时任务或写后更新。
面试回答
缓存击穿通常指某个热点 key 在失效瞬间遭遇大量并发访问,导致很多请求同时回源数据库,本质上是一种 cache stampede。常见解决方案包括对重建缓存过程加互斥锁、做 singleflight 合并请求、使用逻辑过期配合异步刷新,或者让极热点 key 通过主动更新而不是自然过期。 Redis 官方博客对 stampede 的定义和“locks/promises”方案有直接说明。
缓存雪崩
雪崩就是:不是一个 key 出问题,而是一大片 key 同时失效,或者整个缓存层大面积不可用。
于是原本应该被 Redis 挡住的流量,短时间内大规模打到数据库。 AWS 官方架构博客提到,很多 feature group 如果 TTL 时间接近,会出现集中失效,因此会给 TTL 加 jitter,把过期时间打散,来缓解 cache stampede。
怎么解决
第一,TTL 加随机值。 这是最经典的方法。不要让很多 key 同时过期。AWS 官方博客明确说会加入 jitter 来分散 TTL 删除时间,缓解 stampede。
第二,多级缓存。 本地缓存 + Redis + DB,避免 Redis 一层失守就直接把所有流量打到数据库。AWS 官方博客的案例就用了 local cache、remote cache、database 的多级结构。
第三,限流、降级、熔断。 当缓存层异常时,保护数据库,不让所有请求都无上限回源。 这更多是系统设计常识。
第四,热点数据预热。 在大促、活动开始前,提前把热点 key 打进缓存。
面试回答
缓存雪崩是指大量 key 在同一时间失效,或者缓存层整体不可用,导致原本应由缓存承接的流量在短时间内集中回源下游。最常见的解决思路是给 TTL 加随机扰动打散过期时间、使用多级缓存、在下游加限流和降级保护,以及提前预热热点数据。 AWS 官方博客直接提到了 TTL jitter 和多级缓存策略。
追问 1:Bloom Filter 为什么能防穿透?
因为它能快速判断一个元素一定不存在,从而把这类请求挡在缓存层前面。Redis 官方文档明确说明,Bloom filter 用很小内存就能做存在性判断。
追问 2:空值缓存会不会有问题?
会有两个常见问题:
会占用一些缓存空间 如果数据后来真的被创建了,短 TTL 内可能还会命中旧的“空值结果”
所以通常会给空值设置更短 TTL。AWS 的官方案例就是用 negative caching 来减少无效查询。
追问 3:击穿和雪崩的关系是什么?
击穿更像是单个热点 key 的 stampede。 雪崩更像是很多 key 一起失效,或者缓存整体失守,范围更大。
缓存穿透、击穿、雪崩分别是什么?
缓存穿透、击穿、雪崩的共同点都是缓存没有把请求挡住,流量回到了数据库。穿透指请求的数据本身不存在,所以每次都会 miss 缓存和数据库,常见解决方案是缓存空值、Bloom Filter 和参数校验。击穿通常指某个热点 key 刚好失效时,大量并发请求同时回源,本质上是一种 cache stampede,常见解决方案是互斥锁、singleflight、逻辑过期和异步刷新。雪崩指大量 key 同时过期,或者缓存层整体不可用,导致大批流量集中打到下游,常见解决方案是给 TTL 加随机值、做多级缓存、限流降级和热点预热。 Redis 官方博客对 stampede 有直接定义,Redis 官方文档提供了 Bloom Filter,AWS 官方架构博客则直接给出了 negative caching 和 TTL jitter 的工程实践。
Redis 的过期删除策略和内存淘汰策略分别是什么?
- 过期删除策略回答的是:“这个 key 的 TTL 到了,Redis 怎么把它删掉?”
- 内存淘汰策略回答的是:“Redis 内存已经到 maxmemory 上限了,这时如果还要写新数据怎么办?”
Redis 官方文档里这两套机制是分开的:过期属于 EXPIRE 相关机制,淘汰属于 maxmemory-policy。
Redis 里过期删除策略和内存淘汰策略是两回事。过期删除是针对设置了 TTL 的 key,Redis 主要通过两种方式处理:一种是惰性删除,也就是访问 key 时发现已经过期就立即删除;另一种是定期主动删除,后台随机采样带 TTL 的 key,把已经过期的删掉。内存淘汰策略则是在 Redis 达到 maxmemory 上限后触发,Redis 会根据 maxmemory-policy 选择淘汰哪些 key。常见策略有 noeviction、allkeys-lru、allkeys-lfu、allkeys-random,以及只针对带 TTL 数据的 volatile-lru、volatile-lfu、volatile-random、volatile-ttl。如果没有任何 key 设置 TTL,这类策略会退化得像 noeviction 一样。所以一个 key 被删,可能是因为过期了,也可能是因为内存满了被淘汰了,这两个机制不能混为一谈。
追问 1:Redis 是不是 TTL 一到就马上删?
不是。 Redis 官方明确说过期是通过被访问时检查,或者后台增量清理来完成,所以不保证 TTL 归零那一刻立刻删。
追问 2:volatile-* 和 allkeys-* 的区别是什么?
allkeys-* 是在所有 key 里选。 volatile-* 是只在带过期时间的 key 里选。没有 TTL 的 key 不参与。
追问 3:缓存场景一般选什么淘汰策略?
官方文档给的经验是: 如果热点访问明显,allkeys-lru 往往是一个不错的默认选择;如果更看重访问频率,可以考虑 allkeys-lfu。
Redis 分布式锁怎么做?为什么不能简单 SETNX 一下就完事?
最基础可落地的 Redis 分布式锁,不是只写一个 SETNX,而是:
SET lock_key unique_token NX PX 30000
- NX:只有 key 不存在时才设置成功
- PX 30000:给锁加过期时间,避免持锁进程挂掉后锁永远不释放
- unique_token:每个客户端自己的随机值,释放锁时用来确认“这把锁是不是我加的”
- 返回 OK 说明拿锁成功;返回 Nil 说明没抢到。
Redis 官方文档里已经明确说了:历史上的 SETNX 锁模式不推荐,更简单的单实例做法是用 SET ... NX EX/PX 获取锁,再用 Lua 脚本保证“比较 value 和删除 key”是原子的:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
endRedis 官方文档就是这么建议的,而且 Redis 也保证 Lua 脚本在服务器端原子执行。
面试回答: 不能只用 SETNX,原因有三个。第一,如果不带过期时间,持锁进程异常退出后可能造成死锁;第二,如果写成 SETNX 再 EXPIRE,两步不是原子操作,中间崩溃会留下没有 TTL 的锁;第三,释放锁时不能直接 DEL,因为锁可能已经过期并被别人重新获取,所以必须给锁设置唯一随机 token,并通过 Lua 脚本在 value 匹配时再删除。更稳妥的单实例做法是 SET key token NX PX ttl 获取锁,Lua 原子脚本解锁;如果需要更高容错和更强保证,可以进一步了解 Redis 官方文档里的 Redlock。
第一层问题:没有过期时间,进程挂了就可能死锁
如果你只是这样写: SETNX lock:order 1
拿到锁的服务如果宕机了,这个 key 可能一直留在 Redis 里,别人永远拿不到锁。所以锁一定要带自动过期时间。Redis 官方在历史 SETNX 锁说明里也专门讨论了 timeout 问题,而现在更推荐直接用 SET ... NX EX/PX 一步完成“加锁 + 过期时间”。
第二层问题:SETNX + EXPIRE 分两步写,不原子
很多人会写: SETNX lock:order 1EXPIRE lock:order 30
问题是这两步不是原子操作。 如果 SETNX 成功后,服务还没来得及执行 EXPIRE 就挂了,这把锁仍然没有 TTL,还是可能卡死。Redis 官方推荐直接用单条 SET ... NX EX/PX,本质上就是为了把“创建锁”和“设置过期时间”合成一个原子动作。
第三层问题:释放锁时不能直接 DEL
这点非常高频。
假设你拿锁时设了 30 秒过期。 结果你的业务执行太久,30 秒后锁自动过期了;这时别的客户端已经重新拿到了同名锁。 如果你这时再直接:
DEL lock:order
你删掉的就可能是别人刚拿到的新锁。
Redis 官方文档明确说,正确做法是把锁的 value 设成一个不可猜测的随机 token,释放时只在 value 还等于这个 token 时才删除。
这个锁就绝对安全吗?
不是。 Redis 官方自己也说了:上面这个是单实例锁模式;如果你要更高保证、要容忍单点故障,官方给出的更“canonical”的方案是 Redlock。同时,SETNX 页面也直接写了,旧的 SETNX 模式是discouraged,推荐用更安全的方案。
追问 1:为什么解锁要用 Lua?
因为 Redis Lua 脚本原子执行,可以把“判断 token 是否匹配”和“删除 key”合并成一个不可分割的操作。
追问 2:为什么 SETNX + EXPIRE 不行?
因为它是两条命令,不原子;SET ... NX EX/PX 是一条命令,能一步完成。
追问 3:SETNX 现在还能用吗?
能用,但 Redis 官方文档把这种历史锁模式标成了 discouraged,更推荐 SET 模式或 Redlock。
追问 4:单机 Redis 锁够不够?
面试里更稳的回答是: 很多业务场景下,单实例 + SET NX PX + Lua 已经够用;但如果系统对锁语义要求很强、还要考虑故障容忍,Redis 官方提供了 Redlock 作为更高保证的方案。
Redlock 是什么?
Redlock 是 Redis 官方提出的一种“多节点分布式锁算法”,目的就是比单机 Redis 锁更能抗单点故障。它的目标是:比“单个 Redis 实例加锁”更能抗单点故障,在大多数 Redis 节点还活着时,尽量保证同一时刻只有一个客户端拿到锁。Redis 官方把它描述为一种更“canonical”的 DLM(Distributed Lock Manager)算法,并给了三类目标:互斥、无死锁、以及多数节点存活时仍可获取/释放锁。
核心思路是:把同一把锁同时去 5 台彼此独立的 Redis master 上“投票”,拿到多数派才算真的拿到锁。Redis 官方示例里常用 N=5,客户端需要在至少 3 台上成功,并且总耗时还要小于锁的有效期。
Redlock 算法流程:
- 准备一个随机 token 和锁 TTL。
- 记录开始时间。
- 并行向多个独立 Redis master 执行 SET key token NX PX ttl。
- 如果在多数节点成功,而且总耗时小于 ttl,则认为加锁成功。
- 锁的实际有效时间 = 原始 ttl - 获取锁花掉的时间。
- 如果失败,就尽量去所有节点释放这把锁。
Redlock 不是“万能强一致分布式锁”,而是 Redis 官方提出的、比单机锁更强一些的多节点锁方案。
面试回答: Redlock 是 Redis 官方提出的一种多节点分布式锁算法。它假设有多个彼此独立的 Redis master,客户端会用同一个 key 和随机 token 去所有节点尝试 SET NX PX 加锁。只有当客户端在多数节点上成功获取锁,并且总耗时小于锁的有效期时,才认为加锁成功。这样做是为了比单实例或简单主从切换方案更能抗单点故障,因为 Redis 复制是异步的,主从切换可能导致同一把锁被两个客户端同时持有。解锁时不能直接 DEL,而要校验 token 后再删。需要注意的是,Redlock 不是绝对强一致方案,Redis 官方文档也提醒要关注 fencing token、时钟漂移和一致性边界。