☰
数据库事务、隔离级别与索引优化:从原理到分布式实战
2026/10/7 3:36:46 网站建设 项目流程

关系型数据库的核心原理,听着像是学院派才会关心的事,但实际排查过线上问题的人都知道,这些基础知识往往是最后救命的工具。最近我又被同事问了一圈:事务注解为什么没回滚?主键索引和唯一索引不都是“唯一”吗?一个慢查询把库拖垮了,用 explain 看半天也不知道索引到底哪里失效。这些问题的根,全都落在事务、隔离级别和存储引擎这三块基石上。这篇文章会直接拆开它们,顺带把分布式事务和 SQL 优化这条关联链路一起讲清楚。不管是日常开发、性能调优还是面试冲刺,都能从这里拿到可复用的判断方法,而不是零散的“八股文”背诵。

1. 事务:ACID 不是口号,是每秒都在发生的博弈

1.1 为什么需要事务

很多人对事务的理解停留在“要么全部成功,要么全部失败”,这句话没错,但远远不够。事务存在的真实原因,是数据库在高并发和故障面前仍然需要保证业务数据的正确性。拿最经典的转账场景来说:A 给 B 转 100 元,第一步扣 A 的余额,第二步加 B 的余额。如果第二步执行到一半数据库突然宕机,没有事务的约束,A 的钱已经少了,B 的钱却没到账,这 100 元就凭空消失了。

事务的四个特性 ACID,就是为了解决这一类问题。原子性保证转账的扣款和入账是一个不可分割的整体;一致性保证转账前后总金额不变,所有约束都成立;隔离性保证两个并发事务互相干扰的程度是可控的;持久性保证事务一旦提交,即使数据库崩溃,数据也不会丢。这四个特性不是并列关系,原子性、隔离性和持久性本质上是手段,最终目标是一致性。这个理解角度,面试时讲出来会明显比死背定义高级。

从工程上看,事务还会直接影响到你的代码设计。比如一个接口里同时写了订单表和库存表的更新,你选择在 DAO 层开启事务还是 Service 层开启事务,锁的持有时间完全不同。Service 层事务会把整个业务逻辑包进去,锁的粒度大、并发能力弱;DAO 层事务则可能把多条 SQL 拆成多个事务,中间一旦异常就出现部分成功。这些都是实际踩过的坑。

1.2 redo log、undo log 与崩溃恢复

理解了 ACID,接着要问:数据库到底靠什么实现事务?答案不是靠一句“事务开启”那么简单的魔法,而是靠 redo log、undo log 和缓冲池组成的完整链路。

redo log(重做日志)负责持久性。InnoDB 的数据默认放在磁盘上,但每次写都直接落盘性能太差,所以先写进内存缓冲池(Buffer Pool),然后异步刷到磁盘。崩溃发生时,内存里的脏页可能还没来得及刷盘,数据就丢了。redo log 的 WAL(Write-Ahead Logging)机制解决了这个问题:在修改数据页之前,先把“我准备把某页某位置改成什么值”这个操作记录到 redo log,并把 redo log 落盘。这样崩溃恢复时,只要重新执行 redo log 就能把数据找回来。这也是为什么磁盘上会出现多个 ib_logfile 文件。

undo log(回滚日志)负责原子性和 MVCC。事务里每一条修改操作,都会被记录成反向操作,比如 update 之前先把旧值写到 undo log。事务回滚时,按照 undo log 反向执行就能恢复原状。更妙的是,undo log 还保留了多版本数据链,读事务可以根据版本链读到历史快照,这是后面要讲的隔离级别的核心支撑。

一个容易被忽略的实践点是参数 innodb_flush_log_at_trx_commit。值为 1 代表每次事务提交都强制刷盘,最安全但性能压力大;值为 2 代表每次提交只把 redo log 写到操作系统缓存,每秒再刷盘,性能好但数据库主机断电可能丢最多一秒的事务;值为 0 是每秒刷一次盘,最不保险。金融类业务必须用 1,普通业务如果对性能敏感且能接受极小概率丢失,可以用 2。这几个配置在面试里也经常被追问,知道为什么比记住数字更重要。

