拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【Spring基础系列4】注解@Transactional

【Spring基础系列4】注解@Transactional

主要讲解注解@Transactional的基础知识、使用姿势,以及事务不生效的几种Case。

前言

前面已经讲解了IOC的基础知识,以及Spring常用的注解,这篇文章是对上一篇文章《【Spring基础系列3】Spring常用的注解》的补充,由于这个注解需要讲述的内容比较多,一方面该注解非常重要,另一方面非常容易入坑,所以这个注解的内容,就单独放到这篇文章来讲。

项目准备

为了更好通过示例讲解注解@Transactional的特性,本文会有大量的示例,这些示例是依赖如下配置和数据,如果只关注基础知识,可以跳过这一部分。

使用的是Mysql + Innodb存储引擎,事务隔离级别设置为可重复读RR。

pom.xml需要添加的依赖包:

<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency>

使用Spring + MyBatis的方式对DB进行操作,下面是XML映射文件:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.mybatis.dao.UserDao"> <!-- 根据uid查询一个用户信息 --> <select id="selectUserById" parameterType="Integer" resultType="com.mybatis.entity.MyUser"> select * from user_test where uid = #{uid} </select> <!--修改一个用户 --> <update id="updateUser" parameterType="com.mybatis.entity.MyUser"> update user_test set uname =#{uname},usex = #{usex} where uid = #{uid} </update> </mapper>

提供的接口:

@Repository("userDao") @Mapper /* * 使用Spring自动扫描MyBatis的接口并装配 (Spring将指定包中所有被@Mapper注解标注的接口自动装配为MyBatis的映射接口 */ public interface UserDao { /** * 接口方法对应的SQL映射文件中的id */ public MyUser selectUserById(Integer uid); public int updateUser(MyUser user); }

DB结构:

CREATE TABLE `user_test` ( `uid` tinyint(2) NOT NULL, `uname` varchar(20) DEFAULT NULL, `usex` varchar(10) DEFAULT NULL, PRIMARY KEY (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8

DB初始数据:

uidunameusex
1张三女
2陈恒男
3楼仔男

@Transactional基础知识

概述

  • @Transactional是声明式事务管理编程中使用的注解

  • 可以添加在接口实现类或接口实现方法上,而不是接口类中

  • 访问权限只有public的方法才起作用

  • 当接口与接口中方法上同时带有@Transactional注解时,方法上注解属性会覆盖类注解上的相同属性

  • 系统设计时,将标签放置在需要进行事务管理的方法上,而不是不假思索的放置在接口实现类上

  • 错误使用的方式,会在“事务不生效的几种case”中讲述

  • 多线程下事务管理,因为线程不属于spring托管,故线程不能够默认使用spring的事务,也不能获取spring注入的bean;在被spring声明式事务管理的方法内开启多线程,多线程内的方法不被事务控制,具体可参考“事务不生效的几种case”中多线程事务不生效的场景。

使用姿势

讲解基础知识前,我们先看@Transactional怎么使用,下面是DB数据正常更新的情况:

@Controller("userController") public class UserController { @Autowired private UserDao userDao; public void update(Integer id) { MyUser user = new MyUser(); user.setUid(id); user.setUname("张三-testing"); // 变更数据 user.setUsex("女"); userDao.updateUser(user); } public MyUser query(Integer id) { MyUser user = userDao.selectUserById(id); return user; } @Transactional(rollbackFor = Exception.class) public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); //throw new Exception("测试事务回滚生效"); } }

再看一下测试用例:

public static void main(String[] args) throws Exception { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController uc = (UserController) applicationContext.getBean("userController"); try { uc.testSuccess(); } finally { MyUser user = uc.query(1); System.out.println("修改后的记录:" + user); } } // 输出: // 原记录:User[uid=1,uname=张三,usex=女] // 修改后的记录:User[uid=1,uname=张三-test,usex=女]

我们发现DB更新成功,现在我们修改一下代码,将DB数据回退到原始状态,然后再把抛出异常放开,看事务是否生效:

public void update(Integer id) { MyUser user = new MyUser(); user.setUid(id); user.setUname("张三-testing"); // 变更数据 user.setUsex("女"); userDao.updateUser(user); } @Transactional(rollbackFor = Exception.class) public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); throw new Exception("测试事务回滚生效"); }

再看看执行结果:

原记录:User[uid=1,uname=张三,usex=女] 修改后的记录:User[uid=1,uname=张三,usex=女]

我们发现因为程序抛出异常,DB数据正常回滚,符合预期。

实现原理

  • @Transactional实质是使用了JDBC的事务来进行事务控制的

  • @Transactional基于Spring的动态代理的机制 @Transactional实现原理:

  • 事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入DataSource实例的某个与DataSourceTransactionManager相关的某处容器中。在接下来的整个事务中,客户代码都应该使用该connection连接数据库,执行所有数据库命令[不使用该connection连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚](物理连接connection逻辑上新建一个会话session;DataSource与TransactionManager配置相同的数据源)

  • 事务结束时,回滚在第1步骤中得到的代理connection对象上执行的数据库命令,然后关闭该代理connection对象(事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用)

事务特性

pring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口:

public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }

事务的隔离级别:

  • @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用

  • @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)

  • @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)

  • @Transactional(isolation = Isolation.SERIALIZABLE):串行化

事务传播行为(如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为):

  • ransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

  • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

  • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

属性配置

字段说明:

  • value :主要用来指定不同的事务管理器;主要用来满足在同一个系统中,存在不同的事务管理器。比如在Spring中,声明了两种事务管理器txManager1, txManager2。然后,用户可以根据这个参数来根据需要指定特定的txManager。value 适用场景,即在一个系统中,需要访问多个数据源或者多个数据库,则必然会配置多个事务管理器的

  • isolation:事务的隔离度,默认值采用 DEFAULT。

  • propagation:事务的传播行为,默认值为 REQUIRED,具体取值可参考“事务特性”:

    • 例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

  • readOnly:该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)。

  • timeout:该属性用于设置事务的超时秒数,如果超过这个时间就强制回滚,默认值为-1表示永不超时。

  • rollbackFor :该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:

    • 指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)

    • 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})

  • rollbackForClassName:该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:

    • 指定单一异常类名称:@Transactional(rollbackForClassName="RuntimeException")

    • 指定多个异常类名称:@Transactional(rollbackForClassName={"RuntimeException","Exception"})

  • noRollbackFor :该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:

    • 指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)

    • 指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})

  • noRollbackForClassName:该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:

    • 指定单一异常类名称:@Transactional(noRollbackForClassName="RuntimeException")

    • 指定多个异常类名称:@Transactional(noRollbackForClassName={"RuntimeException","Exception"})

