Skip to content

这里为你整理了针对你提出的 MySQL、Java、Kafka 和 Redis 的详细技术解答。

🛠️ MySQL 相关

1. ACID 特性分别怎么实现?

事务的 ACID 特性是数据库稳定性的基石,其底层实现主要依赖于多种日志和锁机制:

  • 原子性 (Atomicity):通过 **Undo Log(回滚日志)**实现。如果事务执行过程中发生错误或系统崩溃,可以通过 Undo Log 将数据回滚到事务开始前的状态,保证操作“要么全做,要么全不做”。
  • 一致性 (Consistency):由数据库的约束(如主键、外键、唯一键、数据类型)以及原子性、隔离性、持久性共同保障。如果事务违反了这些约束,就会被回滚,从而维持数据库从一个一致状态变换到另一个一致状态。
  • 隔离性 (Isolation):通过 锁机制(如行锁、表锁)和 **MVCC(多版本并发控制)**来实现。它控制了并发事务之间的可见性,避免脏读、不可重复读和幻读。
  • 持久性 (Durability):通过 **Redo Log(重做日志)**实现。当数据被修改后,Redo Log 会先记录下来,即使系统崩溃,重启后也能通过重放 Redo Log 恢复数据,保证提交的事务结果是永久的。

2. 为什么要用 B+ 树?

InnoDB 存储引擎选择 B+ 树主要为了解决磁盘 I/O 和范围查询的效率问题:

  • 降低树高(减少 I/O):B+ 树是多路平衡查找树,相比于二叉树,它的树高更低。数据库读取数据以页(Page,默认 16KB)为单位,树高越低,查询时需要发生的磁盘 I/O 次数就越少。
  • 范围查询高效:B+ 树的叶子节点之间通过双向链表连接。当需要查询某个范围的数据(如 WHERE id BETWEEN 10 AND 100)时,只需要定位到起始和结束节点,通过链表指针顺序遍历即可,效率极高。
  • 稳定性:所有查询最终都会走到叶子节点,查询性能稳定。

3. LIMIT 分页查询为什么后面页面的查询会很慢?

分页查询通常写成 LIMIT offset, size

  • 原因:当翻到很后面的页面时(例如 LIMIT 100000, 10),数据库并不是直接跳过 100000 行去取 10 行。它需要先扫描并读取前 100010 行数据,然后丢弃前 100000 行,只返回最后 10 行。
  • 后果:随着 offset 值越来越大,扫描的数据量呈指数级增长,导致查询速度越来越慢。

4. 索引失效有哪些场景?

  • 模糊匹配:使用左模糊或全模糊,如 LIKE '%abc'LIKE '%abc%'
  • 函数/表达式操作:在索引列上进行计算、函数调用或类型转换,如 WHERE YEAR(create_time) = 2023WHERE age + 1 = 10
  • 违反最左匹配原则:在联合索引 (a, b, c) 中,如果查询条件没有包含最左边的 a,索引通常会失效。
  • 使用 OR 连接:如果 WHERE 条件中使用了 OR,且 OR 两边的字段并非都建立了索引。
  • 数据量太少/选择性低:如果查询优化器发现全表扫描比走索引更快(例如查询性别为“男”的数据,数据分布极不均匀),可能会放弃使用索引。

☕ Java 相关

1. 两个 new Integer(100)是否 ==

  • 结果不相等(false)
  • 原因new Integer(100) 会在堆中创建新的对象。== 比较的是对象的内存地址,两个 new 出来的对象地址肯定不同。
  • 注意:如果是 Integer a = 100; Integer b = 100;,结果则是 true,因为这是自动装箱,JVM 会缓存 -128 到 127 之间的值,复用同一个对象;但超出这个范围(如 128)也会返回 false

2. 用 Spring 的时候怎么避免循环依赖?