1.3 事务注解的失效现场

Java 开发几乎每天都会碰到 @Transactional。这个注解用起来简单,但失效的坑特别多,很多线上事故其实就是小花样。

第一,Spring 默认只对 RuntimeException 和 Error 回滚,对受检异常(Checked Exception)不回滚。你写了一个 try-catch 把业务异常吞掉,或者抛出自定义的业务异常但没有继承 RuntimeException,事务就不会回滚。这是最常见的一坑。解决办法是 rollbackFor = Exception.class,或者更精准地指定业务异常类。

第二,同类内部的 this 调用会使事务失效。Spring 事务靠 AOP 代理实现,如果你在同一个类里调用另一个标注了 @Transactional 的方法,那是 this 直接调用,没经过代理对象,增强逻辑完全失效。这个很难排查,因为代码看起来明明标了注解。绕开方法要么注入自身对象,要么把事务方法拆到另一个 Service 里。

第三,方法不是 public 的,或者类没被 Spring 管理,事务同样失效。数据库层面的 autocommit 如果被改成了开启状态,也会让事务边界变得模糊。稍不注意,明明开了事务,每条 SQL 却各自提交了。

我自己的习惯是:事务注解只做粗粒度事务控制,并且尽量用 REQUIRED 传播行为覆盖整个 Service 方法;细粒度锁控制用编程式事务 TransactionTemplate,这样边界清楚、异常路径透明。另外,一定要在 Service 层开事务,而不是在 Controller 或 DAO 层开,否则事务作用域过窄或过宽,都会带来维护成本。

2. 隔离级别:并发越高,数据越容易“看花眼”

2.1 四种隔离级别对照表

事务隔离级别解决的是“多个事务同时执行时,到底能看到什么”的问题。标准 SQL 定义了四种级别,从宽松到严格依次是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。

隔离级别脏读不可重复读幻读锁特征
读未提交可能可能可能基本不加读锁
读已提交不会可能可能每次读一致快照
可重复读不会不会可能(InnoDB 下可避免)事务开始时统一快照
串行化不会不会不会完全串行,锁冲突严重

这张表只是标准定义。实际上 MySQL 的 InnoDB 在可重复读级别下,通过 MVCC 和间隙锁把幻读也基本解决了,所以很多人误以为 RR 就是安全的。但如果真的把隔离级别从 RC 调整到 RR,业务并发性能会明显下降,因为间隙锁会增多。

2.2 脏读、不可重复读与幻读的真实业务场景

脏读最容易理解:事务 A 修改了一条数据但还没提交,事务 B 读到了这个未提交的数据。之后 A 回滚了,B 却拿着一个不存在的数据出去了。这在余额查询、报表统计里是致命的。

不可重复读是同一事务内,两次相同的查询得到不同的结果,因为另一个事务在这期间提交了 update。比如一个事务先查订单金额,做一些校验,再查一次发现金额变了,整个业务逻辑就乱套了。这也是把隔离级别从 RU 升到 RC 能解决的事。

幻读更隐蔽:事务 A 按条件查询得到 2 行数据,事务 B 插入了满足同样条件的第 3 行并提交,事务 A 再次查询发现多了一行。注意,update 和 delete 在这类场景下也可能出现“当前读”的幻行问题。InnoDB 的 RR 隔离级别下,普通的 SELECT 走快照读,看不到新插入的行,因此幻读被避免了;但如果走 SELECT FOR UPDATE、UPDATE、DELETE 这种当前读,仍然需要 next-key lock(行锁 + 间隙锁)来锁住范围,防止其他事务插入。

所以,面试谈到 MySQL 的 RR 时,一定要把“快照读避免了幻读,当前读靠 next-key lock 防幻读”这个细节讲清楚。这是拉开差距的关键点。

2.3 MySQL 默认 RR 到底怎么工作的:MVCC

MySQL 的默认隔离级别是 RR,很多人不理解为什么不是 RC。核心原因是历史兼容性,但背后真正支撑它的是 MVCC(多版本并发控制)。

