摘要: 原创出处 sf.gg/a/1190000013341344 「handaqiang」欢迎转载,保留摘要,谢谢!

  • 前言

  • 基础概念

    • 1. 什么是事务传播行为?

    • 2. Spring 中七种事务传播行为

  • 代码验证

    • 1.PROPAGATION_REQUIRED

    • 2.PROPAGATION_REQUIRES_NEW

    • 3.PROPAGATION_NESTED

    • 4. REQUIRED,REQUIRES_NEW,NESTED 异同

    • 5. 其他事务传播行为

  • 模拟用例

  • 结论


前言

Spring 在 TransactionDefinition 接口中规定了 7 种类型的事务传播行为。事务传播行为是 Spring 框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。这是 Spring 为我们提供的强大的工具箱,使用事务传播行可以为我们的开发工作提供许多便利。但是人们对他的误解也颇多,你一定也听过“service 方法事务最好不要嵌套”的传言。要想正确的使用工具首先需要了解工具。本文对七种事务传播行为做详细介绍,内容主要代码示例的方式呈现。

基础概念

1. 什么是事务传播行为?

事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

用伪代码说明:

 public void methodA(){methodB();//doSomething}@Transaction(Propagation=XXX)public void methodB(){//doSomething}

代码中methodA()方法嵌套调用了methodB()方法,methodB()的事务传播行为由@Transaction(Propagation=XXX)设置决定。这里需要注意的是methodA()并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。

2. Spring 中七种事务传播行为

定义非常简单,也很好理解,下面我们就进入代码测试部分,验证我们的理解是否正确。

代码验证

文中代码以传统三层结构中两层呈现,即 Service 和 Dao 层,由 Spring 负责依赖注入和注解式事务管理,DAO 层由 Mybatis 实现,你也可以使用任何喜欢的方式,例如,Hibernate,JPA,JDBCTemplate 等。数据库使用的是 MySQL 数据库,你也可以使用任何支持事务的数据库,并不会影响验证结果。

首先我们在数据库中创建两张表:

user1

CREATE TABLE `user1` (`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,`name` VARCHAR(45) NOT NULL DEFAULT '',PRIMARY KEY(`id`)
)
ENGINE = InnoDB;

user2

CREATE TABLE `user2` (`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,`name` VARCHAR(45) NOT NULL DEFAULT '',PRIMARY KEY(`id`)
)
ENGINE = InnoDB;

然后编写相应的 Bean 和 DAO 层代码:

User1

public class User1 {private Integer id;private String name;//get和set方法省略...
}

User2

public class User2 {private Integer id;private String name;//get和set方法省略...
}

User1Mapper

public interface User1Mapper {int insert(User1 record);User1 selectByPrimaryKey(Integer id);//其他方法省略...
}

User2Mapper

public interface User2Mapper {int insert(User2 record);User2 selectByPrimaryKey(Integer id);//其他方法省略...
}

最后也是具体验证的代码由 service 层实现,下面我们分情况列举。

1.PROPAGATION_REQUIRED

我们为 User1Service 和 User2Service 相应方法加上Propagation.REQUIRED属性。

User1Service 方法:

@Service
public class User1ServiceImpl implements User1Service {//省略其他...@Override@Transactional(propagation = Propagation.REQUIRED)public void addRequired(User1 user){user1Mapper.insert(user);}
}

User2Service 方法:

@Service
public class User2ServiceImpl implements User2Service {//省略其他...@Override@Transactional(propagation = Propagation.REQUIRED)public void addRequired(User2 user){user2Mapper.insert(user);}@Override@Transactional(propagation = Propagation.REQUIRED)public void addRequiredException(User2 user){user2Mapper.insert(user);throw new RuntimeException();}}

1.1 场景一

此场景外围方法没有开启事务。

验证方法 1:

    @Overridepublic void notransaction_exception_required_required(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequired(user2);throw new RuntimeException();}

验证方法 2:

    @Overridepublic void notransaction_required_required_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiredException(user2);}

分别执行验证方法,结果:

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

1.2 场景二

外围方法开启事务,这个是使用率比较高的场景。

验证方法 1:

    @Override@Transactional(propagation = Propagation.REQUIRED)public void transaction_exception_required_required(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequired(user2);throw new RuntimeException();}

验证方法 2:

    @Override@Transactional(propagation = Propagation.REQUIRED)public void transaction_required_required_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiredException(user2);}

