Java事务隔离级别搞不懂?面试必问4个级别,并发问题实战解决指南

Java事务隔离级别搞不懂?面试必问4个级别,并发问题实战解决指南 一

文章目录CloseOpen

事务隔离级别到底解决什么问题?从3个“读”坑说起

你可能会说“事务不就是保证ACID吗?隔离性不就是让事务之间互不干扰?”这话没错,但太笼统了。我举个例子,去年我帮朋友的电商平台排查一个bug:用户A下单买最后一件商品,系统显示“下单成功”,但用户B几乎同时也下单成功了,结果库存变成了-1。查日志发现,两个事务同时读取到库存=1,都扣减后提交,典型的“幻读”问题。后来把隔离级别从“读已提交”改成“可重复读”,加了行锁,才解决。

为什么会出现这种问题?因为数据库默认允许多个事务同时执行(并发),但如果不加以控制,就会出现3种常见的“读”问题:

脏读

:你事务里改了数据还没提交,我事务就能读到这个“临时数据”。比如你给我转账1000,事务执行了update balance set money=money+1000 where id=1,但还没commit,我这边查余额已经多了1000,结果你突然回滚了,我看到的就是“假钱”。
不可重复读:你两次读同一个数据,结果不一样。比如你第一次查余额是5000,这时候我转走了3000并提交,你再查变成2000。这种在统计报表场景就很坑,比如财务做月度汇总,查了一半数据被改了,报表就不准了。
幻读:你两次查询同一个条件的结果集行数不一样。开头说的库存问题就是典型:事务1查“库存>0”有1条,准备扣减;事务2也查“库存>0”有1条,扣减后提交;事务1再查,发现库存已经是0了,但之前的结果集让它以为还能扣减。

这些问题怎么解决?数据库大佬们就设计了“事务隔离级别”,相当于给并发事务之间加了“隔离带”,级别越高,隔离带越宽,并发问题越少,但性能也可能越差。接下来我就逐个拆这4个级别,看完你就知道怎么选了。

4个隔离级别逐个拆:从“概念”到“代码”,连小白都能懂