MVCC 简单说就是“读写不冲突”。每行数据在 update 时不会直接覆盖旧值,而是新建一个版本,旧版本保留在 undo log 的版本链里。每个事务在第一次读的时候会生成一个 ReadView,记录当前活跃事务列表。之后读数据时,通过版本链判断哪个版本对当前事务可见——只有创建版本的事务已经提交、并且版本比 ReadView 早的数据才可见。

这样 RR 下同一个事务内,无论 SELECT 执行多少次,看到的都是第一次读时生成的快照,天然避免了不可重复读。而 RC 每次 SELECT 都会生成新的 ReadView,所以第二次读能看到别的已提交事务的修改。

一个常见的困惑是:既然 RR 都保持一致性快照了,为什么还会有死锁?因为 UPDATE 和 SELECT FOR UPDATE 是当前读,它们直接读最新数据并加锁。RR 下为了防幻读,会在范围查询加间隙锁,两个事务各自锁住范围的不同间隙,再互相申请对方范围内的记录锁,就死锁了。这也是很多数据库团队把隔离级别从 RR 降到 RC 的原因,因为 RC 基本只加行锁,间隙锁少,死锁概率大幅下降。

2.4 隔离级别怎么选

选择隔离级别不是越严格越好,关键看业务容忍度。

读未提交基本不会用在正式环境,除非是某些极端的“写多读少且读结果永远允许过期”的统计任务,我一般不建议使用。读已提交适合大多数互联网业务,能保证读到已提交数据,并发好,死锁少。可重复读适合需要事务内多次读取结果必须一致的场景,比如对账、统计、复式计算。串行化几乎只用在资金类、极强一致性以及数据量小、并发极低的场景。

实践中如果要改 MySQL 默认级别,先要确认主从复制模式。在老版本基于 statement 的 binlog 复制下,RC 会因为无法像 RR 那样保证日志执行顺序一致而导致主从数据不一致,这可能是 MySQL 一直保留 RR 的官方原因之一。到了基于 row 的 binlog(现在默认就是 row),RC 基本没问题了。所以不要裸改,要做完整测试。

3. 存储引擎:InnoDB 凭什么能“通吃”

3.1 先搞清楚存储引擎是什么

存储引擎是 MySQL 中负责“数据怎么存、怎么取、怎么保证可靠性”的组件。表级别选择存储引擎,也是 MySQL 和很多商业数据库最大的差异之一。你把不同的引擎理解为不同的“文件柜”:InnoDB 是一个带抽屉锁、带日志、自带崩溃恢复功能的保险柜;MyISAM 是一个轻便但容易损坏的文件架;MEMORY 则是一个断电就清空的临时板子。

查看当前默认引擎和表引擎的命令很简单:

SHOW VARIABLES LIKE 'default_storage_engine'; SHOW TABLE STATUS LIKE 'your_table'\G;

日常开发基本只关心 InnoDB,但面试题如果没有明确限定,“MySQL 的存储引擎有哪些”这种问题就需要把 InnoDB、MyISAM、MEMORY,以及 NDB、Archive 这些也提一句,才算完整。

3.2 InnoDB 的底层设计

InnoDB 能成为默认引擎,核心理由有三个:支持事务、支持崩溃恢复、支持行级锁。但它的底层设计还有更多值得深挖的东西。

最关键的是聚簇索引。InnoDB 的表数据本身就是按主键索引组织的,主键索引的叶子节点直接存整行数据,所以查询走主键索引时一次索引查找就能拿回全部列。二级索引(普通索引)的叶子节点保存的是主键值,所以走二级索引查数据需要先找到主键,再回主键索引取整行,这个过程叫回表。这也是为什么主键设计得越小越好,因为所有二级索引都要冗余主键值,主键太长会让每个索引叶子节点变大,占用更多磁盘和内存。