事务不生效的几种case

所有的测试Case,测试完毕后,DB数据需要手动更新成原始数据,保证测试Case互不影响。

Case1: 类内部访问

简单来讲就是指非直接访问带注解标记的方法 B,而是通过类普通方法 A,然后由 A 访问 B,下面是一个简单的 case,我们在类UserController中新增一个方法testFail():

public void testFail() throws Exception { testSuccess(); throw new Exception("测试事务回滚不生效"); }

这里我们是通过testFail()调用testSuccess(),再看一下测试用例:

public static void main(String[] args) throws Exception { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController uc = (UserController) applicationContext.getBean("userController"); try { uc.testFail(); } finally { MyUser user = uc.query(1); System.out.println("修改后的记录:" + user); } } // 输出: // 原记录:User[uid=1,uname=张三,usex=女] // 修改后的记录:User[uid=1,uname=张三,usex=女]

从上面的输出可以看到,事务并没有回滚,主要是因为类内部调用,不会通过代理方式访问。

Case2: 私有方法

在私有方法上,添加@Transactional注解也不会生效,私有方法外部不能访问,所以只能内部访问,上面的 case 不生效,这个当然也不生效了:

@Transactional(rollbackFor = Exception.class) private void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); throw new Exception("测试事务生效"); }

直接使用时,下面这种场景不太容易出现,因为 IDEA 会有提醒,文案为: Methods annotated with '@Transactional' must be overridable

Case3: 异常不匹配

@Transactional注解默认处理运行时异常,即只有抛出运行时异常时,才会触发事务回滚,否则并不会回滚:

@Transactional public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); throw new Exception("测试事务生效"); }

测试 case 如下:

public static void main(String[] args) throws Exception { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserController uc = (UserController) applicationContext.getBean("userController"); try { uc.testSuccess(); } finally { MyUser user = uc.query(1); System.out.println("修改后的记录:" + user); } } // 输出: // 原记录:User[uid=1,uname=张三,usex=女] // 修改后的记录:User[uid=1,uname=张三-test,usex=女]

输出结果如下,事务并未回滚(如果需要解决这个问题,通过设置@Transactional的 rollbackFor 属性即可)

Case4: 多线程

这个场景可能并不多见,在标记事务的方法内部,另起子线程执行 db 操作,此时事务同样不会生效

下面给出两个不同的姿势,一个是子线程抛异常,主线程 ok;一个是子线程 ok,主线程抛异常。

父线程抛出异常

我们在类UserController中新增一个方法testMultThread(),该方法在主线程会抛出异常:

public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); } @Transactional(rollbackFor = Exception.class) public void testMultThread() throws Exception { new Thread(new Runnable() { @SneakyThrows @Override public void run() { testSuccess(); } }).start(); throw new Exception("测试事务不生效"); }

上面这种场景不生效很好理解,子线程的异常不会被外部的线程捕获,testMultThread这个方法的调用不抛异常,因此不会触发事务回滚,调用方式和前面一样,就换一个方法:

uc.testMultThread();

这里提醒一下,输出的数据因为都是在主线程中输出,所以输出结果都是“张三”,后来我看了库表,发现DB数据已经更新为“张三-testing”,所以事务回滚没有生效。

子线程抛出异常

我们修改代码如下,让子任务抛出异常:

public void testSuccess() throws Exception { Integer id = 1; MyUser user = query(id); System.out.println("原记录:" + user); update(id); throw new Exception("测试事务生效"); } @Transactional(rollbackFor = Exception.class) public void testMultThread() throws Exception { new Thread(new Runnable() { @SneakyThrows @Override public void run() { testSuccess(); } }).start(); }

同上,DB数据没有回滚,发现DB数据已经更新为“张三-testing”,所以事务回滚没有生效。

Case5: 传播属性

这个内容,我后面再单独出一篇文章来讲,要不然这篇文章篇幅又太长了。

返回列表