Redis、Java 并发、Spring 和 MySQL 的核心知识点
4. Redis 为什么快?(宏观分析)
Redis 的高性能并非单一原因造成的,而是多个设计层面共同作用的结果。我们可以从以下 5 个方面 去分析:
- 纯内存操作:数据主要存储在内存中,读写速度远高于磁盘。
- 单线程模型:避免了多线程的上下文切换和锁竞争开销。
- IO 多路复用:基于 Reactor 模式,一个线程能高效处理成千上万个连接请求。
- 高效的数据结构:底层针对不同场景优化了数据结构(如压缩列表、跳表等),保证了操作的高效性。
- 协议简单:RESP 通信协议解析高效,且 C 语言实现,贴近操作系统。
5. 底层数据结构如何实现快速定位?
Redis 为了兼顾时间和空间效率,在底层数据结构上做了很多优化,根据数据量大小和内容自动选择最优结构:
- SDS:动态字符串,通过预分配内存减少频繁分配。
- Hash(哈希):
- 小数据时用 ziplist(压缩列表),节省内存。
- 大数据时用 hashtable(哈希表),通过拉链法解决冲突,实现 O(1) 的查找、插入和删除。
- Zset(有序集合):
- 小数据时用 ziplist。
- 大数据时用 跳表结合哈希表。跳表通过多级索引实现了 O(logN) 的查询效率,且范围查询性能极佳。
- IntSet(整数集合):当 Set 都是整数且量少时使用,基于有序数组实现,查找快且省内存。
6. List 结构的“快速列表”是怎样的形式?
在 Redis 3.2 之前,List 是用 ziplist 或 linkedlist 实现的。现在的版本使用的是 quicklist。
你可以把它理解为:“一个用双向链表连接起来的压缩列表”。
- 结构形式:
- 它是一个双向链表(
linkedlist)。 - 但链表的每一个节点,不再是一个简单的字符串,而是一个压缩列表(
ziplist)。
- 它是一个双向链表(
- 优势:
- 兼顾内存与性能:利用 ziplist 连续内存存储的特性,减少了大量指针的内存消耗;同时通过链表分段存储,避免了大列表在内存中频繁搬移数据。
- 压缩机制:quicklist 还支持中间节点的 LZF 压缩,进一步节省内存。
7. 跳表的结构与层数
跳表主要用于 Zset 的大数据存储场景。
- 结构原理:
- 本质是一个多层的有序链表。
- 最底层(第 1 层)包含所有元素,每一层都是下一层的“索引”。
- 越高层的链表元素越少,跨度越大。查找时,从最高层开始向右扫描,如果当前值大于目标值,则下降一层继续查找。这种“二分查找”的思想使得查询时间复杂度为 O(logN)。
- 层数:
- Redis 的跳表默认最高层数是 32 层(源码中定义
ZSKIPLIST_MAXLEVEL 32)。 - 但并不是每个节点都有 32 层,新节点的层数是通过随机算法生成的(1 到 32 之间),以此保证跳表的平衡性。
- Redis 的跳表默认最高层数是 32 层(源码中定义
8. 既然是单线程,为什么还这么快?
这里的“单线程”通常指的是 Redis 处理网络 IO 和命令读写的核心流程是单线程的。
它之所以快,是因为:
- 无锁竞争:多线程并发访问共享资源需要加锁(如 synchronized),这会消耗 CPU 且可能导致线程阻塞。单线程天然线程安全,没有锁开销。
- 无上下文切换:多线程切换需要保存和恢复寄存器、缓存数据,非常消耗资源。单线程没有这个负担。
- CPU 不是瓶颈:Redis 的瓶颈通常在于内存速度或网络带宽,而不是 CPU 计算能力。单线程足以应对网络吞吐量。
注意:Redis 6.0 引入了多线程,但仅限于网络数据的读写(IO 线程),核心命令执行逻辑依然是单线程的,以保证原子性。
9. & 10. & 11. 异步机制、NIO 与共同点
- 9. 它的异步机制叫什么?
它被称为 IO 多路复用(IO Multiplexing),在 Redis 中具体基于 Reactor 模式实现。 - 10. 与 NIO 的相同点?
Java 的 NIO 和 Redis 的 IO 多路复用在概念上是相通的。 - 11. 共同点(都是非阻塞)?
二者的共同点在于都使用了非阻塞 IO 和事件驱动模型:- 多路复用器:Redis 使用
epoll,Java NIO 使用Selector。 - 机制:它们都能在一个线程里同时监听多个客户端连接。当某个连接有数据可读/可写时,操作系统会通知应用程序,然后程序再去处理具体的读写操作。这避免了传统阻塞 IO 中,线程在
read/write时无限期等待。
- 多路复用器:Redis 使用
12. 哨兵机制:主观下线 vs 客观下线
哨兵(Sentinel)是 Redis 的高可用解决方案,它通过分布式的方式监控主从节点。
- 主观下线:
- 定义:某个哨兵实例(A)在配置的时间内(
down-after-milliseconds),没有收到从节点(或主节点)的心跳回复。 - 性质:这是单个哨兵的个人判断,可能是因为网络波动或该哨兵自己网络不通,并不代表节点真的挂了。
- 定义:某个哨兵实例(A)在配置的时间内(
- 客观下线:
- 定义:当足够数量(quorum)的哨兵都认为该主节点主观下线了,哨兵们会通过“投票”机制达成一致,认为该主节点确实挂了。
- 性质:这是集体共识。一旦达成客观下线,哨兵集群就会自动触发故障转移流程(选举新主、通知客户端)。
13. 心跳保持与状态判定
- 怎么保持心跳?
哨兵会以每秒 1 次的频率向所有主从节点发送 PING 命令,同时也向其他哨兵节点发送 PING。 - 什么时候认为挂了?
- 如果节点在
down-after-milliseconds毫秒内没有回复 PONG,哨兵就标记它为主观下线。 - 对于主节点,只有在多数哨兵确认主观下线(即客观下线)后,才会进行故障转移。
- 如果节点在
- 什么时候觉得活着?
- 只要节点在规定时间内回复了 PONG,或者在故障转移后旧主节点恢复了并连接到新主,它就被认为是活的(重新加入集群作为从节点)。
14. & 15. & 16. ThreadLocal 注意事项与内存泄漏
- 14. 使用 ThreadLocal 的注意事项
最核心的注意事项是:务必在使用完后调用remove()方法清除数据。 - 15. 没有 remove 会发生什么?
如果你只set但不remove,数据会一直保留在当前线程的ThreadLocalMap中。- 短期:可能导致线程处理后续任务时读取到错误的旧数据(脏数据)。
- 长期(线程池场景):由于线程池中的线程是复用的,不清理会导致旧数据一直存在,占用内存,最终可能导致 OOM(内存溢出)。
- 16. 为什么 GC 不回收?
虽然 Java 有 GC,但 ThreadLocal 的实现机制导致了强引用链。- 原因:
Thread对象内部有一个ThreadLocalMap,ThreadLocal作为 Key(弱引用),而你存的 Value 是强引用。 - 泄漏点:当
ThreadLocal实例被回收后,ThreadLocalMap中会出现 Key 为 null 的 Entry,但 Value 依然被强引用着,无法被 GC 回收。 - 结论:必须手动调用
remove()来打破这个引用链,否则在长生命周期的线程(如线程池线程)中,这些垃圾数据会越积越多。
- 原因:
17. 你的理解:IOC 是什么?
IOC 的全称是 Inversion of Control(控制反转)。
- 通俗理解:
以前我们写代码,如果 A 类需要用到 B 类的功能,A 类会自己new一个 B 类的对象(主动获取依赖)。
使用 Spring 的 IOC 后,A 类不再自己创建 B,而是告诉 Spring:“我需要 B”。Spring 容器在启动时,会把准备好的 B 对象注入(Injection,即 DI - 依赖注入)到 A 类中。 - 核心思想:
将对象的创建、销毁、依赖关系的维护,从程序员手中反转给 Spring 容器去管理。 - 好处:
- 解耦:代码之间依赖关系清晰,不通过硬编码耦合。
- 易于测试:可以方便地注入 Mock 对象。
- 统一管理:单例、作用域等由容器统一配置。
18. MySQL Undo 日志
与 Redo 日志(重做日志,用于崩溃恢复,保证持久性)不同,Undo 日志主要用于事务的原子性和多版本并发控制。
- 核心作用:
- 事务回滚:当执行 DML 操作(INSERT/UPDATE/DELETE)时,MySQL 会生成对应的 Undo Log。
- 如果事务执行一半想
ROLLBACK,MySQL 就利用 Undo Log 中的旧数据,把记录恢复到事务开始前的状态。
- 如果事务执行一半想
- MVCC(多版本并发控制):当一个事务去读取某行数据时,如果该行正在被另一个事务修改(加锁),当前事务可以通过 Undo Log 找到该行数据的历史版本(快照),从而实现不加锁读取(快照读)。
- 事务回滚:当执行 DML 操作(INSERT/UPDATE/DELETE)时,MySQL 会生成对应的 Undo Log。
- 存储位置:通常存储在 Undo 表空间中。