缓冲池和变更缓冲也很重要。写操作不会立刻改磁盘,而是先改 Buffer Pool 里的页,再异步刷盘。对于二级索引上的随机插入操作,InnoDB 不会每次更新索引页都去写一次磁盘,而是先记录在 Change Buffer 中,等页被读到或后台线程聚合后合并。这个机制的收益非常大,能显著减少随机写放大,特别是在“插入但长期不查”的二级索引场景下。

奔溃恢复依赖 redo log,事务回滚和多版本依赖 undo log。锁机制则包括记录锁、间隙锁、next-key lock、意向锁。这些机制共同保证了在高并发写入下,InnoDB 依然能保持“不出错、可恢复、可并发”。

3.3 MyISAM、MEMORY 等引擎一览

MyISAM 在旧时代很流行,典型特征是不支持事务、不支持外键,锁粒度是表锁,写入并发差,但读性能曾经被认为快。实际上它也有一个优点:支持 FULLTEXT 全文索引,InnoDB 直到 MySQL 5.6 才开始支持全文索引。

MyISAM 的崩溃恢复能力很差,因为写操作只更新内存数据并定期刷盘,没有 redo log 机制,一旦机器断电表很容易损坏。这也是我在生产环境从不建议保留 MyISAM 表的原因,就算读多写少,也完全可以用 InnoDB 配合只读实例来扛。

MEMORY 引擎把表存在内存里,查询速度极快,但服务器重启数据全没,通常用来做临时中间表,不适合长期数据。Archive 引擎只支持插入和查询,压缩率高,适合日志归档。NDB 是集群引擎,配置复杂,一般不在常规业务里使用。

一个实用技巧:验证一个表是不是 InnoDB,直接 show table status。如果 engine 不是 InnoDB,优先考虑迁移。迁移可以用一条语句:

ALTER TABLE old_table ENGINE=InnoDB;

但注意大表会锁表,过程较长,建议在低峰期执行,或者通过新建表 + 拷贝的方式平滑切换。

3.4 选型与迁移建议

特性InnoDBMyISAMMEMORY
事务支持支持不支持不支持
锁粒度行锁 + 间隙锁表锁表锁
崩溃恢复redo log 可靠恢复容易损坏重启即丢失
外键支持不支持不支持
MVCC支持不支持不支持
全文索引5.6 起支持支持不支持
适合场景绝大多数 OLTP只读、表小、无并发写临时表、缓存

选型时,我基本只用 InnoDB,唯一的例外是如果线上还有全文检索需求且不想引入 Elasticsearch,可以临时用 MyISAM 建一个只读副本,或者改用 InnoDB 的 FULLTEXT 特性。从运维角度看,引擎统一还能减少备份恢复工具链的差异,省不少事。

4. 索引原理与 SQL 优化:那些面试必问的“送命题”

4.1 主键索引和唯一索引的区别

“主键索引和唯一索引不都是唯一吗?”这是面试高频题,也是很多开发容易混淆的点。

首先,主键索引是一种特殊的唯一索引,但它在 InnoDB 里的地位完全不同。主键是聚簇索引的锚点,整张表的数据就是按主键排序存储的。没有显式主键时,InnoDB 会选一个非空的唯一索引作为隐式主键;如果都没有,会生成一个隐藏的 rowid。所以从存储结构上看,主键索引叶子节点存的是整行数据,唯一索引叶子节点存的是主键值。

其次,唯一索引允许存在多个 NULL 值,因为 NULL 在 MySQL 里代表“未知”,多个未知并不违反唯一性。主键索引不允许 NULL,因为它必须能标识每一行。

还有一点需要理解:唯一索引还承担着约束职责。插入数据时,唯一索引需要检查冲突,因此会有额外的唯一性校验开销。主键索引的这种检查同样有,但更多是存储层面的必选项。

实践建议是:主键尽量用自增 ID 或有序的 UUID。自增主键写入时是纯顺序插入,不会频繁触发页分裂;随机 UUID 作为主键会导致页分裂和碎片,性能和空间都很差。如果一定要用 UUID,可以改成类似 UUID 转有序字符串,或者使用雪花算法生成的分布式 ID。

4.2 哪些场景会导致索引失效