Spring 默认通过三级缓存机制解决单例 Bean 的循环依赖(如 A 依赖 B,B 依赖 A)。

  • 解决方法:通常不需要手动避免,Spring 的 @Autowired 在单例模式下默认可以解决构造器或字段注入的循环依赖。
  • 避免手段:如果想从设计上规避,可以使用 @Lazy 注解进行延迟加载,或者重构代码,引入中间类来解耦,或者使用 setter 方法代替构造器注入。

3. 类型擦除问题,ArrayList 可以存入一个 String 吗?

  • 答案可以(通过反射等手段)。
  • 解释:Java 的泛型是通过类型擦除实现的。在编译期,泛型信息会被擦除(例如 ArrayList<Integer> 变回 ArrayList),只保留原始类型并在必要时插入强制类型转换。因此,在运行时,JVM 并不知道集合原本应该是 Integer,你可以通过反射绕过编译器检查向其中添加 String,但这在取值时会抛出 ClassCastException

4. Java 序列化生成的 uuid 有什么用?

这里的 UUID 通常指 serialVersionUID

  • 作用:它是一个版本号。在对象反序列化时,JVM 会校验字节流中的 serialVersionUID 是否与当前类的 serialVersionUID 一致。
  • 目的:保证在序列化结构发生变化(如增加字段、修改类名)时,能够识别出版本不兼容,从而避免反序列化失败或数据错乱。

5. Java 有哪些锁?讲讲 AQS

  • Java 锁分类
    • synchronized:JVM 内置锁,可重入、互斥。
    • JUC 包下的锁:如 ReentrantLock(可重入锁)、ReadWriteLock(读写锁)、StampedLock 等。
  • AQS (AbstractQueuedSynchronizer)
    • 定义:它是 JUC(java.util.concurrent)包中锁的底层核心框架。
    • 原理:AQS 使用一个 volatile int state 变量来表示同步状态,并维护一个双向队列(CLH 队列)来管理等待线程。ReentrantLockCountDownLatchSemaphore 等都是基于 AQS 实现的。它通过 CAS 操作来修改 state,保证了状态变更的原子性。

6. synchronized 锁的原理

  • 核心:每个 Java 对象都关联着一个监视器(Monitor)。
  • 过程:当线程执行 monitorenter(进入同步块)时,会尝试获取对象的 Monitor 锁。如果 Monitor 的计数器为 0,线程获取锁并将其加 1;如果线程已经拥有该锁,计数器再次加 1(可重入);其他线程则被阻塞。当执行 monitorexit(退出同步块)时,计数器减 1,减为 0 时释放锁。
  • 优化:JVM 对 synchronized 进行了大量优化,包括偏向锁、轻量级锁(自旋锁)和重量级锁的升级过程,以减少线程阻塞带来的开销。

7. CAS 操作怎么避免 ABA 问题?

  • 什么是 ABA:线程 1 读取了值 A,线程 2 将 A 改为 B,又改回 A。线程 1 再次 CAS 时发现还是 A,于是认为没有变化,成功修改。但实际上中间发生了变化。
  • 解决方案
    • 版本号机制:使用 AtomicStampedReference,不仅记录值,还记录一个版本号(Stamp)。每次修改版本号加 1,即使值变回 A,版本号也不同,从而能识别出变化。
    • 时间戳:类似版本号,利用时间戳作为标识。

8. volatile 关键字有什么用?

  • 作用:它是 JVM 提供的轻量级同步机制,主要有两大作用:
    1. 保证可见性:当一个线程修改了 volatile 变量的值,新值会立即刷新回主内存,其他线程读取该变量时会直接从主内存读取,确保看到最新的值。
    2. 禁止指令重排序:防止编译器和处理器为了优化性能而改变代码的执行顺序(利用内存屏障实现)。
  • 注意volatile 不能保证原子性(例如 i++ 这种复合操作它无法保证线程安全)。

🚀 Kafka 相关

