五、Spring事务管理
5.1 梗概
事务,其实这个概念是数据库的知识点,但这里还是讲一下,他是数据库操作的最基本单元,逻辑上一组操作,要么都成功,要么有一个失败,所有的操作就都失败。这个时候我们就需要进行回滚(刚才的加成功的减掉,减成功地加上。。。。,回到刚才没有执行这个事务之前的状态)。
典型的案例:银行转账 现在,Mike 要给 Amy 转520,那么 Mike 的账户要 -520 Amy 的账户要 +520 这两个操作要都成功,这个事务才算完成,如果Mike的钱-520之后,银行的服务器恰好断电了,导致Amy的账户没能加上这520,那么这个事务就会失败,然后回滚,把520 再给Mike加上。
事务有四个特性(ACID):
- 原子性:他是最小的执行单元
- 一致性:简单说就是现在Mike少了520,那么Amy就是多520,我称之为 守恒
- 隔离性:多事务操作的时候,事务之间互不影响
- 持久性:操作成功后,数据会修改并保存在数据库,不像变量用完就被回收了
5.2 环境搭建
在Spring框架中开发的时候,我们通常把系统分成三个层,一个是用户操作的Web层,然后是Web层直接调用的Service层(主要实现业务操作),Service层再去调用我们的Dao层(主要执行数据库操作,不写业务)。
下面,我们以事务的典型案例转账为例,基本思想图解如下: (1)在Dao层写 加钱 和 减钱 两个方法 (2) 然后在Service层调用这两个方法 (3)最后我们用测试类模拟web点击操作的过程  废话话不多说,咱们实际操作案例来一波: 先在数据库中建表 (表名:account):   之后配置我们的jar包,添加(pom.xml):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
然后写我们的jdbc配置文件(jdbc.properties):
prop.driverClass=com.mysql.cj.jdbc.Driver
prop.url=jdbc:mysql://localhost:3306/user_db?&useSSL=false&serverTimezone=UTC
prop.username=root
prop.password=root
再写我们的配置文件:
<?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: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/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--开启注解扫描-->
<context:component-scan base-package="com.example"></context:component-scan>
<!--引入外部属性文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--配置数据库连接池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${prop.url}"/>
<property name="username" value="${prop.username}"/>
<property name="password" value="${prop.password}"/>
<property name="driverClassName" value="${prop.driverClass}"/>
</bean>
<!--JdbcTemplate对象-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!--注入dataSource-->
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
写完这些,就可以开始写我们的主逻辑代码了: 先写我们的dao层: AccountDao.java
package com.example.dao;
public interface AccountDao {
public void addMoney(String id, Integer money);
public void reduceMoney(String id, Integer money);
}
AccountDaoImpl.java
package com.example.dao.impl;
import com.example.dao.AccountDao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
@Repository
public class AccountDaoImpl implements AccountDao {
@Resource
private JdbcTemplate jdbcTemplate;
@Override
public void addMoney(String id, Integer money) {
String sql = "update account set money = money + ? where id = ?";
Object[] args = {money, id};
jdbcTemplate.update(sql,args);
System.out.println("加钱操作完成");
}
@Override
public void reduceMoney(String id, Integer money) {
String sql = "update account set money = money - ? where id = ?";
Object[] args = {money, id};
jdbcTemplate.update(sql, args);
System.out.println("减钱操作完成");
}
}
然后写我们的Service层: AccountService:
package com.example.service;
import com.example.dao.AccountDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class AccountService {
@Resource
private AccountDao accountDao;
public void transferMoney(String id_1, String id_2, Integer money){
accountDao.reduceMoney(id_1, money);
accountDao.addMoney(id_2,money);
System.out.println("转账事务执行成功");
}
}
最后是我们的测试类:
package com.example.test;
import com.example.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
@Test
public void test_1(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.transferMoney("1","2",520);
}
}
运行结果如下:(可见Mike的爱心520,Amy已收到) 
5.3 事务场景引入
上面这么做,乍一看好像没毛病。但是,前面说过了,如果突然出现服务器断电等等情况,Mike的520可能就不翼而飞了。我们用异常来模拟一下断电情况:  修改完这个业务逻辑代码后,我们再次运行测试方法会发现: Mike减钱的操作执行了,但是Amy由于异常情况收不到Mike的钱,这么搞,问题就大条了!!!   那么,我们该怎么补救呢?这就需要用到我们的事务管理了。
5.4 Spring事务管理
对事务的操作分为以下四个步骤: 1、开启事务 2、编写业务逻辑代码 3-1、没有发生异常,提交事务 3-2、发生异常,回滚操作
另外,Spring事务管理API 针对不同的框架提供不同的实现类。 使用jdbc或者mybatis的时候,我们使用的是DataSourceTransactionManager实现类 使用hibernate的话,我们使用的是HibernateTransactionManager实现类。  在Spring中进行事务管理操作有两种方式,一种是编程式事务管理(繁琐,一般不会这么用,这里不做过多介绍),一种是声明式事务管理(常用),下面将就声明式事务管理 使用jdbc的形式给出详细的代码和解析。
5.5 声明式事务管理(基于注解方式)
声明式事务管理,底层使用AOP原理,在方法前后分别切入 开启事务管理 和 提交事务。
Spring框架使用声明式事务管理分成以下几个步骤: 1、在配置文件中创建事务管理器 2、在配置文件中开启事务注解(需要引入tx名称空间) 3、在Service类或类方法上添加@Transactional 事务注解
回归正题,我们首先要在配置文件中创建事务管理器。 在applicationContext.xml中添加代码如下:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
然后还需要开启事务注解(记得在最前面添加tx名称空间):
<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.example"></context:component-scan>
<context:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${prop.url}"/>
<property name="username" value="${prop.username}"/>
<property name="password" value="${prop.password}"/>
<property name="driverClassName" value="${prop.driverClass}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
然后我们就可以在Service类或者Service类中的方法上边添加事务注解@Transactional 添加在类上边表示,为这个类的所有方法添加事务 添加在方法上边,表示只为这个方法添加事务
回归我们的源码,在我们的Service中添加@Transactional注解如下:
package com.example.service;
import com.example.dao.AccountDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
@Transactional
public class AccountService {
@Resource
private AccountDao accountDao;
public void transferMoney(String id_1, String id_2, Integer money){
accountDao.reduceMoney(id_1, money);
int i = 10 / 0;
accountDao.addMoney(id_2,money);
System.out.println("转账事务执行成功");
}
}
然后我们再次执行测试方法,观看此时的数据库数据变化(可见,一模一样,Mike的520并没有不翼而飞):  
5.6 @Transactional注解的参数详解
参数 | 描述 |
---|
propagation | 事务传播行为 | ioslation | 事务隔离级别 | timeout | 设置超时时间,事务必须在此时间内提交,否则事务回滚(默认值是-1,自动设置以秒为单位) | readOnly | 是否只读,默认值是false,表示可以增删改查,设置为true,只能做查询操作 | rollbackFor | 设置出现哪些异常进行事务回滚 | noRollbackFor | 设置出现哪些异常不进行事务回滚 |
5.6.1 事务传播行为
 Spring框架事务传播行为有7种:(常用的是REQUIRED和REQUIRED_NEW) 以下描述都是针对给update加事务并附上以下各种属性而论。  SUPPORTS:如果add有事务,那么update在add中运行,否则update在自己的事务中运行。 NOT_SUPPORT:如果add有事务,那么将add挂起,update运行在自己的事务中。 MANDATORY:update只能被有事务的方法调用,如果add方法不是事务,那么调用update的时候,抛出异常。 NEVER:update不能被有事务的方法调用,如果add方法是事务,那么抛出异常。 NESTED:如果add方法是事务,那么update在add事务的嵌套事务内运行,否则,update自己启动一个新的事务,并在自己的事务内运行。
