声明式事务控制
关于事务
理解事务
事务是一组操作的执行单元,相对于数据库操作来讲,事务管理的是一组SQL指令,比如增加,修改,删除等
事务的一致性,要求,这个事务内的操作必须全部执行成功,如果在此过程种出现了差错,比如有一条SQL语句没有执行成功,那么这一组操作都将全部回滚
事务由事务开始和事务结束之间执行的全部数据库操作组成。
事务四大特性
原子性A:事务是不可分割的最小操作单元,要么全成功,要么全失败
一致性C:事务完成时,必须所据都保持一致状态
隔离性I:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下进行
持久性D:事务一旦提交或回滚,他对数据库的改变是永久的
声明式事务控制
理解声明式事务
Spring
对事务逻辑的代码有封装,在配置文件中或者注解管理即可实现相关操作
Spring
的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在Spring
配置文件中声明式的处理事务来代替代码式的处理事务
声明式事务处理的作用
事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可
在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便
Spring提供了一个PlatformTransactionManager
来表示事务管理器,所有的事务都由它负责管理。而事务由TransactionStatus
表示。如果手写事务代码,使用try...catch
如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 TransactionStatus tx = null ;try { tx = txManager.getTransaction(new DefaultTransactionDefinition ()); jdbcTemplate.update("..." ); jdbcTemplate.update("..." ); txManager.commit(tx); } catch (RuntimeException e) { txManager.rollback(tx); throw e; }
Spring为啥要抽象出PlatformTransactionManager
和TransactionStatus
?原因是JavaEE除了提供JDBC事务外,它还支持分布式事务JTA(Java
Transaction
API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。
Spring为了同时支持JDBC和JTA两种事务模型,就抽象出PlatformTransactionManager
。因为我们的代码只需要JDBC事务,因此,在AppConfig
中,需要再定义一个PlatformTransactionManager
对应的Bean,它的实际类型是DataSourceTransactionManager
:
1 2 3 4 5 6 7 8 9 10 11 @Configuration @ComponentScan @PropertySource("jdbc.properties") public class AppConfig { ... @Bean PlatformTransactionManager createTxManager (@Autowired DataSource dataSource) { return new DataSourceTransactionManager (dataSource); } }
Spring 声明式事务控制底层就是AOP
编程式:自己写代码实现功能
声明式:通过配置让框架实现框架实现
编程式事务控制
相关对象
PlatformTransactionManager
接口是 spring
的事务管理器,它里面提供了我们常用的操作事务的方法。例如获取事务状态、提交事务和回滚事务等。
img
PlatformTransactionManager
是接口类型,不同的 Dao
层技术则有不同的实现类
例如:
TransactionDefinition
TransactionDefinition
是事务的定义信息对象,包含了事务的隔离级别、传播行为、超时时间和是否只读等信息。
在这里插入图片描述
TransactionStatus
TransactionStatus
接口提供的是事务具体的运行状态,方法介绍如下。
在 Spring 的编程式事务管理里,TransactionStatus
对象由
PlatformTransactionManager
的 getTransaction()
方法返回。它记录了事务的当前状态,例如事务是否为新开启的、是否已完成等,并且能让开发者对事务进行控制,比如提交或者回滚事务。
常用方法
isNewTransaction()
功能 :判断当前事务是否为新开启的事务。若返回
true
,表示该事务是在此次调用 getTransaction()
方法时新开启的;若返回
false
,则说明当前事务是已存在事务的一部分(比如在事务传播行为是
REQUIRED
时,加入了已有的事务)。
hasSavepoint()
功能 :判断当前事务是否有保存点。保存点是事务中的一个标记,可让事务部分回滚到该标记处,而非整个事务回滚。
setRollbackOnly()
功能 :将当前事务标记为仅回滚。一旦调用此方法,事务在结束时会被强制回滚,即便后续代码尝试提交事务也不会生效。
isRollbackOnly()
isCompleted()
功能 :判断事务是否已经完成,即是否已经提交或者回滚。
声明式事务控制说明
使用编程的方式使用Spring事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在AppConfig
中追加一个上述定义的PlatformTransactionManager
外,再加一个@EnableTransactionManagement
就可以启用声明式事务:
1 2 3 4 5 6 7 @Configuration @ComponentScan @EnableTransactionManagement @PropertySource("jdbc.properties") public class AppConfig { ... }
然后,对需要事务支持的方法,加一个@Transactional
注解:
1 2 3 4 5 6 7 8 @Component public class UserService { @Transactional public User register (String email, String password, String name) { ... } }
或者更简单一点,直接在Bean的class
处加上,表示所有public
方法都具有事务支持:
1 2 3 4 5 @Component @Transactional public class UserService { ... }
事务隔离级别
SQL 标准定义了四种隔离级别,MySQL
全都支持。这四种隔离级别分别是:
读未提交(READ UNCOMMITTED)
读提交 (READ COMMITTED)
可重复读 (REPEATABLE READ)
串行化 (SERIALIZABLE)
从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读 是
MySQL 的默认级别。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了
4 种隔离级别对这三个问题的解决程度
img
事务传播行为
REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。
NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
NEVER:以非事务方式运行,如果当前存在事务,抛出异常
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行
REQUIRED 类似的操作
超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置
是否只读:建议查询时设置为只读
示例
假设我们有一个简单的用户账户表,需要进行转账操作。
我们需要配置 Spring 的上下文和数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/test" /> <property name ="username" value ="root" /> <property name ="password" value ="password" /> </bean > <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="accountService" class ="com.example.AccountService" > <property name ="jdbcTemplate" ref ="jdbcTemplate" /> <property name ="transactionManager" ref ="transactionManager" /> </bean > </beans >
然后,我们创建一个 AccountService
类来处理转账业务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.example;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.TransactionDefinition;import org.springframework.transaction.TransactionStatus;import org.springframework.transaction.support.DefaultTransactionDefinition;public class AccountService { private JdbcTemplate jdbcTemplate; private PlatformTransactionManager transactionManager; public void setJdbcTemplate (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } public void setTransactionManager (PlatformTransactionManager transactionManager) { this .transactionManager = transactionManager; } public void transferMoney (int fromAccountId, int toAccountId, double amount) { TransactionDefinition def = new DefaultTransactionDefinition (); TransactionStatus status = transactionManager.getTransaction(def); try { jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?" , amount, fromAccountId); jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?" , amount, toAccountId); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); e.printStackTrace(); } } }
最后,我们可以编写一个测试类来调用 AccountService
的
transferMoney
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Main { public static void main (String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext ("applicationContext.xml" ); AccountService accountService = (AccountService) context.getBean("accountService" ); accountService.transferMoney(1 , 2 , 100.0 ); } }
在这个示例中,我们使用 PlatformTransactionManager
和
TransactionDefinition
来手动管理事务。在
transferMoney
方法中,我们首先获取事务状态,然后执行数据库操作,如果操作过程中出现异常,我们会回滚事务,否则提交事务。
基于 XML 的声明式事务控制
Spring
的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在
Spring 配置文件中声明式的处理事务来代替代码式的处理事务。
声明式事务控制明确事项:
配置事务管理器与命名空间
在 applicationContext.xml
中添加事务相关命名空间及事务管理器配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx ="http://www.springframework.org/schema/tx" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/test" /> <property name ="username" value ="root" /> <property name ="password" value ="password" /> </bean > <bean id ="transactionManager" class ="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="jdbcTemplate" class ="org.springframework.jdbc.core.JdbcTemplate" > <property name ="dataSource" ref ="dataSource" /> </bean > <bean id ="accountService" class ="com.example.AccountService" > <property name ="jdbcTemplate" ref ="jdbcTemplate" /> </bean > <tx:advice id ="txAdvice" transaction-manager ="transactionManager" > <tx:attributes > <tx:method name ="transferMoney" propagation ="REQUIRED" isolation ="DEFAULT" timeout ="-1" read-only ="false" rollback-for ="Exception" /> </tx:attributes > </tx:advice > <aop:config > <aop:pointcut id ="servicePointcut" expression ="execution(* com.example.AccountService.*(..))" /> <aop:advisor advice-ref ="txAdvice" pointcut-ref ="servicePointcut" /> </aop:config > </beans >
配置项
作用
<tx:advice>
定义事务通知,指定事务管理器和事务属性
<tx:method>
配置具体方法的事务规则(如传播行为、隔离级别)
<aop:pointcut>
定义切点,匹配需要事务管理的方法
<aop:advisor>
将事务通知与切点关联,实现声明式事务控制
移除 AccountService
中的
PlatformTransactionManager
依赖和手动事务管理代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.example;import org.springframework.jdbc.core.JdbcTemplate;public class AccountService { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate (JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } public void transferMoney (int fromAccountId, int toAccountId, double amount) { jdbcTemplate.update("UPDATE accounts SET balance = balance - ? WHERE id = ?" , amount, fromAccountId); jdbcTemplate.update("UPDATE accounts SET balance = balance + ? WHERE id = ?" , amount, toAccountId); } }
事务属性详解
在 <tx:method>
标签中配置事务规则:
属性
说明
name
匹配的方法名(支持通配符,如 *
表示所有方法)
propagation
事务传播行为(默认 REQUIRED
)
isolation
事务隔离级别(默认
DEFAULT
,使用数据库默认隔离级别)
timeout
事务超时时间(单位秒,默认 -1
表示不超时)
read-only
是否只读事务(默认 false
,查询操作建议设为
true
)
rollback-for
触发回滚的异常类型(如 Exception
表示所有异常均回滚)
no-rollback-for
不触发回滚的异常类型
编写测试类验证事务是否生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Main { public static void main (String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext ("applicationContext.xml" ); AccountService accountService = (AccountService) context.getBean("accountService" ); try { accountService.transferMoney(1 , 2 , 100.0 ); } catch (Exception e) { System.err.println("事务回滚:" + e.getMessage()); } } }
基于注解的声明式事务控制
注解的方式,只需在方法上面加一个@Transaction
注解,那么方法执行之前spring会自动开启一个事务,方法执行完毕之后,会自动提交或者回滚事务,而方法内部没有任何事务相关代码,用起来特别的方便。
将会通过如下完整示例进行说明
首先创建数据库表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE TABLE IF NOT EXISTS t_account ( id INT PRIMARY KEY AUTO_INCREMENT, account_no VARCHAR (20 ) NOT NULL UNIQUE , account_name VARCHAR (50 ) NOT NULL , balance DECIMAL (10 ,2 ) NOT NULL DEFAULT 0.00 , create_time DATETIME DEFAULT CURRENT_TIMESTAMP , update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4; CREATE TABLE IF NOT EXISTS t_transaction ( id INT PRIMARY KEY AUTO_INCREMENT, from_account_id INT NOT NULL , to_account_id INT NOT NULL , amount DECIMAL (10 ,2 ) NOT NULL , transaction_time DATETIME DEFAULT CURRENT_TIMESTAMP , status VARCHAR (20 ) NOT NULL , remark VARCHAR (200 ), FOREIGN KEY (from_account_id) REFERENCES t_account(id), FOREIGN KEY (to_account_id) REFERENCES t_account(id) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4;
创建实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package edu.software.ergoutree.spring6jdbc.affairs.entity;import lombok.Data;import java.math.BigDecimal;import java.time.LocalDateTime;@Data public class Account { private Integer id; private String accountNo; private String accountName; private BigDecimal balance; private LocalDateTime createTime; private LocalDateTime updateTime; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package edu.software.ergoutree.spring6jdbc.affairs.entity;import lombok.Data;import java.math.BigDecimal;import java.time.LocalDateTime;@Data public class Transaction { private Integer id; private Integer fromAccountId; private Integer toAccountId; private BigDecimal amount; private LocalDateTime transactionTime; private String status; private String remark; }
创建 DAO 层
1 2 3 4 5 6 7 8 9 10 11 package edu.software.ergoutree.spring6jdbc.affairs.dao;import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;import java.math.BigDecimal;public interface AccountDao { Account findById (Integer id) ; Account findByAccountNo (String accountNo) ; void updateBalance (Integer id, BigDecimal balance) ; void save (Account account) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package edu.software.ergoutree.spring6jdbc.affairs.dao;import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.BeanPropertyRowMapper;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;import java.math.BigDecimal;@Repository public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Account findById (Integer id) { String sql = "SELECT * FROM t_account WHERE id = ?" ; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper <>(Account.class), id); } @Override public Account findByAccountNo (String accountNo) { String sql = "SELECT * FROM t_account WHERE account_no = ?" ; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper <>(Account.class), accountNo); } @Override public void updateBalance (Integer id, BigDecimal balance) { String sql = "UPDATE t_account SET balance = ? WHERE id = ?" ; jdbcTemplate.update(sql, balance, id); } @Override public void save (Account account) { String sql = "INSERT INTO t_account (account_no, account_name, balance) VALUES (?, ?, ?)" ; jdbcTemplate.update(sql, account.getAccountNo(), account.getAccountName(), account.getBalance()); } }
创建 Service 层
1 2 3 4 5 6 7 8 9 10 11 package edu.software.ergoutree.spring6jdbc.affairs.service;import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;import java.math.BigDecimal;public interface AccountService { void transfer (String fromAccountNo, String toAccountNo, BigDecimal amount) ; Account createAccount (Account account) ; Account getAccount (String accountNo) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package edu.software.ergoutree.spring6jdbc.affairs.service;import edu.software.ergoutree.spring6jdbc.affairs.dao.AccountDao;import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Override @Transactional( propagation = Propagation.REQUIRED, rollbackFor = Exception.class, // 任何异常都会触发回滚。 timeout = 30 ) public void transfer (String fromAccountNo, String toAccountNo, BigDecimal amount) { Account fromAccount = accountDao.findByAccountNo(fromAccountNo); if (fromAccount == null ) { throw new RuntimeException ("转出账户不存在" ); } Account toAccount = accountDao.findByAccountNo(toAccountNo); if (toAccount == null ) { throw new RuntimeException ("转入账户不存在" ); } if (fromAccount.getBalance().compareTo(amount) < 0 ) { throw new RuntimeException ("账户余额不足" ); } BigDecimal fromBalance = fromAccount.getBalance().subtract(amount); BigDecimal toBalance = toAccount.getBalance().add(amount); accountDao.updateBalance(fromAccount.getId(), fromBalance); accountDao.updateBalance(toAccount.getId(), toBalance); } @Override @Transactional(propagation = Propagation.REQUIRED) public Account createAccount (Account account) { accountDao.save(account); return account; } @Override @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public Account getAccount (String accountNo) { return accountDao.findByAccountNo(accountNo); } }
我们对需使用事务的目标上加@Transaction
注解
@Transaction放在接口上 ,那么接口的实现类中所有public都被spring自动加上事务
@Transaction放在类上 ,那么当前类以及其下无限级子类中所有pubilc方法将被spring自动加上事务
@Transaction放在public方法上 ,那么该方法将被spring自动加上事务
注意:@Transaction只对public方法有效
下面我们看一下@Transactional 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value () default "" ; @AliasFor("value") String transactionManager () default "" ; Propagation propagation () default Propagation.REQUIRED; Isolation isolation () default Isolation.DEFAULT; int timeout () default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly () default false ; Class<? extends Throwable >[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable >[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
参数介绍
参数
描述
value
指定事务管理器的bean名称,如果容器中有多事务管理器PlatformTransactionManager,那么你得告诉spring,当前配置需要使用哪个事务管理器
transactionManager
同value,value和transactionManager选配一个就行,也可以为空,如果为空,默认会从容器中按照类型查找一个事务管理器bean
propagation
事务的传播属性,下篇文章详细介绍
isolation
事务的隔离级别,就是制定数据库的隔离级别,数据库隔离级别大家知道么?不知道的可以去补一下
timeout
事务执行的超时时间(秒),执行一个方法,比如有问题,那我不可能等你一天吧,可能最多我只能等你10秒
10秒后,还没有执行完毕,就弹出一个超时异常吧
readOnly
是否是只读事务,比如某个方法中只有查询操作,我们可以指定事务是只读的
设置了这个参数,可能数据库会做一些性能优化,提升查询速度
rollbackFor
定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常及其子类异常的时候,spring会让事务回滚
如果不配做,那么默认会在 RuntimeException 或者 Error
情况下,事务才会回滚
rollbackForClassName
同 rollbackFor,只是这个地方使用的是类名
noRollbackFor
定义零(0)个或更多异常类,这些异常类必须是Throwable的子类,当方法抛出这些异常的时候,事务不会回滚
noRollbackForClassName
同 noRollbackFor,只是这个地方使用的是类名
通过 @Transactional 注解,Spring
会自动管理事务的开始、提交和回滚。在@Transaction标注类或者目标方法上执行业务操作,此时这些方法会自动被spring进行事务管理
在 transfer
方法中,如果在转账过程中出现任何异常,事务会自动回滚,确保数据的一致性
下面讲解一些出现的注解:
@Transactional
注解 :用于声明事务的属性。
propagation
:事务传播行为
Propagation.REQUIRED
表示如果当前没有事务,则创建一个新事务;如果已经存在一个事务,则加入该事务。
Propagation.SUPPORTS
表示如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。
rollbackFor
:指定哪些异常会触发事务回滚,这里指定为
Exception.class
表示任何异常都会触发回滚。
timeout
:指定事务的超时时间,单位为秒。
readOnly
:指定事务是否为只读事务,用于提高查询性能。
isolation
:指定事务的隔离级别
扩展事务功能
嵌套事务与传播行为 REQUIRES_NEW
场景: 日志记录操作需要独立事务,不受主事务回滚影响。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Service public class LogService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional(propagation = Propagation.REQUIRES_NEW) public void logTransaction (String fromAccountNo, String toAccountNo, BigDecimal amount) { String sql = "INSERT INTO t_transaction (from_account_id, to_account_id, amount, status) VALUES (?, ?, ?, ?)" ; jdbcTemplate.update(sql, fromAccountNo, toAccountNo, amount, "PROCESSING" ); } } @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public void transfer (String fromAccountNo, String toAccountNo, BigDecimal amount) { logService.logTransaction(fromAccountNo, toAccountNo, amount); }
控制隔离级别和只读事务的配置
1 2 3 4 5 6 7 8 9 10 11 @Transactional(isolation = Isolation.READ_COMMITTED) public BigDecimal getAccountBalance (String accountNo) { return accountDao.findByAccountNo(accountNo).getBalance(); } @Transactional(readOnly = true) public List<Transaction> getTransactionHistory (String accountNo) { return accountDao.getTransactionHistory(accountNo); }
注解优先级和类级的配置
1 2 3 4 5 6 7 8 @Service @Transactional(isolation = Isolation.READ_COMMITTED, timeout = 10) public class AccountServiceImpl implements AccountService { @Override @Transactional(isolation = Isolation.SERIALIZABLE, timeout = 30) public void highConcurrencyTransfer (...) { ... } }
若容器中存在多个事务管理器,需通过 transactionManager
指定:
1 2 @Transactional(transactionManager = "orderTransactionManager") public void createOrder (...) { ... }
新建一个测试类测试上述代码能否正常执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package edu.software.ergoutree.spring6jdbc.affairs;import edu.software.ergoutree.spring6jdbc.affairs.entity.Account;import edu.software.ergoutree.spring6jdbc.affairs.service.AccountService;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest @Transactional public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer () { Account account1 = new Account (); account1.setAccountNo("1001" ); account1.setAccountName("张三" ); account1.setBalance(new BigDecimal ("1000.00" )); accountService.createAccount(account1); Account account2 = new Account (); account2.setAccountNo("1002" ); account2.setAccountName("李四" ); account2.setBalance(new BigDecimal ("500.00" )); accountService.createAccount(account2); accountService.transfer("1001" , "1002" , new BigDecimal ("200.00" )); Account updatedAccount1 = accountService.getAccount("1001" ); Account updatedAccount2 = accountService.getAccount("1002" ); assertEquals(new BigDecimal ("800.00" ), updatedAccount1.getBalance()); assertEquals(new BigDecimal ("700.00" ), updatedAccount2.getBalance()); } @Test public void testTransferWithInsufficientBalance () { Account account1 = new Account (); account1.setAccountNo("1003" ); account1.setAccountName("王五" ); account1.setBalance(new BigDecimal ("100.00" )); accountService.createAccount(account1); Account account2 = new Account (); account2.setAccountNo("1004" ); account2.setAccountName("赵六" ); account2.setBalance(new BigDecimal ("500.00" )); accountService.createAccount(account2); assertThrows(RuntimeException.class, () -> { accountService.transfer("1003" , "1004" , new BigDecimal ("200.00" )); }); Account updatedAccount1 = accountService.getAccount("1003" ); Account updatedAccount2 = accountService.getAccount("1004" ); assertEquals(new BigDecimal ("100.00" ), updatedAccount1.getBalance()); assertEquals(new BigDecimal ("500.00" ), updatedAccount2.getBalance()); } }
另外可以操作如下测试方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testNestedTransaction () { Account account1 = accountService.createAccount(...); Account account2 = accountService.createAccount(...); try { accountService.transfer("1001" , "1002" , new BigDecimal ("200.00" )); } catch (Exception e) { Account updatedAccount1 = accountService.getAccount("1001" ); assertEquals(1000.00 , updatedAccount1.getBalance()); List<Transaction> logs = logService.getTransactionHistory("1001" ); assertFalse(logs.isEmpty()); } }