六、Spring 事务管理
1.Spring 事务简介
-
事务概述:保证数据库操作同时成功或者同时失败
-
Spring 事务的概述:在数据层保证数据库操作同时成功或者同时失败
2.转账案例分析
- 转账肯定有一个业务方法:给转出用户减钱,给转入用户加钱
- 要求:
- 要么同时成功要么同时失败
2.1Spring 平台事务管理器
-
提供规范接口
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException; void commit(TransactionStatus var1) throws TransactionException; void rollback(TransactionStatus var1) throws TransactionException; }
-
方法
- void commit(TransactionStatus var1):用于提交事务
- void rollback(TransactionStatus var1):用户事务回滚
-
具体实现:DataSourceTransactionManager 来实现的,通过DataSource dataSource 以 JDBC 事务的方式来控制事务
2.2转账案例分析
-
业务分析
- 在业务层需要保证事务的同时成功或者同时失败
- 结果
- 出现异常:张三转账给李四,比如中途出现问题,张三的钱和李四的钱应该不出现误差
- 没有出现异常:张三转账给李四,没有出现问题,张三的钱正常减少,李四的钱正常增多
-
提供service 方法
public interface IAccountService { /** * 实现转账操作 * @param srcId 转账人 * @param deskId 接收人 * @param money 转账金额 */ public void transfer(Long srcId,Long deskId,int money); }
-
下载插件的官方地址 https://plugins.jetbrains.com/
-
提供service 实现方法
@Service public class AccountServiceImpl implements IAccountService { //会使用到 mapper @Autowired private AccountMapper mapper; public void transfer(Long srcId, Long deskId, int money) { mapper.outAccount(srcId,money);//转账扣钱 mapper.inAccount(deskId,money);//接收转账钱 } }
-
提供 mapper 接口
public interface AccountMapper { void outAccount(@Param("id") Long srcId, @Param("money") int money); void inAccount(@Param("id")Long deskId,@Param("money") int money); }
-
提供 mapper.xml
</update> <update id="outAccount"> update account set money = money-#{money,jdbcType=INTEGER} where id = #{id,jdbcType=BIGINT} </update> <update id="inAccount"> update account set money = money+#{money,jdbcType=INTEGER} where id = #{id,jdbcType=BIGINT} </update>
-
测试
@Test public void testMybatis(){ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); IAccountService bean = context.getBean(IAccountService.class); bean.transfer(1L,2L,500); }
3.基于注解的方式实现
3.1@EnableTransactionManagement
-
@EnableTransactionManagement:用于开启事务支持的,直接添加到spring 配置类
-
说明
名称 @EnableTransactionManagement 位置 配置类上方 作用 设置当前spring环境支持事务 -
修改配置类
@Configuration @ComponentScan("cn.sycoder") @PropertySource("db.properties") @Import({JdbcConfig.class,MyBatisConfig.class}) //开启事务支持 @EnableTransactionManagement public class SpringConfig { }
3.2 配置事务管理,配置数据源
-
如果不配置的话会报:找不到数据源错误
-
PlatformTransactionManager
-
代码
public class JdbcConfig { @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.driverClassName}") private String driverClassName; @Value("${jdbc.url}") private String url; //配置连接池 @Bean public DataSource dataSource(){ DruidDataSource source = new DruidDataSource(); source.setUsername(username); source.setPassword(password); source.setDriverClassName(driverClassName); source.setUrl(url); return source; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager manager = new DataSourceTransactionManager(); manager.setDataSource(dataSource); return manager; } }
3.3 @Transactional
-
@Transactional:为业务添加事务的
-
说明
名称 @Transactional 位置 业务层接口上方,或者实现类上方,或者具体业务方法上方 作用 为当前的业务方法添加事务支持 -
修改业务层
-
业务方法上添加
@Transactional public void transfer(Long srcId, Long deskId, int money) { mapper.outAccount(srcId,money);//转账扣钱 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 }
-
业务类上添加
@Service @Transactional public class AccountServiceImpl implements IAccountService { //会使用到 mapper @Autowired private AccountMapper mapper; public void transfer(Long srcId, Long deskId, int money) { mapper.outAccount(srcId,money);//转账扣钱 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 } }
-
接口层添加
@Transactional public interface IAccountService { /** * 实现转账操作 * @param srcId 转账人 * @param deskId 接收人 * @param money 转账金额 */ public void transfer(Long srcId,Long deskId,int money); }
4.事务角色
-
-
在没有开启Spring事务之前:两条语句分别开启两个事务 T1 和 T2
- 如果同时成功,T1和T2都会正常提交
- 如果T1正常,T2之前抛出异常,就会出现T1能够正常转账,但是T2收不到钱,因为不是同一个事务导致金钱异常
public void transfer(Long srcId, Long deskId, int money) { mapper.outAccount(srcId,money);//转账扣钱 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 }
-
开启Spring 事务管理之后
- 在转账 transfer 方法上加入 @Transactional 注解之后,该方法会新建一个事务T
- 把 mapper 中 outAccount 事务 T1 加入到 事务T中,把 mapper 中 inAccount 事务 T2 也加入到事务T中
- 通过 @Transactional 注解统一了 transfer 方法的事务保证转账和入账方法变成同一事务操作
4.1事务管理员&事务协调员
- 事务管理员:发起新事务,使用 @Transactional 注解开启事务
- 事务协调员:加入新事务,保证多个事务变成同一事务下的操作
5.@Transactional 属性
5.1 readOnly
-
概述:表示只读,没有写操作。可以通过这个属性告诉数据库我们没有写操作,从而数据库可以针对只读sql做优化操作
-
使用
@Transactional(readOnly = true) public Account selectById(Long id){ return mapper.selectByPrimaryKey(id); }
-
如果对于有写操作的使用这个属性,会报如下错误
5.2 timeout
-
超时概述:事务再执行的时候,由于某些原因卡住,长时间占用数据库资源。此时很可能程序sql有问题,希望撤销事务,能够让事务结束,释放资源,即超时回滚。
-
默认值是-1.-1表示用不回滚,单位是秒
-
int timeout() default -1;
-
使用
@Transactional(readOnly = true,timeout = 1) public Account selectById(Long id){ try { Thread.sleep(10000L); } catch (InterruptedException e) { e.printStackTrace(); } return mapper.selectByPrimaryKey(id); }
5.3 rollbackFor&rollbackForClassName
-
回滚概述:回滚策略,希望对于什么样的异常回顾
-
注意:并不是所有的异常 Spring 都会回滚,Spring 只对 Error 异常和 RuntimeException 异常回滚
-
使用
@Transactional(rollbackFor = IOException.class) public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱 if(true){ throw new IOException(""); } mapper.inAccount(deskId,money);//接收转账钱 }
@Transactional(rollbackForClassName = "IOException") public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱 if(true){ throw new IOException(""); } mapper.inAccount(deskId,money);//接收转账钱 }
5.4 noRollbackFor&noRollbackForClassName
-
不会滚概述:出现这个异常不回滚
-
使用
@Transactional(noRollbackFor = ArithmeticException.class) public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 }
@Transactional(noRollbackForClassName = "ArithmeticException") public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 }
5.5 isolation
-
概述:设置事务隔离级别;
-
如果不记得事务隔离级别,回去复习一下我讲的MySql
- DEFAULT :默认隔离级别, 会采用数据库的隔离级别
- READ_UNCOMMITTED : 读未提交
- READ_COMMITTED : 读已提交
- REPEATABLE_READ : 重复读取
- SERIALIZABLE: 串行化
-
使用
@Transactional(isolation = Isolation.REPEATABLE_READ) public Account selectById(Long id) throws IOException { return mapper.selectByPrimaryKey(id); }
5.6propagation
-
事务传播行为:事务协调员对事务管理员所携带的事务的处理态度
-
说明
传播属性 说明 REQUIRED 外围方法会开启新事务,内部方法会加入到外部方法的事务中 SUPPORTS 外围方法没有事务,则内部方法不执行事务 MANDATORY 使用当前事务,如果当前没有事务就抛异常 REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起 NOT_SUPPORTED 不支持事务 NEVER 不支持事务,如果存在事务还会抛异常 NESTED 如果当前存在事务,则在嵌套事务内执行,如果不存在,执行REQUIRED类似操作 -
实操
-
REQUIRED:T1和T2会加入T中
@Transactional(propagation = Propagation.REQUIRED)//事务T public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱 //事务T1 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱 //事务T2 }
-
SUPPORTS:外围没事务,所以内部只执行自己的事务,T1 和 T2 单独执行
public void transfer(Long srcId, Long deskId, int money) throws IOException { mapper.outAccount(srcId,money);//转账扣钱//事务T1 System.out.println(1/0); mapper.inAccount(deskId,money);//接收转账钱//事务T2 }
-
REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起
@Transactional(propagation=Propagation.REQUIRES_NEW) public void outAccount(Long id, int money){ mapper.outAccount(id,money);//转账扣钱 } @Transactional(propagation=Propagation.REQUIRES_NEW) public void inAccount(Long id, int money){ mapper.inAccount(id,money);//转账扣钱 } public void transfer(Long srcId, Long deskId, int money) throws IOException { outAccount(srcId,money); inAccount(deskId,money); throw new RuntimeException(); }
- 这种情况上面一条语句能够正常执行
@Transactional(propagation = Propagation.REQUIRES_NEW) public void outAccount(Long id, int money) { mapper.outAccount(id, money);//转账扣钱 } @Transactional(propagation = Propagation.REQUIRES_NEW) public void inAccount(Long id, int money) { if (true) throw new RuntimeException(); mapper.inAccount(id, money);//转账扣钱 }
-
6.基于XML事务
-
导入依赖
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.17.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.2.17.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <!-- <scope>test</scope>--> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!-- spring 整合 mybatis 的包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <!-- mybatis 包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <!-- spring 操作 jdbc 包--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.29.RELEASE</version> </dependency> </dependencies>
-
配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" 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 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <bean id="accountService" class="cn.sycoder.service.impl.AccountServiceImpl"> <!-- <property name="mapper" ref="mapper"/>--> </bean> <!-- <bean id="mapper" class="cn.sycoder.mapper.AccountMapper"></bean>--> <aop:config> <aop:advisor advice-ref="tx" pointcut="execution(* cn.sycoder.service.impl.*.*(..))"></aop:advisor> </aop:config> <tx:advice id="tx" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- (this dependency is defined somewhere else) --> <property name="dataSource" ref="dataSource"/> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <context:property-placeholder location="db.properties"/> </beans>
-
配置详解
-
引入db.properties
<context:property-placeholder location="db.properties"/>
-
配置连接池
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
-
配置事务管理器
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
-
配置 aop 事务增强
<aop:config> <aop:advisor advice-ref="tx" pointcut="execution(* cn.sycoder.service.impl.*.*(..))"/> </aop:config> <tx:advice id="tx" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice>
-
-
注意:如果你还想通过 xml 配置 mybatis ,那么你还需要把 mybatis 配置文件搞一份过来,通过xml 配置好 mybatis 之后,然后再获取 sqlSessionFactory 去获取 mapper 文件
-
注意:spring 是面试重头戏,所以,你需要花时间认真巩固和复习,ioc 和 di 特别是对于常用注解,以及事务机制,aop 等都很爱问。