索引失效是慢 SQL 的重灾区。我这里列出的每一个场景,都是我实际在慢查询日志里看到过的。

一、对索引列使用函数或计算。比如 where YEAR(create_time)=2024,会让 create_time 索引失效,因为要计算完才能比较。改成 create_time >= '2024-01-01' AND create_time < '2025-01-01',就能走索引。

二、隐式类型转换。字符串字段 where mobile=138xxxxxxx,如果 mobile 是 varchar,但参数是数字,MySQL 会先把字段转换成数字再比较,索引就失效。解决办法是参数也写成字符串。

三、LIKE 前置模糊。where name like '%张' 没法走索引,因为前缀未知。如果业务必须用这类模糊搜索,优先考虑全文索引或外部搜索引擎。

四、联合索引不满足最左前缀。比如联合索引 (a,b,c),直接查 b 或 c 就无法使用该索引。这是通用规则,必须遵守。这里也常考:如果查询条件是 where a between ... and ... and b=?,那 b 上索引到底能不能用?答案是 a 范围右侧的列无法完全利用,需要结合执行计划具体判断。

五、OR 连接时,如果其中一个条件没有索引,整个表达式可能全表扫描。优先使用 UNION 或为 OR 涉及的每个列分别建索引。全域优化器也可能选择扫描,所以尽量写成 UNION 或增加选择性。

六、对索引列做负数条件。!=、NOT IN、NOT LIKE 往往会让优化器放弃索引。但这不绝对,覆盖索引时可以走索引扫描。关键是看 selectivity 和 cost。

还有两个容易被误解的点:is null 不一定失效,在 MySQL 8.0 里,is null 可以走索引,但允许 NULL 的列上看重复率;索引列参与运算比如 id+1=5,也会失效,因为计算后索引键不再匹配原始索引。

修复思路是打开 EXPLAIN,看 key 列是否为 NULL,看 type 是否从 ref 变成了 ALL。一旦发现索引失效,优先重写 SQL,而不是盲目加索引。

4.3 EXPLAIN 优化实操

执行计划是 SQL 优化的第一工具。面试官问“说一些 SQL 上面的优化”,不是要让背几条口诀,而是想听你怎么定位问题、怎么分析。最好的回答方式就是讲 EXPLAIN。

EXPLAIN SELECT order_id, amount FROM orders WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 10;

重点看四列:

  • type:常见从好到坏是 system > const > eq_ref > ref > range > index > ALL。出现 ALL 就要警惕全表扫描。
  • key:实际使用的索引名,为 NULL 表示没走索引。
  • rows:预估扫描行数,越小越好。
  • Extra:Using index 表示覆盖索引,Using filesort 表示排序没有用到索引,Using temporary 表示用了临时表,需要重点关注。

一个经典慢 SQL 是深分页:offset 很大时,MySQL 要扫描并丢弃前面的行。比如 limit 100000,20,优化方式是把 offset 改成基于主键定位:

SELECT * FROM orders WHERE order_id > last_order_id ORDER BY order_id LIMIT 20;

这样每次只扫 20 条。还有尽量不要 SELECT *,只取需要的列,这样更可能使用覆盖索引,减少回表。批量插入数据时,用一条 INSERT 语句插入多行,或者用 LOAD DATA,比循环单条 INSERT 快得多。排序时,如果 ORDER BY 的字段不在索引里,就会 filesort,避免办法是让排序字段成为联合索引的一部分,或者降低返回数据量。

SQL 优化永远要结合具体数据分布。数据量千万级和十万级的优化路径完全不同,不同索引选择性差异也很大。所以一定要先 EXPLAIN,再手工验证,这才是面试官想听到的“理性框架”。

5. 分布式事务:单库事务解决不了的问题,怎么解

5.1 跨库事务为什么这么难

当业务拆分为微服务后,订单服务维护订单表,库存服务维护库存表,两个服务通常使用不同的数据库。单库事务的 ACID 只能保证在一个数据库实例内,跨库之后不再有统一的 redo log 和全局事务管理器,所以经典事务直接失效。

