java中数据库事务嵌套与mysql事务隔离级别

原文链接: java中数据库事务嵌套与mysql事务隔离级别

今天在java代码中遇到一个数据库相关的bug,在分析和解决问题过程中,调研了一下java的事务传播属性,以及mysql事务隔离级别,这俩知识点以前虽然了解但其实没有完全理解,希望通过这个问题好好总结一下。

问题初现

背景:java中方法a(加了数据库事务注解)调用了方法b(同样加了数据库事务注解),B中对表t的部分行执行了更新操作;方法a中在调用b后,执行了对表t的select操作,但发现select到的数据中并不包含B的修改。用简易代码大概表示为:

@Transactional(propagation = Propagation.REQUIRED, readOnly = false, rollbackFor=Exception.class)
public void a() {
    …… ……
    //调用方法b
    b();
    
    //查询方法b中对表t更新的行
    Object result = selectFromTableT();
    
    //oops!! result中没有方法b的更新数据
}

@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = false, rollbackFor=Exception.class)
public void b() {
    //执行对表t的update操作
    updateTableT();
}

问题大概可以总结为,嵌套事务中执行的数据库修改操作对外层事务不可见,首先猜测是@Transactional注解中的事务传播属性REQUIRES_NEW的问题,会不会因为b方法新起了事务导致不同事务之间修改不可见?mysql事务隔离级别是默认的Repeatable Read

事务传播属性

首先全面了解一下集中事务传播属性,spring中总共定义了七种事务传播属性:

public enum Propagation {
    //支持当前事务,如果当前没有事务,就新建一个事务,这是最常用的选择
    REQUIRED(0),
    
    //支持当前事务,如果当前没有事务,就以非事务方式执行。 
    SUPPORTS(1),
    
    //支持当前事务,如果当前没有事务,就抛出异常。 
    MANDATORY(2),
    
    //新建事务,如果当前存在事务,把当前事务挂起。 
    REQUIRES_NEW(3),
    
    //以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
    NOT_SUPPORTED(4),
    
    //以非事务方式执行,如果当前存在事务,则抛出异常。 
    NEVER(5),
    
    //如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
    NESTED(6);
}

其中REQUIRES_NEWNESTED容易混淆,看起来都是在原有事务的基础上再开一个嵌套事务,他们的区别在哪?嵌套事务机制到底是怎样,内外事务的提交和回滚分别是何时触发的?

  • REQUIRES_NEW会启动一个独立的新事务,这个事务将被完全 commited 或 rollback 而不依赖于外部事务,它拥有自己的隔离范围,自己的锁等等。当内部事务开始执行时,外部事务将被挂起,内务事务结束时,外部事务将继续执行。
  • NESTED会开始一个 "嵌套的" 事务,它是已经存在事务的一个真正的子事务。 嵌套事务开始执行时,它将取得一个savepoint,如果这个嵌套事务失败,将回滚到此savepoint。 嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交。

由此可见,REQUIRES_NEW启动的新事务不依赖于外部事务,是完全独立的,这意味着事务commit和rollback操作都是独立的,不受外部事务commit或者rollback影响。
NESTED是依赖于外部事务的子事务,只有当外部事务commit时,子事务才能commit;外部事务发生异常rollback,子事务也要回滚。

回到上面的问题,方法b的事务传播属性设置为REQUIRES_NEW,意味着会开启一个完全独立的事务。当方法a中调用包含新事务的方法b之后,执行selectFromTableT方法查询方法b的修改行时,方法b中对表的修改操作已经提交,已经提交的修改为什么对方法a不可见?看起来这个问题还和mysql事务隔离级别有关,是时候捡起数据库隔离级别细读一番了。

mysql事务隔离级别

首先回顾一下mysql的四种事务隔离级别:

1. Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

2. Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

3. Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

4. Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

不同事务隔离级别下几种典型问题:

1. 脏读
主要表现为一个事务中前后两次读取数据不一致,例如在Read Uncommitted隔离级别下,一个事务可以读取其他未提交事务的修改,前后两次读取之间可能有其他事务的修改或者回滚操作,导致前后数据不一致;

2. 幻读
主要表现为一个事务前后读取行数不一致或者读到了不存在的数据,用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。

3. 不可重复读
和脏读类似,表现为一个事务内前后两次读取数据不一致,不可重复读问题是由于在一个事务执行过程中有其他事务已经commit的修改,导致前后读取不一致。

. 脏读 幻读 不可重复读
Read Uncommitted Yes Yes Yes
Read Committed No Yes Yes
Repeatable Read No Yes No
Serializable No No No

现在想来以前根本没有理解脏读和不可重复度两种问题的区别,虽然两种问题都表现为一个事务内对同一条数据前后读取结果不一致。
脏读是因为当前事务内在两次读取之间有其他事务修改了同样的数据行但未提交;不可重复度是因为在当前事务两次读取之间有其他事务修改了同样的数据行而且已经提交
以前理解的误区在于,理所当然的以为已经提交事务的修改对任何隔离级别的事务都是可见的,还是太天真了┐(´д`)┌

那么上面问题的原因也很清晰了,因为事务隔离级别是mysql默认的Repeatable Read,这种隔离级别下要保证一个事务内前后读取到同样的数据,也就意味着对其它已提交或者未提交的修改都不可见,所以上述问题中方法a的事务对b方法中事务已提交的修改也选择不见(即使已经提交(|||゚д゚))。

例行总结

上面问题的原因总结为,在事务传播属性REQUIRES_NEW和mysql事务隔离级别Repeatable Read的组合情况下,由于方法b设置的传播属性REQUIRES_NEW会开启一个独立的新事务,同时Repeatable Read隔离级别下,为了保证不可重复读,即方法a在调用方法b前后读取的数据一致,因此方法b中对数据库的修改在a中不可见。
解决方法:将方法b的事务传播属性设置为REQUIRED,保证不开启新事务,方法b与方法a共用同一个事务,同一个事务内修改一定都是可见的;如果某些特殊情况下,一定需要将方法b的事务传播属性设置为REQUIRES_NEW,那么可以修改事务隔离级别为READ_COMMITED,不保证重复读,从而可以读到其它事务已提交的修改。

java的优势就在于可以灵活配置事务传播属性和事务隔离级别满足不同场景的数据库操作。但是使用过程中要清楚每种组合情况下可能会产生什么影响,例如我踩的坑是因为组合使用了REQUIRES_NEWREPATABLE READ,导致子事务中提交的修改对外部事务不可见,同时还会造成外部事务发生异常回滚后,子事务并未回滚的问题。

我理解的REQUIRES_NEW的使用场景是内外事务之间完全独立,不需要保证数据一致性,不需要跟随外部事务一起回滚,而且可以有自己的隔离范围和锁。但如果要使用该属性,一定要确认自己的使用场景,内部事务和外部事务真的是完全独立的,也不需要保证内外数据一致性。我理解大部分场景下,更适合使用REQUIREDNESTED,最主要是需要保证内外数据一致性。



参考阅读
解惑 spring 嵌套事务
Transaction Isolation Levels
MySQL事务隔离级别
《Spring技术内幕(第2版)》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容