验证方法 3:

    @Transactional@Overridepublic void transaction_required_required_exception_try(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");try {user2Service.addRequiredException(user2);} catch (Exception e) {System.out.println("方法回滚");}}

分别执行验证方法,结果:

结论:以上试验结果我们证明在外围方法开启事务的情况下Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。

2.PROPAGATION_REQUIRES_NEW

我们为 User1Service 和 User2Service 相应方法加上Propagation.REQUIRES_NEW属性。User1Service 方法:

@Service
public class User1ServiceImpl implements User1Service {//省略其他...@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void addRequiresNew(User1 user){user1Mapper.insert(user);}@Override@Transactional(propagation = Propagation.REQUIRED)public void addRequired(User1 user){user1Mapper.insert(user);}
}

User2Service 方法:

@Service
public class User2ServiceImpl implements User2Service {//省略其他...@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void addRequiresNew(User2 user){user2Mapper.insert(user);}@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void addRequiresNewException(User2 user){user2Mapper.insert(user);throw new RuntimeException();}
}

2.1 场景一

外围方法没有开启事务。

验证方法 1:

    @Overridepublic void notransaction_exception_requiresNew_requiresNew(){User1 user1=new User1();user1.setName("张三");user1Service.addRequiresNew(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiresNew(user2);throw new RuntimeException();}

验证方法 2:

    @Overridepublic void notransaction_requiresNew_requiresNew_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addRequiresNew(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiresNewException(user2);}

分别执行验证方法,结果:

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

2.2 场景二

外围方法开启事务。

验证方法 1:

    @Override@Transactional(propagation = Propagation.REQUIRED)public void transaction_exception_required_requiresNew_requiresNew(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2 user3=new User2();user3.setName("王五");user2Service.addRequiresNew(user3);throw new RuntimeException();}

验证方法 2:

    @Override@Transactional(propagation = Propagation.REQUIRED)public void transaction_required_requiresNew_requiresNew_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2 user3=new User2();user3.setName("王五");user2Service.addRequiresNewException(user3);}

验证方法 3:

    @Override@Transactional(propagation = Propagation.REQUIRED)public void transaction_required_requiresNew_requiresNew_exception_try(){User1 user1=new User1();user1.setName("张三");user1Service.addRequired(user1);User2 user2=new User2();user2.setName("李四");user2Service.addRequiresNew(user2);User2 user3=new User2();user3.setName("王五");try {user2Service.addRequiresNewException(user3);} catch (Exception e) {System.out.println("回滚");}}

分别执行验证方法,结果:

结论:在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。

3.PROPAGATION_NESTED

我们为 User1Service 和 User2Service 相应方法加上Propagation.NESTED属性。User1Service 方法:

@Service
public class User1ServiceImpl implements User1Service {//省略其他...@Override@Transactional(propagation = Propagation.NESTED)public void addNested(User1 user){user1Mapper.insert(user);}
}

User2Service 方法:

@Service
public class User2ServiceImpl implements User2Service {//省略其他...@Override@Transactional(propagation = Propagation.NESTED)public void addNested(User2 user){user2Mapper.insert(user);}@Override@Transactional(propagation = Propagation.NESTED)public void addNestedException(User2 user){user2Mapper.insert(user);throw new RuntimeException();}
}

3.1 场景一

此场景外围方法没有开启事务。

验证方法 1:

    @Overridepublic void notransaction_exception_nested_nested(){User1 user1=new User1();user1.setName("张三");user1Service.addNested(user1);User2 user2=new User2();user2.setName("李四");user2Service.addNested(user2);throw new RuntimeException();}

验证方法 2:

    @Overridepublic void notransaction_nested_nested_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addNested(user1);User2 user2=new User2();user2.setName("李四");user2Service.addNestedException(user2);}

分别执行验证方法,结果:

结论:通过这两个方法我们证明了在外围方法未开启事务的情况下Propagation.NESTEDPropagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。

3.2 场景二