拿下单流程举例:先扣库存,再创建订单。如果扣库存成功,创建订单失败,但是两个操作各自发生在自己的本地事务里,数据库层面并不感知对方是否成功。这时需要分布式事务来保证最终一致性。但根据 CAP,在网络分区时,分布式系统必须在一致性和可用性之间权衡。强一致的分布式事务代价非常高,互联网业务普遍采用“最终一致性 + 补偿”的思路。

这里面还有一个容易混淆的概念:“分布式事务一致性”和“单库事务隔离级别”不是一回事。分布式事务更关心的是跨服务、跨库状态怎么对齐,通常讨论最终一致性,而隔离级别讨论的是在一个数据库内并发事务的可见性。

5.2 常见方案:2PC、TCC、消息事务、SAGA

分布式的第一反应是 2PC(两阶段提交)。它有一个协调者,第一阶段问所有参与者“能提交吗”,第二阶段广播“提交”或“回滚”。问题是协调者单点、同步阻塞、参与者资源锁住时间长,性能很差,不适合高并发场景。真正在公司里自研 2PC 的很少,更多是用 Seata 这种 AT 模式,本质也是 2PC 的改进版,但侵入小。

TCC 是业务层面的补偿方案:Try 阶段预留资源,Confirm 阶段确认执行,Cancel 阶段补偿回滚。比如扣库存前先检查预扣,然后锁定库存,确认时扣减,取消时释放。TCC 的优点是性能好、不长期占用资源,缺点是需要为每个业务写三套逻辑,开发成本高。

可靠消息事务是目前最常用的“最终一致性”方案,比如 RocketMQ 事务消息。它的思路是把“业务操作”和“发送消息”放到同一个本地事务里。具体流程是:发送 half message(半消息),执行本地事务,本地事务成功后向 Broker 发送 commit,Broker 才会真正让消费者看到消息;本地事务失败则 rollback,消息删除。如果半消息一直没确认,Broker 会回查本地事务状态,做到最终一致。这样订单服务只要保证本地订单创建和消息发送同成功、同失败,库存服务消费消息扣减库存即可。缺点是有消息延迟,不适合要求实时一致的系统。

SAGA 是一个长事务拆成一串本地事务,每个步骤记录正反操作,按顺序执行,失败则反向补偿。适合流程很多、节点多样的场景,比如旅游预订。它没有全局锁,性能好,但补偿设计复杂,业务语义散落在代码里。

5.3 订单与库存场景拆解

我用一个最典型的“订单与库存分布式事务”给你捋一遍:

假设下单接口先调订单服务创建订单,再调库存服务扣库存。最直接的做法是订单本地事务执行 insert order 和 send MQ,库存服务消费 MQ 执行扣库存。这里如果扣库存失败,订单已经创建了,怎么处理?方案一是订单服务发一条补偿消息,系统对订单做“已取消”处理,同时库存回补;方案二是库存服务先做预扣,如果库存不足,直接回查订单服务取消订单。

实际生产上,我会优先选择 RocketMQ 事务消息,因为对业务入侵最小,最终一致性目标清晰。流程是:

  1. 订单服务发送事务消息(半消息)。
  2. 执行本地事务:创建订单,状态置为待支付。
  3. 向 Broker 提交 commit。
  4. 库存服务消费消息,执行扣库存。
  5. 如果库存扣减失败,返回消费失败,消息会被重试或进入死信队列,需要报警和人工处理。

这套方案里,最关键的不是消息中间件,而是“消息状态”和“业务状态”的对齐。比如本地事务成功但 commit 消息失败,Broker 会通过事务回查接口确认订单是否存在,以此决定消息是否放行。回查接口必须幂等,否则会造成重复扣减。

还有一个容易忽略的问题:消费端一定要做幂等。消息可能被重复投递,库存扣减必须能重入,比如用订单号加唯一约束,或者用 Redis 记录已完成消息 ID。不做幂等,最终一致性的链条迟早会在某一次重试时崩掉。

6. 面试题速查表:先背下来,再讲出原理

6.1 高频问题清单与回答骨架