很多人学隔离级别只记定义,比如“读未提交就是能读到没提交的”,但不知道实际场景怎么用。我当年也是这样,面试背得滚瓜烂熟,结果项目里还是踩坑。后来我发现,把每个级别和“解决什么问题”“牺牲什么性能”对应起来,就好懂多了。

  • 读未提交(Read Uncommitted):最“奔放”的隔离级别
  • 这个级别最简单:一个事务可以读到另一个事务没提交的修改。相当于两个事务完全“裸奔”,互相能看到对方的临时数据。

    能解决什么?

    几乎啥也解决不了,脏读、不可重复读、幻读都可能发生。 性能怎么样? 最好,因为几乎不加锁,数据库不用做额外处理。 什么场景用? 我工作这么多年,只在“对数据准确性要求极低,纯追求速度”的场景见过,比如实时监控系统显示服务器CPU使用率(就算读错了,下次刷新就对了)。实际业务系统几乎不会用。

  • 读已提交(Read Committed):大多数数据库的“默认选手”
  • 这个级别是Oracle、SQL Server的默认隔离级别:一个事务只能读到另一个事务已经提交的修改。相当于给临时数据加了个“窗帘”,没提交的别人看不到。

    解决了脏读

    :因为你没提交的数据,我读不到了。比如你转账1000没提交,我查余额还是原来的数,你回滚了也不影响我。 但可能有不可重复读:你提交了修改,我就能读到。比如我第一次查余额5000,你转走3000提交后,我再查就是2000。 MySQL里怎么看? 如果你用MySQL,执行SET TRANSACTION ISOLATION LEVEL READ COMMITTED;就能切换到这个级别。我之前做一个日志分析系统,每天凌晨跑批处理统计前一天的日志,用的就是这个级别——因为日志一旦写入就不会改了,不怕不可重复读,而且比更高的级别快不少。

  • 可重复读(Repeatable Read):MySQL的“看家本领”
  • 这是MySQL InnoDB的默认级别:一个事务执行过程中,多次读取同一数据,结果始终一致。就算其他事务改了数据并提交,你也看不到。

    解决了不可重复读

    :比如你第一次查余额5000,我转走3000提交,你再查还是5000(直到你自己的事务提交)。 怎么做到的? MySQL用了“MVCC(多版本并发控制)”技术,简单说就是给每行数据存了多个“快照版本”。你事务开始时,会拿到一个“版本号”,之后不管别人怎么改,你都只看自己版本号对应的快照。就像你给数据拍了张照,别人再怎么涂鸦,你看的还是原来那张。 那幻读呢? MySQL的可重复读通过“间隙锁”解决了部分幻读场景。比如你执行select * from goods where stock > 0 for update;(加行锁和间隙锁),这时候其他事务就不能插入或修改满足stock > 0的记录,避免了“读的时候有1条,写的时候突然多了/少了”的问题。但如果不加锁的普通查询,还是可能有幻读(不过实际业务中,配合MVCC,大部分情况能接受)。
    我项目里的用法:电商订单系统的“创建订单”流程,我都是用这个级别。用户下单时,先查库存(可重复读保证不会读到别人刚扣减的库存),再扣减,最后提交。就算有1000个人同时下单,也不会超卖——这比读已提交安全,又比串行化快得多。

  • 串行化(Serializable):最“严格”也最“慢”的级别
  • 这个级别是“终极方案”:所有事务串行执行,一个跑完另一个才能开始,相当于单线程操作数据库。

    解决所有并发问题

    :脏读、不可重复读、幻读全没了,因为根本没有并发了。 性能怎么样? 最差,因为要排队执行,吞吐量极低。 什么场景用? 我只在“数据绝对不能错,哪怕慢死”的场景见过,比如银行转账的核心记账系统(一笔都不能错),或者库存只有1件的“秒杀”(必须保证只有一个人能买到)。

    4个级别对比表:一张表看懂怎么选

    为了让你更清晰,我做了个对比表,把每个级别的特点、解决的问题、性能都列出来了:

    隔离级别 解决脏读? 解决不可重复读? 解决幻读? 性能(从高到低)
    读未提交 1(最快)
    读已提交 2
    可重复读 大部分情况✅ 3
    串行化 4(最慢)

    实战选级别:记住这3个原则

    讲了这么多,你可能还是会问“我到底选哪个?”其实没那么复杂,记住我 的3个原则,90%的场景都能搞定:

  • 普通业务系统优先用“读已提交”或“可重复读”
  • 如果你的系统是订单、用户管理这类常规业务,数据要准但不用“绝对准”(比如用户改个昵称,重复读时看到旧数据也没关系),选读已提交(Oracle默认);如果是电商库存、财务统计,需要多次查询结果一致,选可重复读(MySQL默认)。我之前做的电商平台,商品详情页用读已提交(快),下单流程用可重复读(准),效果很好。

  • 别轻易用“串行化”,除非你能接受“慢”
  • 串行化虽然安全,但相当于“单线程”,并发一高就卡。我之前帮一个客户调优系统,发现他们把所有事务都设成串行化,导致高峰期订单提交要等5秒以上。后来改成可重复读+行锁,响应时间降到500ms以内——记住,大多数时候,“合理的锁策略+合适的隔离级别”比“最高级别”更实用。

  • 用Spring时别忘了显式配置
  • 如果你用Spring开发,记得在@Transactional注解里指定隔离级别,比如@Transactional(isolation = Isolation.REPEATABLE_READ)。别依赖数据库默认值,万一换了数据库(比如从MySQL迁到Oracle),默认级别变了,可能出大问题。我之前就见过有人把代码从MySQL迁到Oracle,没改隔离级别,结果出现不可重复读,排查了半天才发现是默认级别不一样。

    最后再啰嗦一句:事务隔离级别不是“越高越好”,而是“够用就好”。你要做的是根据业务场景,找到“数据一致性”和“性能”的平衡点。如果你按我说的试了,或者有其他问题,欢迎在评论区告诉我——咱们一起避坑,少走弯路!


    你有没有想过,事务隔离级别说的“隔离”,到底是靠什么实现的?其实背后全是锁在帮忙——就像你在图书馆看书,想独占一本书就得拿在手里(加锁),别人只能等你看完(释放锁)。事务隔离级别本质上就是通过不同的锁策略,控制多个事务怎么“排队”或者“并行”访问数据,级别越高,锁的规则越严,事务之间的干扰就越少,但大家排队的时间也可能越长。

    拿具体级别来说,读未提交基本是“放飞自我”的状态,数据库几乎不加锁,事务A改了数据还没提交,事务B就能直接读到这个临时值,所以才会有脏读。这种级别一般只用在对数据准确性要求极低的场景,比如实时监控系统显示服务器负载,就算读错一次,下一秒刷新就对了,没人会较真。而读已提交就规矩多了,它会给数据加“共享锁”——你读数据的时候,别人可以读但不能改,等你读完释放锁,别人改了再提交,你下次读才能看到新数据。不过这种锁只在读取瞬间持有,读完就放,所以还是可能出现不可重复读,就像你查工资条,第一次看是10000,刚放下手机财务就发了奖金并提交,你再查变成12000,前后对不上。

    到了可重复读,锁的玩法就升级了。MySQL的InnoDB会用“行锁”+“MVCC多版本快照”:你事务开始时,数据库会给你拍一张数据快照,之后不管别人怎么改数据、加行锁提交,你看到的始终是快照里的旧数据。我之前做订单系统的时候,就遇到过用读已提交导致不可重复读的问题——用户下单时查库存是5件,刚准备扣减,另一个事务把库存改成3件并提交,结果第一个事务再查发现数量不对,订单数据直接乱了。后来改成可重复读,加上行锁锁定那几行库存记录,才算把数据稳住。至于最高级的串行化,就简单粗暴了,直接上“表锁”——整个表同一时间只能有一个事务操作,相当于大家排队挨个来,所以脏读、不可重复读、幻读全解决了,但性能也是真的差,我见过有人把秒杀系统设成串行化,结果并发一上来,页面直接卡到超时。

    其实选隔离级别时,你得想清楚“锁的成本”:级别越高,锁的粒度越细(比如行锁比表锁影响范围小),或者持有时间越长,事务排队的时间就越久,系统吞吐量可能掉一半。我一般 先从业务场景出发——普通查询用读已提交够了,订单、库存这种核心数据用可重复读,实在要求“绝对不能错”的场景(比如银行转账记账),再考虑串行化。别一上来就用最高级别,不然性能坑可能比数据不一致的坑还难填。


    如何在MySQL中查看和修改当前的事务隔离级别?

    可通过SQL命令查看当前隔离级别:MySQL 5.7及之前版本用 SELECT @@tx_isolation;,MySQL 8.0及以上用 SELECT @@transaction_isolation;。修改当前会话隔离级别可执行 SET TRANSACTION ISOLATION LEVEL 隔离级别名称;(如 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;),若需全局生效则用 SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别名称;,修改后需重启连接生效。

    不可重复读和幻读的区别是什么?

    不可重复读指同一事务内,两次读取同一行数据时,因其他事务修改并提交导致结果不同(如第一次读余额5000,第二次读变成2000);幻读指同一事务内,两次执行相同条件的范围查询时,因其他事务插入/删除数据导致结果集行数变化(如第一次查“库存>0”有1条,第二次查变成0条)。简单说,不可重复读是“行数据变了”,幻读是“行数变了”。

    Spring中如何通过@Transactional设置事务隔离级别?

    在Spring的 @Transactional 注解中,通过 isolation 属性指定隔离级别,例如 @Transactional(isolation = Isolation.REPEATABLE_READ)。Isolation枚举提供了5个选项:DEFAULT(默认,跟随数据库设置)、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。需注意,若数据库不支持指定级别(如某些数据库不支持SERIALIZABLE),会抛出异常。

    为什么MySQL默认隔离级别是可重复读而不是读已提交?

    MySQL InnoDB选择可重复读作为默认级别,主要是平衡了数据一致性和性能。可重复读通过MVCC(多版本并发控制)机制,在保证事务执行过程中读取数据一致性的 避免了读已提交可能出现的不可重复读问题,且通过行锁而非表锁实现,性能优于串行化。而Oracle默认读已提交,是因为其MVCC实现方式不同,更侧重并发性能,两者设计理念均基于自身数据库架构特点。

    事务隔离级别和锁机制有什么关系?

    事务隔离级别依赖锁机制实现并发控制:读未提交通常不加锁,直接读取最新数据;读已提交通过行级共享锁(读锁)和释放机制,保证只读取已提交数据;可重复读在MySQL中结合MVCC和行锁(必要时加间隙锁),避免不可重复读和部分幻读;串行化则通过表级锁强制事务串行执行。隔离级别越高,锁的粒度或持有时间通常越大,并发性能越低,需根据业务场景平衡。

    0
    显示验证码
    没有账号?注册  忘记密码?