最近啊,我准备跳槽了!

是的,一个在公司搬了五年砖的我,终于在上个月鼓起勇气投了几家心仪的公司,走上了社招之路。期间当然被“摧残”了一遍又一遍,什么MySQL索引原理、JVM内存模型、Netty工作原理……但让我印象最深刻的,还是某家互联网大厂面试官的一道看似简单实则“暗藏杀机”的题:

“请你讲讲 Spring 的事务隔离级别,顺便说说你对数据库事务一致性的理解。”

我心想:这不就ACID嘛!但刚开口,我就意识到问题不简单。

这不,今天就来和大家唠唠这个问题,咱也不整啥高深理论,用我亲身经历的项目故事,手把手带你了解Spring的事务隔离级别,看到最后你能秒杀面试官。

咱得从 ACID 开始说起!

“事务”是什么?

我问你:如果你去ATM取钱,钱扣了但没吐出来,你心里烦不烦?

数据库事务就是为了避免这种“尴尬局面”,保证数据的一致性,它必须满足四大特性,也就是经典的 ACID

  • 原子性(Atomicity):事务中的操作要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成后,数据要从一个一致状态变为另一个一致状态。
  • 隔离性(Isolation):多个事务并发执行时,相互不干扰。
  • 持久性(Durability):事务提交后,其结果会永久保存在数据库中。

今天我们的主角就是第三个:隔离性

隔离性是怎么“出事”的?

事务不隔离,会怎么样?咱举个例子你就懂了。

项目背景介绍

我在公司维护一个库存系统,两个功能常一起操作数据库:

  • A功能:用户下单,扣库存;
  • B功能:库存管理员后台查询库存;

现在,假如A和B是两个并发事务,那我们就可能遇到以下经典问题:

1、脏读(Dirty Read)

B事务读到了A事务还未提交的数据

例子:

  • A开始事务,扣库存 -1,还没提交;
  • B事务读取库存,看到已经是-1;
  • A事务回滚,库存恢复;
  • B读到的是“根本不存在”的数据 → 脏读!

2、不可重复读(Non-repeatable Read)

B事务在一个事务中两次读取同一数据,值却不同

例子:

  • B开始事务,读取库存为100;
  • A更新库存为50并提交;
  • B再次读取库存,发现是50 → 不可重复读!

3、幻读(Phantom Read)

B事务两次读取满足条件的记录数不同,因为有新数据插入或删除。

例子:

  • B查“库存小于100”的记录,共10条;
  • A插入一条“库存为80”的记录;
  • B再次查询,发现多了一条 → 幻读!

数据库怎么防?四大隔离级别来了!

为了解决这些问题,数据库提供了四种隔离级别,它们是:

Java社招面试题:说一下 Spring 的事务隔离?我这次靠这个问题反客为主!_数据

越往下越严格,但性能代价也越大

注意:MySQL InnoDB 默认是 REPEATABLE READ,而 Oracle 默认是 READ COMMITTED

Spring 是怎么支持事务隔离的?

故事继续,我在项目里用到了 Spring 的声明式事务,也就是你经常写的:

Java社招面试题:说一下 Spring 的事务隔离?我这次靠这个问题反客为主!_数据_02

这个注解非常强大!可以指定:

  • 传播行为(Propagation)
  • 事务隔离级别(isolation)
  • 回滚规则
  • 只读标志

今天我们只说隔离级别,Spring 提供如下常量:

Java社招面试题:说一下 Spring 的事务隔离?我这次靠这个问题反客为主!_数据_03

用法也很简单,比如我们要设置成 READ_COMMITTED:

Java社招面试题:说一下 Spring 的事务隔离?我这次靠这个问题反客为主!_数据_04

项目实战演练:用隔离级别解决“库存错乱”

我在一个促销项目中遇到过一个“库存显示错乱”的bug,用户下单后库存还没变,后台却显示库存已经减少,最后发现是 脏读 问题!
原来,我们的后台查询用了默认隔离级别,而MySQL默认是REPEATABLE READ,但有些同事临时建的表用的是 MyISAM 存储引擎(不支持事务!),最终导致了读未提交数据。

解决方案:

  • 强制所有表使用 InnoDB;
  • 对查询方法标注 @Transactional(isolation = Isolation.READ_COMMITTED),防止脏读。

这个bug查了一周,后端前端互甩锅,结果只是个“隔离级别”没配对的问题……

Spring 事务背后的原理一丢丢

面试官常会追问:“那你知道Spring是怎么实现这些的吗?”

这时候,来点底层“调味料”:

  • Spring事务是通过 AOP(面向切面编程) 实现的,核心是 TransactionInterceptor
  • 它通过 动态代理,在方法前后加入“开启事务”、“提交/回滚事务”的逻辑。
  • 隔离级别其实是通过 Connection 接口的 setTransactionIsolation() 方法设置给底层数据库连接的。

例如:

Java社招面试题:说一下 Spring 的事务隔离?我这次靠这个问题反客为主!_数据_05

事务失效的坑你踩过没?

讲完原理,不讲“坑”太抽象。Spring事务最容易“失效”的地方是这几个:

  • 自己调用自己:A方法里调用了B(同类),B加了@Transactional,但不会生效!因为没经过代理。
  • 用了private方法:Spring不会代理private方法,加了@Transactional也无效。
  • 异常被捕获了:抛异常才能触发回滚,try-catch后你要手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  • 多线程中事务失效:事务只在线程上下文中有效,异步线程需要额外配置事务传播。

一句话总结各个隔离级别适合场景

最后,如果面试官让你总结下每个隔离级别的使用场景,你可以这样答:

  • READ UNCOMMITTED:很少用,测试阶段玩玩就好。
  • READ COMMITTED:避免脏读,适合大部分高并发系统。
  • REPEATABLE READ:MySQL默认,防止不可重复读,但需要注意幻读(MySQL用MVCC解决了大部分)。
  • SERIALIZABLE:最安全,最慢,用在强一致性场景,如金融核心系统。

面试“反客为主”的关键一击

故事回到开头,我在面试官问我事务隔离级别之后,不仅解释了四种隔离级别,还结合MySQL的MVCC机制补了一句:

“面试官,其实MySQL在REPEATABLE READ下是用多版本并发控制(MVCC)来避免幻读的,而不是加锁。”

面试官眼睛一亮:“哎哟不错哟!你这个点很多人答不到。”

我也就凭这一句,把原本的“答题”变成了“讲解”,顺利拿到offer!

写在最后

事务隔离级别这个知识点,看起来简单,实则“杀伤力”巨大,写业务代码的时候可能忽略,面试的时候却能成为你翻盘的关键!

希望你看完这篇文章,不只是背下四个隔离级别,而是能真正理解它们在项目中怎么应用、Spring是怎么支持的、底层怎么实现的。

最后送你一句面试金句:

“业务无小事,事务需谨慎,隔离不对,数据打回原形。”

END

如果你喜欢这篇文章,欢迎点个“在看”+“分享”,技术路上,一起成长!评论区欢迎大家说说你踩过哪些事务坑,咱们一起“反坑为主”!

我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!