【Spring基础系列4】注解@Transactional
发布时间:2026/9/30 7:10:15 锦皓数字建站

主要讲解注解Transactional的基础知识、使用姿势以及事务不生效的几种Case。前言前面已经讲解了IOC的基础知识以及Spring常用的注解这篇文章是对上一篇文章《【Spring基础系列3】Spring常用的注解》的补充由于这个注解需要讲述的内容比较多一方面该注解非常重要另一方面非常容易入坑所以这个注解的内容就单独放到这篇文章来讲。项目准备为了更好通过示例讲解注解Transactional的特性本文会有大量的示例这些示例是依赖如下配置和数据如果只关注基础知识可以跳过这一部分。使用的是Mysql Innodb存储引擎事务隔离级别设置为可重复读RR。pom.xml需要添加的依赖包dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.4.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version6.0.6/version /dependency使用Spring MyBatis的方式对DB进行操作下面是XML映射文件?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.mybatis.dao.UserDao !-- 根据uid查询一个用户信息 -- select idselectUserById parameterTypeInteger resultTypecom.mybatis.entity.MyUser select * from user_test where uid #{uid} /select !--修改一个用户 -- update idupdateUser parameterTypecom.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) ) ENGINEInnoDB DEFAULT CHARSETutf8DB初始数据uidunameusex1张三女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[uid1,uname张三,usex女] // 修改后的记录:User[uid1,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[uid1,uname张三,usex女] 修改后的记录:User[uid1,uname张三,usex女]我们发现因为程序抛出异常DB数据正常回滚符合预期。实现原理Transactional实质是使用了JDBC的事务来进行事务控制的Transactional基于Spring的动态代理的机制 Transactional实现原理事务开始时通过AOP机制生成一个代理connection对象并将其放入DataSource实例的某个与DataSourceTransactionManager相关的某处容器中。在接下来的整个事务中客户代码都应该使用该connection连接数据库执行所有数据库命令[不使用该connection连接数据库执行的数据库命令在本事务回滚的时候得不到回滚]物理连接connection逻辑上新建一个会话sessionDataSource与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(propagationPropagation.NOT_SUPPORTED,readOnlytrue)readOnly该属性用于设置当前事务是否为只读事务设置为true表示只读false则表示可读写默认值为false。例如Transactional(readOnlytrue)。timeout该属性用于设置事务的超时秒数如果超过这个时间就强制回滚默认值为-1表示永不超时。rollbackFor 该属性用于设置需要进行回滚的异常类数组当方法中抛出指定异常数组中的异常时则进行事务回滚。例如指定单一异常类Transactional(rollbackForRuntimeException.class)指定多个异常类Transactional(rollbackFor{RuntimeException.class, Exception.class})rollbackForClassName该属性用于设置需要进行回滚的异常类名称数组当方法中抛出指定异常名称数组中的异常时则进行事务回滚。例如指定单一异常类名称Transactional(rollbackForClassNameRuntimeException)指定多个异常类名称Transactional(rollbackForClassName{RuntimeException,Exception})noRollbackFor 该属性用于设置不需要进行回滚的异常类数组当方法中抛出指定异常数组中的异常时不进行事务回滚。例如指定单一异常类Transactional(noRollbackForRuntimeException.class)指定多个异常类Transactional(noRollbackFor{RuntimeException.class, Exception.class})noRollbackForClassName该属性用于设置不需要进行回滚的异常类名称数组当方法中抛出指定异常名称数组中的异常时不进行事务回滚。例如指定单一异常类名称Transactional(noRollbackForClassNameRuntimeException)指定多个异常类名称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[uid1,uname张三,usex女] // 修改后的记录:User[uid1,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 overridableCase3: 异常不匹配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[uid1,uname张三,usex女] // 修改后的记录:User[uid1,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: 传播属性这个内容我后面再单独出一篇文章来讲要不然这篇文章篇幅又太长了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。