默认的传播行为是REQUIRED,调用方法如下:
@Transactional(propagation = Propagation.REQUIRED)

5.6.2 事务隔离级别
ioslation:事务隔离级别 事务有特性成为隔离性,多事务操作之间不会产生影响。不考虑隔离性会产生很多问题。 典型的有三种读的问题: 1、脏读:事务A正在修改数据a 5000,并 - 4000 改为 1000,且尚未提交,此时事务B也读取了数据a 5000,并 + 1000 改为 6000,这个时候,A提交数据a,a就变成1000,b又提交数据a,a又变成了6000,显然,这个数据就乱了。(简单说就是,事务A正在改数据a的时候,事务B读取了原先的a进行修改,此时事务B读取的数据其实就脏了,正常情况,他得等事务A改完数据a再读取这个数据a) 2、不可重复读:事务A读取数据a 5000,此时事务B也读取数据a 5000并修改为500,且提交事务,事务A又再读取一次数据a,此时读取的就是500,两次读取的数据不同,就是不可重复读。 3、虚读:一个未提交事务读取到另一个提交事务添加数据。  通过设置事务隔离性,可以有效避免以上三种读得问题,他的属性值可以取以下几个值:  格式:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
5.7 声明式事务管理(基于XML)
在配置文件中配置事务管理器
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${prop.url}"/>
<property name="username" value="${prop.username}"/>
<property name="password" value="${prop.password}"/>
<property name="driverClassName" value="${prop.driverClass}"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
配置通知
<tx:advice id="myAdvice">
<tx:attributes>
<tx:method name="transferMoney"/>
</tx:attributes>
</tx:advice>
也可以在tx:method中加自己需要的各种属性如下:  配置切入点和切面
<aop:config>
<aop:pointcut id="accountPC" expression="execution(* com.example.service.AccountService.*(..))"/>
<aop:advisor advice-ref="myAdvice" pointcut-ref="accountPC"/>
</aop:config>
该写的都写了,现在我们把下面这一段注释掉,然后运行我们的测试方法  再去刷新数据库的数据信息,可见事务正常回滚。 
5.8 声明式事务管理(完全注解方式)
配置类代码如下:
package com.example.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@ComponentScan(basePackages = "com.example")
@EnableTransactionManagement
public class TxConfig {
@Bean
public DruidDataSource getDruidDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/user_db?&useSSL=false&serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("root");
return dataSource;
}
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource);
return manager;
}
}
然后我们把刚才注释掉的注解取消掉:  编写测试方法:
@Test
public void test(){
ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.transferMoney("1","2",520);
}
运行后跟之前有xml的时候一模一样,大功告成。
|