1. kafka 为什么并发量最高?

  • 顺序读写:Kafka 将消息追加到日志文件的末尾,利用了磁盘的顺序 I/O,速度远快于随机 I/O。
  • 零拷贝 (Zero-Copy):利用操作系统的 sendfile 机制,数据可以直接从 PageCache 传输到网络 Socket,减少了用户态和内核态之间的上下文切换和数据拷贝。
  • 分区 (Partition):一个 Topic 可以分为多个 Partition,分布在不同的 Broker 上,实现了水平扩展和真正的并行处理。
  • 批量处理与压缩:Producer 和 Consumer 都支持批量发送和拉取数据,并支持 GZIP、Snappy 等压缩算法,减少了网络传输开销。

2. kafka 能保证顺序读顺序写吗?

  • 顺序写。Producer 写入消息时,是追加到 Partition 的末尾,保证了物理上的顺序写。
  • 顺序读在单个 Partition 内能保证。Consumer 从 Partition 拉取消息是按顺序的。
  • 全局顺序不能。如果一个 Topic 有多个 Partition,虽然单个 Partition 有序,但不同 Partition 之间是并行的,无法保证全局的严格顺序。

🧩 Redis 相关

1. redis 的 String 结构和 java 的 String 有什么不同?

  • Java String:是不可变的字符序列(UTF-16 编码),一旦创建就不能修改。修改字符串实际上是创建新对象。
  • Redis String:底层是 SDS (Simple Dynamic String)。它是可变的,支持动态扩容。SDS 记录了当前长度(len)和剩余空间(free),获取字符串长度的时间复杂度是 O(1),且二进制安全(可以存储图片、序列化对象等任意数据)。

2. redis 的 ZSET 了解吗?底层是什么结构?

  • ZSET (Sorted Set):有序集合,每个元素关联一个分数(score),通过分数对元素进行排序。
  • 底层结构:为了兼顾查询效率和排序效率,ZSET 采用了双存储结构
    • 跳跃表 (SkipList):用于根据 score 进行快速范围查找和排序(核心结构)。
    • 哈希表 (HashTable):用于根据 member 快速查找对应的 score(保证 O(1) 的查询性能)。

3. redis 为什么这么快,为什么不使用多线程?

  • 快的原因
    1. 内存操作:数据存储在内存中,读写速度极快。
    2. 单线程模型:避免了多线程的上下文切换和锁竞争的开销。
    3. I/O 多路复用:利用 epoll/kqueue 等机制,单个线程可以同时处理成千上万个连接的 I/O 事件。
  • 为什么不使用多线程:主要是为了简单和避免锁竞争。Redis 的性能瓶颈通常不在 CPU,而是在内存和网络带宽。单线程模型使得逻辑简单,没有并发访问数据的问题。(注:Redis 6.0 引入了多线程 I/O 来处理网络读写,但核心的命令执行依然是单线程的)

4. 了解 Redis Cluster 吗?多加几个实例数据要怎么迁移?

  • Redis Cluster:是 Redis 的分布式解决方案。它将数据自动切分到 16384 个槽(slot)中,不同的节点负责处理一部分槽。
  • 数据迁移:当新增实例(节点)时,需要将原本属于其他节点的一部分槽(slot)指派给新节点。
    • 过程:系统会将这些槽包含的键值对从旧节点(源节点)迁移到新节点(目标节点)。
    • 机制:Redis 提供了 CLUSTER MEETCLUSTER ADDSLOTS 以及迁移命令(如 MIGRATE),或者使用 redis-cli --cluster reshard 命令自动完成槽的重新分片和数据迁移。

5. redis 某个 key 过期了,删除策略有哪些?

Redis 为了平衡内存和 CPU 性能,采用了两种策略的结合:

  • 惰性删除 (Lazy Expiration):当客户端尝试访问一个 key 时,Redis 才会检查该 key 是否已过期。如果过期,则删除并返回空。这节约了 CPU,但可能会导致过期但未被访问的 key 长期占用内存。