【常见的面试题总结】
文章目录
- 介绍下mysql的redolog,undolog,binlog
- Mysql 两阶段提交(主要从redolog和undolog)
- mysql的数据刷盘
- mysql刷盘流程
- 介绍下MVCC
- mysql崩溃重启会读那些日志
- 怎么在不改变隔离级别的情况下,解决幻读
- 可重复隔离级别模式下是是怎么解决幻读的
- kafka并发消费。并发度比较高,怎么实现的。
- 介绍下MQ的顺序消费和并发消费及其原理。
- 怎么解决redis的lua脚本超时
- MQ消息堆积怎么解决
- MQ怎么解决消息丢失
- MQ怎么解决消息重复消费
- xxl-job异常场景,执行器返回结果丢失,调度器将任务置为失败,失败任务会进行重试的场景,怎么保证幂等性。
- select 、poll、epoll多路复用机制。
- 如何设计一个基于 NIO 的高并发服务端?
- NIO 中 Selector 为什么能避免阻塞?
- Netty 是什么?有什么特点?
- Netty 是如何基于 Java NIO 实现高并发的?
- Nio怎么实现的同步非阻塞
- volatile 为什么不能保证原子性?
- kafka的并发数据量会比MQ的大这么多?
介绍下mysql的redolog,undolog,binlog
- Redo Log(重做日志)
作用:
保证事务的持久性(Durability),即即使数据库崩溃,也能通过 redo log 恢复已提交事务的数据。
用于 InnoDB 的 物理恢复(恢复数据页的修改)。
特点:
物理日志,记录页的修改(脏页数据);
顺序写,写磁盘效率高;
主要用于崩溃恢复(Crash Recovery),重放 redo log 恢复脏页;
属于 InnoDB 存储引擎内部日志,不对外公开。 - Undo Log(回滚日志)
作用:
实现 事务的原子性(Atomicity) 和 隔离性(Isolation);
支持事务回滚(Rollback),通过 undo log 回滚未提交的修改;
支持 MVCC,提供多版本快照读。
特点:
逻辑日志,记录数据修改前的旧版本(旧值);
每次修改数据前,都会生成 undo log;
用于回滚事务(撤销未提交数据);
支持 MVCC 快照,读事务通过 undo log 访问历史版本数据;
存储在系统表空间或独立 undo 表空间。 - Binlog(二进制日志)
作用:
记录所有修改数据库的操作(DDL、DML),用于 主从复制 和 增量备份恢复;
保障数据在多个 MySQL 实例间的同步一致性。
特点:
逻辑日志,记录 SQL 语句或行修改事件;
由 MySQL Server 层生成,所有存储引擎都使用同一个 binlog;
非事务引擎也有 binlog(比如 MyISAM);
支持事务的 原子提交(结合两阶段提交机制保证 binlog 与 redo log 一致);
可配置三种格式:STATEMENT(语句)、ROW(行)、MIXED(混合)。
Mysql 两阶段提交(主要从redolog和undolog)
MySQL 的两阶段提交机制,通常是指 InnoDB 引擎与 Binlog 写入之间的两阶段提交协议(2PC)。它的主要目的是在 保证事务持久性(持久化到磁盘) 的同时,也要确保主从复制的一致性,防止 “事务提交但 Binlog 没写成功” 或 “Binlog 写了但事务没提交” 这类分布式一致性问题。
一 准备阶段(Prepare)
- InnoDB 写入 Redo Log(prepare 状态)
把事务变更写入 Redo Log(WAL),标记为 prepare,先不提交
保证宕机恢复时能重放事务 - 写入磁盘(fsync)
MySQL Server 写入 Binlog(逻辑日志)
把该事务的逻辑操作写入 Binlog 缓存
并持久化到磁盘(fsync)
二 提交阶段(Commit)
InnoDB 接收到 Binlog 写入成功的信号
然后 InnoDB 执行 Redo Log 的 commit 操作
此时事务正式提交
为什么要分两个阶段?
保证 Binlog 与 Redo Log 的一致性:
两者都写成功 → 正常提交
有一方失败 → 都不提交(例如宕机时,InnoDB 看到的是 prepare 状态,就可以自动回滚)
mysql的数据刷盘
InnoDB 的刷盘机制通过 redo log(WAL)+ Buffer Pool + checkpoint 来保证数据一致性和持久化。事务提交时 redo log 先刷盘,后台线程周期性将 Buffer Pool 中的脏页通过 checkpoint 刷盘。为了防止页写入中断损坏,还使用了 doublewrite buffer。参数如 innodb_flush_log_at_trx_commit 和 innodb_io_capacity 可调节刷盘频率和安全性。
mysql刷盘流程
- 从磁盘加载目标数据页到 Buffer Pool(如果还未在内存中)
- 在 Buffer Pool 中修改数据页内容(产生脏页 Dirty Page)
→ 数据并未立刻写回磁盘。 - 生成 Redo Log 记录修改(逻辑或物理更改)
→ redo log 写入 redo log buffer(内存中的日志缓存)。 - 事务提交时(COMMIT):
刷 redo log 到磁盘(WAL:Write-Ahead Logging),称为 redo log 的刷盘;
此时事务才真正被认为“提交成功”。 - 后台线程异步将脏页刷新到磁盘(刷盘)
→ 这个是刷盘动作,称为 Checkpoint,由后台线程控制。
介绍下MVCC
- 什么是 MVCC?
MVCC 是一种数据库实现 并发控制(Concurrency Control) 的技术,目的是实现:
高并发下数据的读写隔离;
避免加锁带来的阻塞,提升系统吞吐量;
支持数据库的隔离级别,比如 Repeatable Read。 - MySQL InnoDB 中的 MVCC 实现
- 隐藏字段
每条记录有两个隐藏字段:
trx_id:创建这条数据的事务ID;
roll_ptr:指向旧版本数据的指针(undo log 位置)。 - 事务快照
每个事务启动时,会生成一个快照,记录当前活跃的事务ID列表;
读取时,事务会检查某条记录的 trx_id 是否在快照范围内,决定是否可见。 - 读取过程
读取当前版本数据(最新记录);
如果当前版本对该事务不可见,则沿 roll_ptr 找到旧版本,直到找到对事务可见的版本。
- 隐藏字段
mysql崩溃重启会读那些日志
崩溃恢复时,MySQL 会读取 redo log 来重放已提交但未刷盘的事务修改,确保数据不会丢失。同时使用 undo log 回滚未提交事务的变更,保证数据一致性。这是基于 InnoDB 的 WAL(日志先行)和多版本控制机制。
怎么在不改变隔离级别的情况下,解决幻读
不改变隔离级别(比如仍保持 READ COMMITTED)的情况下,解决幻读,主要依靠 显式加锁 来防止“幻影行”插入。以下是常用做法:
- 使用范围锁(间隙锁 Gap Lock)
虽然在 READ COMMITTED 下,InnoDB 默认不加间隙锁,但你可以通过 显式加锁让 InnoDB 对查询范围加锁,从而避免幻读。
SELECT * FROM table WHERE condition FOR UPDATE;
SELECT * FROM table WHERE condition LOCK IN SHARE MODE;
这会对满足条件的行以及范围内的间隙加锁,阻止其他事务在该范围插入新行。
幻读是因为在同一事务中,两次查询条件范围之间插入了新数据;
通过加范围锁,阻止其他事务插入这段范围内的数据;
虽然不改变隔离级别,但显式加锁起到了类似 REPEATABLE READ 的防幻读效果。
可重复隔离级别模式下是是怎么解决幻读的
在 可重复读(REPEATABLE READ)隔离级别 下,MySQL(InnoDB引擎)通过 MVCC + next-key锁(间隙锁 + 行锁的组合) 来避免幻读。
具体机制
- MVCC(多版本并发控制)
保证同一事务内,所有的读操作看到的是同一时间点的快照数据版本;
防止不可重复读,但单靠 MVCC 无法阻止幻读,因为幻读是新插入的“幻影行”。 - Next-Key Lock(前键锁)
InnoDB 对读取的范围使用 next-key锁,即对记录的索引记录加行锁 + 对行与行之间的间隙加间隙锁(gap lock);
这样就锁住了索引记录和它前后的间隙,阻止其他事务在该范围插入新行,防止幻读
例子:对 [a, b] 范围加锁,不允许其他事务插入 a 和 b 之间的新行。
工作流程举例:
事务A执行范围查询 SELECT * FROM orders WHERE id BETWEEN 100 AND 200;
InnoDB 给满足条件的记录加行锁,同时给范围的间隙加间隙锁;
事务B尝试在 id=150 插入新记录时会被阻塞,直到事务A提交;
事务A多次查询同一范围,不会看到新插入的“幻影行”。
kafka并发消费。并发度比较高,怎么实现的。
- 分区机制天然并发
Kafka 的每个 Topic 可以有多个 Partition,每个 Partition 可以独立被不同线程消费:
一个消费者线程最多一个分区;
多个分区就能同时被多个线程并发消费;
分区越多,并发能力越强。
吞吐随分区数线性提升(前提是磁盘、CPU 跟得上)。 - 消费是客户端拉取模型(Pull)
Kafka 的消费者主动去拉取数据:
批量拉取(fetch size 可配置)→ 减少网络往返;
多线程并发拉取多个分区 → 提高并发;
不需要 Broker 主动推送 → Broker 更轻量。
介绍下MQ的顺序消费和并发消费及其原理。
顺序消费,单线程,单队列处理。
并发消费,多队列 , 多线程处理 ,牺牲顺序,换取吞吐性能
怎么解决redis的lua脚本超时
Redis 的 Lua 脚本执行超时,通常是因为脚本执行超过了配置的 lua-time-limit(默认 5000ms),导致 Redis 进入“忙碌(BUSY)状态”,此时 Redis 无法处理其他请求,严重时可能需要强制中断脚本。
- 优化 Lua 脚本逻辑
控制数据量:每次操作控制在 1000 条以内(视情况);
拆分为多个小脚本 + 多次调用;
避免写复杂的排序、分页、嵌套循环;
避免操作跨 slot(在 Redis Cluster 中); - 提高 Lua 执行超时时间(临时手段
- 应急终止脚本:SCRIPT KILL
- 重构为客户端控制逻辑(根本优化)
Lua 脚本只判断库存 & 减库存;
客户端控制分批、分页、数据整合等逻辑;
MQ消息堆积怎么解决
- 加快消费速度
增加消费者数量(扩容)或线程池大小;
提高每条消息处理效率(优化业务逻辑);
使用异步/批量消费,减少 I/O 阻塞;
减少重试逻辑/幂等性逻辑的性能开销;
示例:RocketMQ 可以使用 MessageListenerConcurrently 并发消费 - 使用并行消费(分区/分片)
Kafka / RocketMQ 都支持 Topic 下多个 Partition;
确保消息按队列均匀分布,避免热点 Partition;
每个 Partition 可以被不同消费者线程消费,实现并行; - 加速消息投递与过滤
使用更高效的协议或网络栈;
消费端只订阅需要的 tag,减少无效过滤逻辑;
提前做消息过滤(如 RocketMQ 的 tag filtering / SQL92 表达式); - 调整 Broker 配置
Broker 调整队列数、IO线程池大小;
增加 broker 副本数量,实现负载分担;
增大 maxMessageSize、pullBatchSize、consumeMessageBatchMaxSize(RocketMQ) - 临时限流生产者或丢弃非关键消息
消息堆积太多,优先保核心业务的消息;
可以对低优先级的 producer 限流、丢弃;
RocketMQ 提供延迟消息、死信队列机制; - 顺序消费的情况下,单个消息的报错阻塞后续的消息,导致单个队列的消息堆积。
MQ怎么解决消息丢失
1.生产者同步发送
2.手动的ack应答机制。
3.broker的持久刷设置同步刷盘机制。
MQ怎么解决消息重复消费
redis分布式锁锁住消息的msgId,一般还会设置消息的过期时间。
使用数据库的唯一索引保证数据的唯一性。
xxl-job异常场景,执行器返回结果丢失,调度器将任务置为失败,失败任务会进行重试的场景,怎么保证幂等性。
幂等性的使用一般依赖于mysql的唯一索引或者redis的分布式锁,具体的原因具体的分析。
select 、poll、epoll多路复用机制。
目前支持I/O多路复用的系统调用有select,pselect,poll,epoll。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间
如何设计一个基于 NIO 的高并发服务端?
一个 Acceptor 线程负责接收连接;
多个 Selector 线程负责处理读写;
每个客户端连接用非阻塞 Channel;
使用消息队列 + Reactor 模式分离业务逻辑和 IO 处理。
NIO 中 Selector 为什么能避免阻塞?
因为它使用了非阻塞的 Channel;
且只有当通道就绪时(如可读),Selector 才会通知我们;
调用 read() 不会阻塞线程(无数据时返回 0)。
Netty 是什么?有什么特点?
基于 NIO 的高性能异步事件驱动网络框架;
支持多协议开发(TCP/UDP/HTTP 等);
提供灵活的编解码、事件处理机制;
线程模型和零拷贝设计提升性能。
Netty 是如何基于 Java NIO 实现高并发的?
使用 Java NIO 的 Selector 机制;
自定义事件循环(EventLoopGroup);
一个 EventLoop 绑定多个 Channel,每个 Selector 在单线程中高效轮询;
大量优化了 Selector 的使用(如 epoll bug workaround)
Nio怎么实现的同步非阻塞
- 非阻塞模式(Non-blocking)
在非阻塞模式下,调用 read()、write() 等操作不会阻塞当前线程;
如果数据未准备好,调用立即返回,不会挂起线程;
这样线程可以同时处理多个通道。 - 多路复用(Selector)
Selector 是 NIO 的核心,它监控多个 Channel 的事件(连接、读、写等);
线程调用 selector.select(),等待感兴趣的事件发生;
当事件就绪,线程被唤醒,轮询所有就绪的通道,进行对应处理;
线程不会为每个连接创建一个独立线程,避免大量线程切换开销。
volatile 为什么不能保证原子性?
原子性:一个操作要么全部完成、要么完全不做,中间不能被线程调度器打断。
而volatile并不能保证我们的对象只能被一个线程处理完成,因此不具备原子性。
kafka的并发数据量会比MQ的大这么多?
- Kafka:一个分区对应一个日志文件(Segment Log)
每个 Partition 是一个独立的日志文件,写入和读取相互隔离,且顺序写磁盘;
每个 Partition 有唯一的 Leader 负责写入,多个消费者按分区划分并行消费,天然支持多线程并发消费;
读写操作互不影响,减少锁竞争和上下文切换;
多 Partition 可以分布在不同 Broker,实现跨节点并行扩展。
优势:
并发写入和读取无锁冲突,性能极高;
通过分区天然实现消息的水平并行处理;
消费者组内,分区被多个消费者分担,读写分离,提高吞吐。 - 传统 MQ:所有数据写到一个 CommitLog 文件(或者单一队列)
传统 MQ 多数采用一个统一的存储队列(CommitLog)或内存结构,所有消息写入同一个文件/内存队列;
读写通常竞争同一个队列资源,导致锁竞争和上下文切换;
消费者通常采用 push 模型,消息一旦消费即删除,且消息顺序强依赖单队列结构;
并发度受限于单个队列的锁和 IO 资源瓶颈。
劣势:
多消费者并发竞争单个队列锁,写读冲突频繁,性能瓶颈明显;
单个文件或队列成瓶颈,难以横向扩展;
消费者间的负载均衡、扩展性差。