很多后台开发准备 Java 技术面试时,都会遇到事务、存储引擎、索引堆一起的连环问。我整理一张速查表,每个问题给出回答骨架,方便快速过。

问题回答骨架
MySQL 的存储引擎有哪些?InnoDB 支持事务、行锁、崩溃恢复,默认;MyISAM 不支持事务、表锁、无恢复;MEMORY 内存表;根据业务选型。
事务隔离级别有哪些?RU、RC、RR、Serializable;解释脏读、不可重复读、幻读;MySQL InnoDB 默认 RR,MVCC 实现 RR 快照读。
事务注解失效的场景?默认只回滚 RuntimeException;类内部 this 调用;非 public 方法;异常被吞掉;传播行为配置错误。
主键索引和唯一索引的区别?主键非空、聚簇索引锚点、叶子存整行;唯一索引允许 NULL、普通二级索引、叶子存主键。
哪些场景会导致索引失效?隐式类型转换;函数计算;LIKE 前置模糊;联合索引不满足最左前缀;OR 含非索引列;不等值条件。
分布式事务一致性怎么答?单库事务跨库失效;CAP 取舍;2PC 强一致但性能差;TCC 侵入;事务消息最终一致性;SAGA 长流程补偿。
说一些 SQL 上面的优化?EXPLAIN 定位慢 SQL;避免全表扫描;覆盖索引、避免深分页、减少回表;合理使用联合索引;批量插入代替循环插入。

这张表是骨架,面试时不能只背一句结论。每个问题都要带一个真实场景做佐证,比如“我上次在 XX 表发现主键用随机 UUID 导致页分裂,后来改成有序 ID,插入性能提了 X 倍”,这种细节瞬间拉开与背书党的差距。

6.2 怎么讲才不像背书

我发现很多候选人不是不懂,而是讲得太“平”,全是定义没有推导。面试官问隔离级别,你说“可重复读是事务开始后多次读取相同结果”,不会加分。但如果你从 MVCC 的 ReadView、undo log 版本链、当前读和快照读的区分讲起,再举例说明 RR 下为什么 SELECT 不会幻读但 UPDATE 可能锁范围,面试官马上就知道你是真懂。

回答面试题的正确顺序是:结论优先 → 原理展开 → 场景举例 → 坑点提醒。比如“事务注解为什么会失效”,先说“Spring 通过 AOP 代理实现,只有外部调用才能被拦截”,再说“受检异常默认不回滚”,给一个 try-catch 吞异常的例子,最后提一下同类调用问题。每一点都落到实际代码上,就很自然。

如果被追问“分布式事务怎么实现”,也不要在一开始硬凹 2PC。先判断系统是强一致还是最终一致,再给出方案对比,最后落到订单与库存这个具体场景,讲消息事务的半消息和回查机制。这样整个回答逻辑是活的,不是在背模板。

分布式事务面试中的最后一个提醒

关于分布式事务,我一直有一个体会:能用消息事务就别上 TCC,能用 TCC 就别上自研 2PC。越重的方案,开发和运维成本越大。一个秒杀场景要求库存实时性高,那就不能用消息延迟最终一致,反而应该在库存服务本地做预扣,然后通过 TCC 或 Seata 管理跨服务事务;一个普通下单场景能容忍几秒延迟,消息事务就够了。关键是先把业务容忍度搞清楚,再去选技术。

而在单机数据库这一侧,我的经验是:把所有表统一用 InnoDB,把事务隔离级别理解清楚,把索引设计前置到表结构评审阶段,绝大多数慢 SQL 和一致性事故都可以提前规避。那些看起来很高深的技术名词,剥开之后其实都是围绕“怎么不让数据错、怎么不让系统崩、怎么让并发尽量高”这三个问题展开。方向对了,细节学起来就不会散。

最后再分享一个从实际故障里练出来的小技巧:每次写事务相关代码,强制自己写清楚事务边界、异常路径和幂等策略。线上问题往往不是某个原理没学过,而是边界定义模糊、异常处理随意。把这些基础动作做到位,比收藏任何一份“数据库面试题总结”都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询