外围方法开启事务。

验证方法 1:

    @Transactional@Overridepublic void transaction_exception_nested_nested(){User1 user1=new User1();user1.setName("张三");user1Service.addNested(user1);User2 user2=new User2();user2.setName("李四");user2Service.addNested(user2);throw new RuntimeException();}

验证方法 2:

    @Transactional@Overridepublic void transaction_nested_nested_exception(){User1 user1=new User1();user1.setName("张三");user1Service.addNested(user1);User2 user2=new User2();user2.setName("李四");user2Service.addNestedException(user2);}

验证方法 3:

    @Transactional@Overridepublic void transaction_nested_nested_exception_try(){User1 user1=new User1();user1.setName("张三");user1Service.addNested(user1);User2 user2=new User2();user2.setName("李四");try {user2Service.addNestedException(user2);} catch (Exception e) {System.out.println("方法回滚");}}

分别执行验证方法,结果:

结论:以上试验结果我们证明在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务

4. REQUIRED,REQUIRES_NEW,NESTED 异同

由“1.2 场景二”和“3.2 场景二”对比,我们可知:NESTED 和 REQUIRED 修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。但是 REQUIRED 是加入外围方法事务,所以和外围事务同属于一个事务,一旦 REQUIRED 事务抛出异常被回滚,外围方法事务也将被回滚。而 NESTED 是外围方法的子事务,有单独的保存点,所以 NESTED 方法抛出异常被回滚,不会影响到外围方法的事务。

由“2.2 场景二”和“3.2 场景二”对比,我们可知:NESTED 和 REQUIRES_NEW 都可以做到内部方法事务回滚而不影响外围方法事务。但是因为 NESTED 是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。而 REQUIRES_NEW 是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。

5. 其他事务传播行为

鉴于文章篇幅问题,其他事务传播行为的测试就不在此一一描述了,感兴趣的读者可以去源码中自己寻找相应测试代码和结果解释。传送门:https://github.com/TmTse/transaction-test

模拟用例

介绍了这么多事务传播行为,我们在实际工作中如何应用呢?下面我来举一个示例:

假设我们有一个注册的方法,方法中调用添加积分的方法,如果我们希望添加积分不会影响注册流程(即添加积分执行失败回滚不能使注册方法也回滚),我们会这样写:

   @Servicepublic class UserServiceImpl implements UserService {@Transactionalpublic void register(User user){try {membershipPointService.addPoint(Point point);} catch (Exception e) {//省略...}//省略...}//省略...}

我们还规定注册失败要影响addPoint()方法(注册方法回滚添加积分方法也需要回滚),那么addPoint()方法就需要这样实现:

   @Servicepublic class MembershipPointServiceImpl implements MembershipPointService{@Transactional(propagation = Propagation.NESTED)public void addPoint(Point point){try {recordService.addRecord(Record record);} catch (Exception e) {//省略...}//省略...}//省略...}

我们注意到了在addPoint()中还调用了addRecord()方法,这个方法用来记录日志。他的实现如下:

   @Servicepublic class RecordServiceImpl implements RecordService{@Transactional(propagation = Propagation.NOT_SUPPORTED)public void addRecord(Record record){//省略...}//省略...}

我们注意到addRecord()方法中propagation = Propagation.NOT_SUPPORTED,因为对于日志无所谓精确,可以多一条也可以少一条,所以addRecord()方法本身和外围addPoint()方法抛出异常都不会使addRecord()方法回滚,并且addRecord()方法抛出异常也不会影响外围addPoint()方法的执行。

通过这个例子相信大家对事务传播行为的使用有了更加直观的认识,通过各种属性的组合确实能让我们的业务实现更加灵活多样。

结论

通过上面的介绍,相信大家对 Spring 事务传播行为有了更加深入的理解,希望大家日常开发工作有所帮助。

太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解!相关推荐

  1. rabbitmq 拉取消息太慢_面试官:消息队列这些我都要问

    作者:mousycoder segmentfault.com/a/1190000021054802 消息队列连环炮 项目里怎么样使用 MQ 的? 为什么要使用消息队列? 消息队列有什么优点和缺点? k ...

  2. 重复订单号校验_吊打面试官系列重复消费、顺序消费、分布式事务

    你知道的越多,你不知道的越多 前言 消息队列在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在消息队列的使用和原理方面对小伙伴们进行360°的刁难. 作为一个在互联网公司面一次拿一次Of ...

  3. 关于fi dd ler 手机抓包 网卡地址地址_面试官:你给我讲讲抓包神器tcpdump的原理...

    点击上方蓝色字关注我们~ 面试官 你说你会网络编程?你说你熟悉网络知识,那你使用过tcpdump吗?能给我讲下tcpdump是什么,或者你有用过tcpdump解决过实际问题吗? 如果你学过网络,甚至搞 ...

  4. 在小公司做开发太难了 面试面到我心态爆炸...

    不得不说,最近这段时间,收到的简历实在是堆积如山!之前HR都是单个文件发送过来,现在都是发送直接压缩包!!! 我精挑细选之后,终于找出简历比较符合岗位要求的候选人,于是开始了电话面试. 本着现在找工作 ...

  5. Redis问的太深入,面试官说:“你先回去等通知吧“!

    互联网面试题更新了!随便呈上几道,看看大家能否答出来: (0)redis为什么是key,value的,为什么不是支持SQL的? (1)redis是多线程还是单线程?(回答单线程的请回吧,为什么请回,请 ...

  6. 面试官问:你能介绍一下Ansible变量吗?之后你噼里啪啦讲得太详细让面试官惊呆了

  7. 简历写了会Kafka,面试官90%会让你讲讲acks参数对消息持久化的影响

    面试大厂时,一旦简历上写了Kafka,几乎必然会被问到一个问题:说说acks参数对消息持久化的影响? 这个acks参数在kafka的使用中,是非常核心以及关键的一个参数,决定了很多东西. 所以无论是为 ...

  8. plsql能连mysql吗_面试官:能给我讲讲用代码实现MySQL的读写分离的思路吗?

    1.引言 读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做.因此,一般来讲,读写分离有两种实现方式.第一种是依靠中 ...

  9. 3层b+树索引访问磁盘次数_【112期】面试官:为什么选择B+树作为数据库索引结构?谈谈你的理解

    本文同步Java知音社区,专注于Java 阶段汇总集合:++小Flag实现,一百期面试题汇总++ 背景 首先,来谈谈B树.为什么要使用B树?我们需要明白以下两个事实: [事实1] 不同容量的存储器,访 ...

最新文章

  1. Linux rpm 命令参数
  2. CloseableHttpClient加载证书来访问https网站
  3. android studio socket编程实例
  4. jsp医院管理系统_Thymeleaf+SpringBoot+SpringDataJPA实现的中小医院信息管理系统
  5. Umi 4 RC 发布
  6. 使用vue组件搭建网页应用
  7. 利用用户级线程提高多线程应用的性能
  8. Spark streaming细粒度工作原理
  9. 美团外卖批量投放智能安全头盔:骑手可语音处理订单
  10. android expandablelistview横向,Android ExpandableListView使用小结(二)
  11. tp5ajax即点即改,TP5中即点即改,json分页,单删
  12. 生产者消费者---线程管道
  13. Go语言学习笔记(一) : 搭建Windows下的Go开发环境
  14. 新生报到系统_中大深圳校区欢迎你!5个院系1271名本科新生报到
  15. 数学中蕴含的人生哲理
  16. Google Draco 源码解析
  17. 将Firefox浏览器的Google工具栏拖动到浏览器底部
  18. [语音识别] 单音素、三音素、决策树
  19. 临床预测模型之综合判别改善指数IDI计算
  20. java 十六进制负数_Java 十六进制转十进制正负数

热门文章

  1. 图像检索中BOW和LSH的一点理解
  2. Deep learning From Image to Sequence
  3. 【转载】Linux命令-自动挂载文件/etc/fstab功能详解[转]
  4. java线程实现方式
  5. 整数的无符号编码和有符号编码
  6. 8Manage PPM助力中投证券 项目管理向数字化转型
  7. 查看SQL SERVER数据库的连接数
  8. python练习集100题(21-40)
  9. C++的iostream标准库介绍
  10. 面向对象程序设计第三次作业