最近啊,我准备跳槽了!
是的,一个在公司搬了五年砖的我,终于在上个月鼓起勇气投了几家心仪的公司,走上了社招之路。期间当然被“摧残”了一遍又一遍,什么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再次查询,发现多了一条 → 幻读!
数据库怎么防?四大隔离级别来了!
为了解决这些问题,数据库提供了四种隔离级别,它们是:
越往下越严格,但性能代价也越大!
注意:MySQL InnoDB 默认是 REPEATABLE READ,而 Oracle 默认是 READ COMMITTED。
Spring 是怎么支持事务隔离的?
故事继续,我在项目里用到了 Spring 的声明式事务,也就是你经常写的:
这个注解非常强大!可以指定:
- 传播行为(Propagation)
- 事务隔离级别(isolation)
- 回滚规则
- 只读标志
今天我们只说隔离级别,Spring 提供如下常量:
用法也很简单,比如我们要设置成 READ_COMMITTED:
项目实战演练:用隔离级别解决“库存错乱”
我在一个促销项目中遇到过一个“库存显示错乱”的bug,用户下单后库存还没变,后台却显示库存已经减少,最后发现是 脏读 问题!
原来,我们的后台查询用了默认隔离级别,而MySQL默认是REPEATABLE READ,但有些同事临时建的表用的是 MyISAM 存储引擎(不支持事务!),最终导致了读未提交数据。
解决方案:
- 强制所有表使用 InnoDB;
- 对查询方法标注 @Transactional(isolation = Isolation.READ_COMMITTED),防止脏读。
这个bug查了一周,后端前端互甩锅,结果只是个“隔离级别”没配对的问题……
Spring 事务背后的原理一丢丢
面试官常会追问:“那你知道Spring是怎么实现这些的吗?”
这时候,来点底层“调味料”:
- Spring事务是通过 AOP(面向切面编程) 实现的,核心是 TransactionInterceptor。
- 它通过 动态代理,在方法前后加入“开启事务”、“提交/回滚事务”的逻辑。
- 隔离级别其实是通过 Connection 接口的 setTransactionIsolation() 方法设置给底层数据库连接的。
例如:
事务失效的坑你踩过没?
讲完原理,不讲“坑”太抽象。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岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!