Spring声明式事务以及事务传播行为
- Spring声明式事务
- 1.编程式事务
- 2.使用AOP改造编程式事务
- 3.Spring声明式事务
- 事务传播行为
如果对数据库事务不太熟悉,可以阅读上一篇博客简单回顾一下:
MySQL事务以及并发访问隔离级别
Spring声明式事务
-
事务一般添加到JavaEE三层结构中的service层(业务逻辑层)
-
在Spring进行事务管理操作的两种方式
-
编程式事务管理
-
声明式事务管理
-
**转账案例代码准备:**按照以下代码配置完成之后可以进行数据库的操作,但不涉及事务。
-
数据库
create database spring_db; use spring_db; create table account( id int primary key auto_increment, name varchar(20), money double ); insert into account values(null,'jack',1000),(null,'rose',1000);
-
导入Maven依赖
<dependencies> <!--Spring核心包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!-- 切入点表达式 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> <!--mybatis的包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <!--Druid数据库连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <!--数据库驱动包,我的MySQL版本是8.0.33--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!--spring整合mybatis,需要下面两个jar包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <!-- 引入单元测试的jar包(需要在 4.12以上) --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> </dependency> <!-- Spring整合junit的jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>
-
jdbc.properties配置文件
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring_db jdbc.username=root jdbc.password=123456
-
MyBatisConfig配置类
public class MyBatisConfig { @Bean public SqlSessionFactoryBean getSqlSessionFactoryBean(DataSource ds){ SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); //设置pojo的包扫描 factoryBean.setTypeAliasesPackage("top.codermao.domain"); //设置连接池 factoryBean.setDataSource(ds); return factoryBean; } @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc = new MapperScannerConfigurer(); //设置dao层的接口扫描 msc.setBasePackage("top.codermao.dao"); return msc; } }
-
Spring配置类
@Configuration @ComponentScan("top.codermao") @PropertySource("classpath:jdbc.properties") @Import(MyBatisConfig.class) public class SpringConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource getDataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
-
dao层接口
public interface AccountDao { //转出 @Update("update account set money = money - #{money} where id = #{outId}") int outMoney(@Param("outId") int outId, @Param("money")double money); //转入 @Update("update account set money = money + #{money} where id = #{inId}") int inMoney(@Param("inId") int inId, @Param("money")double money); }
-
domain包下实体类
public class Account implements Serializable { private Integer id; private String name; private Double money; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } @Override public String toString() { return "Account{" + "id=" + id + ", name='" + name + '\'' + ", money=" + money + '}'; } }
-
service层接口
public interface AccountService { //转账业务 void transfer(int outId,int inId,double money); }
-
service层实现类
@Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao dao; @Override public void transfer(int outId, int inId, double money) { try { dao.outMoney(outId, money); //可能在转账过程中发生意外: 转出执行,转入还未执行 // int i = 1/0; dao.inMoney(inId, money); } catch (Exception e) { e.printStackTrace(); } } }
-
WebApp类充当Controller层,负责数据的发送和接收
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class WebApp { @Autowired private AccountService service; @Test public void test01(){ service.transfer(1, 2, 200); } }
1.编程式事务
在学习声明式事务之前我们需要了解编程式事务,因为声明式事务是Spring对编程式事务的封装。
- 所谓编程式事务是指用Spring中事务相关的API用硬编码方式来实现事务
- 缺点
- 事务管理代码和业务代码耦合严重
- 后续添加其他业务方法还要重新编写事务代码,冗余
- 缺点
所以学习编程式事务,就是学习Spring事务管理相关API。
步骤一:创建事务管理器
# PlatformTransactionManager(平台事务管理器)
1. 这是一个接口,以下是实现类
1). - DataSourceTransactionManager (重点!!!)
适用于Spring JDBC或MyBatis
2). - HibernateTransactionManager
适用于Hibernate3.0及以上版本
3). - JpaTransactionManager
适用于JPA (Java EE 标准之一,为POJO提供持久化标准规范,并规范了持久化开发的统一API,符合JPA规范的开发可以在不同的JPA框架下运行)
2. 此接口定义了事务的基本操作
1). 获取事务 :
TransactionStatus getTransaction(TransactionDefinition definition)
2). 提交事务 :
void commit(TransactionStatus status)
3). 回滚事务 :
void rollback(TransactionStatus status)
步骤二:定义事务属性TransactionDefinition
# TransactionDefinition(定义事务属性)
1. 实现类
DefaultTransactionDefinition
2. 此接口定义了事务的基本信息
//2. 创建事务定义对象
DefaultTransactionDefinition td = new DefaultTransactionDefinition();
/*
设置事务隔离级别
0). spring默认隔离级别是跟数据库软件一致
1). mysql默认是REPEATABLE_READ
2). oracle默认是READ_COMMITTED
*/
td.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
/*
设置是否只读
1). false,表示读写均可(默认设置,适合增删改操作)
2). true,表示只读(适合查,效率高)
*/
td.setReadOnly(false);
/*
设置超时时间
1). 默认值是-1, 表示永不超时
2). 单位是秒
*/
td.setTimeout(10);
/*
设置事务传播行为
1. 一般增删改:REQUIRED (默认值)
2. 一般查询 SUPPORTS
*/
td.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
步骤三:开启事务
# TransactionStatus(接口)
`public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException`
调用事务管理器的getTransaction方法,即可开启一个事务
这个方法会返回一个TransactionStatus表示事务状态的一个对象,通过TransactionStatus提供的一些方法可以用来控制事务的一些状态,比如事务最终是需要回滚还是需要提交。
- 获取事务是否处于新开启事务状态
- boolean isNewTransaction()
- 获取事务是否处于已完成状态
- boolean isCompleted()
- 获取事务是否处于回滚状态
- boolean isRollbackOnly()
- 刷新事务状态
- void flush()
- 获取事务是否具有回滚存储点
- boolean hasSavepoint()
- 设置事务处于回滚状态
- void setRollbackOnly()
步骤四:执行业务操作
- 书写业务逻辑代码
步骤五:提交 or 回滚
-
无异常发生,提交
dstm.commit(ts);
-
有异常发生,回滚
dstm.rollback(ts);
对service层AccountServiceImpl的transfer方法添加编程式事务
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao dao;
@Autowired
private DataSource dataSource;
@Override
public void transfer(int outId, int inId, double money) {
//1.创建事务管理器
DataSourceTransactionManager dstm = new DataSourceTransactionManager();
//为事务管理器添加与数据层相同的数据源
dstm.setDataSource(dataSource);
//2.创建事务定义对象,设置隔离级别、传播特性、超时时间...
DefaultTransactionDefinition td = new DefaultTransactionDefinition();
/*
设置事务隔离级别
0). spring默认隔离级别是跟数据库软件一致 (ISOLATION_DEFAULT)
1). mysql默认是REPEATABLE_READ
2). oracle默认是READ_COMMITTED
*/
td.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
/*
设置是否为只读事务
1). false,表示读写均可(默认设置,适合增删改操作)
2). true,表示只读(适合查,效率高)
*/
td.setReadOnly(false);
/*
设置超时时间
1). 默认值是-1, 表示永不超时
2). 单位是秒
*/
td.setTimeout(10);
/*
设置事务传播行为
1. 一般增删改:REQUIRED (默认值)
2. 一般查询 SUPPORTS
*/
td.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//3.创建事务状态对象,用于控制事务执行(了解) -> 相当于开启事务
TransactionStatus ts = dstm.getTransaction(td);
try {
dao.outMoney(outId, money);
//可能在转账过程中发生意外: 转出执行,转入还未执行
// int i = 10/0;
dao.inMoney(inId, money);
dstm.commit(ts);//成功,提交
} catch (Exception e) {
e.printStackTrace();
dstm.rollback(ts);//失败,回滚
}
}
}
2.使用AOP改造编程式事务
硬编码方式添加事务耦合度比较高,AOP面向切面编程是动态增强方法,如果引入AOP则可以将业务代码和事务代码分开,实现解耦。
使用AOP处理编程式事务解决了耦合问题。。但是还是有些不够完美。。AOP处理不具备
特例性
,任何业务添加事务都是一样的操作,对某些事务可能对事务属性有一些独特的设置。
步骤一:在SpringConfig上添加开启AOP的注解
...
@EnableAspectJAutoProxy
public class SpringConfig {
...
}
步骤二:AccountServiceImpl中transfer中只需要书写业务代码
- 注意:注意: 在aop使用中,切入点方法transfer千万不能自己catch异常
@Service
public class AccountServiceImpl implements AccountService {
/*
注意: 在aop使用中,切入点方法千万不能自己catch异常
原因: 如果切入点自己catch了异常,那么通知中是调用切入点的地方是不会感知到异常,就不会执行catch了
(相当于异常通知失效)
解决方案:
A方案: 有异常直接抛出,不要catch
B方案: 可以catch,但是再new一个异常抛出
*/
@Override
public void transfer(int outId, int inId, double money) {
dao.outMoney(outId, money);
//可能在转账过程中发生意外: 转出执行,转入还未执行
//int i = 1/0;
dao.inMoney(inId, money);
}
}
步骤三:添加TxAdvice
- 将编程式事务中对事务操作的代码抽取到TxAdvice切面类中
package top.codermao.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import javax.sql.DataSource;
@Component
@Aspect
public class TxAdvice {
@Autowired
private DataSource dataSource;
@Pointcut("execution(* top.codermao.service.*Service.transfer(..))")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp){
Object result = null;
//1. 创建事务管理器
DataSourceTransactionManager dstm = new DataSourceTransactionManager();
//为事务管理器设置与数据层相同的数据源!!!
dstm.setDataSource(dataSource);
//2. 创建事务定义对象 : 隔离级别/传播特性/超时时间...
DefaultTransactionDefinition td = new DefaultTransactionDefinition();
td.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
td.setReadOnly(false);
td.setTimeout(10);
td.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//3.创建事务状态对象,用于控制事务执行(了解) -> 相当于开启事务
TransactionStatus ts = dstm.getTransaction(td);
try{
result = pjp.proceed();
dstm.commit(ts);//成功,提交
} catch (Throwable e) {
e.printStackTrace();
dstm.rollback(ts);//失败,回滚
System.out.println("aop改造编程式事务");
}
return result;
}
}
3.Spring声明式事务
spring底层封装了事务切面类TxAdvice, 让开发者声明配置即可用
步骤一:在SpringConfig类上添加注解开启Spring事务管理支持
...
@EnableTransactionManagement
public class SpringConfig {
...
}
步骤二:在SpringConfig类中配置事务管理器
...
@EnableTransactionManagement
public class SpringConfig {
...
@Bean
public DataSourceTransactionManager getTxManager(DataSource dataSource){
//这里使用DataSourceTransactionManager,因为使用的是MyBatis
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource);
return manager;
}
}
步骤三:在需要添加事务的方法上添加@Transactional注解
使其成为切入点
-
@Transactional注解属性
- 属性都有默认值
-
@Transactional放置位置不同,效果不同
- 如果放在类的方法上,说明当前方法是切入点
- 如果放在类上,说明当前类的所有方法是切入点
- 如果放在接口的方法上,说明此方法的所有重写方法是切入点 (常用)
- 如果放在接口上,说明此接口的所有实现类的所有方法都是切入点 (常用)
事务传播行为
事务传播行为:指的就是当一个事务方法B被另一个事务方法A调用时,这个事务方法B应该对待A的事务态度。(B是自己开启一个新事务,还是融入A的事务,或者不添加事务,或者…)
Spring事务角色 事务管理员 + 事务协调员
事务管理员一般是业务层,事务A
事务协调员一般是数据层,事务B
再次翻译下事务传播行为:事务传播行为是指事务协调员对于事务管理员的态度@Transactional中的propagation属性
下图来源于:B站视频,点击进入,,我觉得讲的很好,,建议大家去瞅瞅。。
案例:往面转账案例添加一个记录日志的功能,要求转账成功之后,要给account_log表插入谁向谁转了多少钱
-
添加一个数据表
create table account_log( out_id int, in_id int, money double );
-
dao层接口添加方法
public interface AccountDao { //转出 @Update("update account set money = money - #{money} where id = #{outId}") int outMoney(@Param("outId") int outId, @Param("money")double money); //转入 @Update("update account set money = money + #{money} where id = #{inId}") int inMoney(@Param("inId") int inId, @Param("money")double money); //记录日志 @Insert("insert into account_log values(#{outId},#{inId},#{money})") void insertLog(@Param("outId") int outId, @Param("inId") int inId,@Param("money") double money); }
-
service层的AccountServiceImpl修改为
@Service public class AccountServiceImpl02 implements AccountService { @Autowired private AccountDao dao; @Autowired private DataSource dataSource; /** 转账事务 */ @Transactional( propagation = Propagation.REQUIRED ) public void s1(int outId,int inId,double money){ dao.outMoney(outId,money); //可能在转账过程中发生意外: 转出执行,转入还未执行 dao.inMoney(inId,money); } /** 记录日志事务 */ @Transactional( propagation = Propagation.REQUIRES_NEW ) public void s2(int outId,int inId,double money){ dao.insertLog(outId,inId,money); } /** 业务逻辑: 1). 如果转账S1操作失败了, S1需要回滚(S1的操作肯定需要事务) 如果S1有事务,S1跟随即可, 如果S1没有事务,S1需要自己创建事务 所以S1适合设置传播行为属性为REQUIRED 2). 如果S1成功,S2向数据库中插入日志 如果转账S1操作失败了, S2不需要回滚,也向数据库中插入日志 所以S2适合设置传播行为属性为REQUIRES_NEW */ @Transactional @Override public void transfer(int outId, int inId, double money) { s1(outId,inId,money); // int i = 10 / 0; s2(outId,inId,money); } }