(四)Spring中的事务管理


全有或全无的操作称为事务。事务允许你将几个操作组合成一个要么发生要么不发生的工作单元。我们可以用四个词来表示事务:
> **原子性:** 原子性确保事务中的所有操作全部发生或全部不发生。(所有操作成功,事务也就成功;任意一个操作失败,事务就失败并回滚)。
> **一致性:** 一旦事务完成,系统必须确保它所建模的业务处于一直状态。
> **隔离性:** 事务允许多个用户对相同的数据进行操作,所以所有的操作应该相隔离。
> **持久性:** 一旦事务完成,事务的结果应该持久化。

<!--more-->
在介绍Spring的事务管理之前,我们首先要介绍一下Spring的JDBC模板技术。
# Spring框架的JDBC模板技术
Spring框架中提供了很多模板类来实现简化编程。比如最基本的JDBC模板(Spring框架提供了`JdbcTemplate`类)
我们以以一个简单的案例来演示如何使用Spring框架提供的JDBC模板类

1. 创建数据表结构
```
create database springlearn;
use springlearn;
create table t_account(
  id int primary key auto_increment,
  name varchar(100),
  money double
);
```
2. 方式一:通过new `DriverManagerDataSource`来插入数据
```java
@Test
public void run(){
    //创建连接池,使用Spring框架内置的连接池
    DriverManagerDataSource dataSource = new DriverManagerDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql:///springlearn");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    //创建模板类
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
    //完成数据的添加
    jdbcTemplate.update("insert into t_account values (null,?,?)","TyCoding",1000);
}
```
此时已经在表中添加了一行数据。
**注意:**
  1. `jdbc:mysql:///`等价于`jdbc:mysql://localhost:3306`
  2. 此时我们先不要插入中文,可能会出现乱码情况。

##  使用Spring框架来管理模板类
上面的案例中我们使用的是new的方式来创建的jdbc模板类,下面我们用Spring来管理这些模板类
1. `spring.xml`
```xml
<?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: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/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <context:component-scan base-package="spring_2"/>
    <!-- Spring的数据库连接池 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///springlearn"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    <!-- Spring的模板类 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 将连接池对象注入到模板类中 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
```
**注意:**
  类似上面我们使用new的方式创建对象,步骤仍是先创建连接池对象(此处使用的Spring内置的连接池对象),然后创建模板类对象(并将连接池对象注入到模板类对象中)

2. `Test测试类`
```java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class Demo4Test {
    @Resource(name="jdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Test
    public void run2(){
        jdbcTemplate.update("insert into t_account values(null,?,?)","TyCoding2",1000);
    }
}
```
**注意:**
  1. Spring整合了JUnit测试,所以我们这里使用了Spring的注解方法加载配置文件`spring.xml`
      * `@RunWith()`用于获取测试类对象
      * `@ContextConfiguration()`用于加载配置文件
  2. 大家还记得前面我们已经介绍了Spring的注入对象方式,其中`@Resource`和`@Autowired`注解都可以实现将一个Java对象交给Spring,通过加载Spring上下文来注入此对象。因为此时我们在Java类中注入的Bean对象名是`jdbcTemplate`,而`spring.xml`中也配置了名字是`jdbcTemplate`Bean的属性,那么在Java类中的`jdbcTemplate`对象就拥有了一些属性。增加name属性仅是为了缩小查找Bean的范围。
  3. 综上我们注入了Bean对象,并加载了配置文件,就可以直接执行相关sql语句了


此时已经插入了两条数据

# Spring框架的事务管理
上面我们简单的介绍了Spring框架如何通过模板类执行SQL语句的,下面我们就分析一下Spring对事务的管理:
上面我们已经介绍了事务的四个特性。
> 原子性
> 一致性
> 隔离性
> 持久性
下面我们仍要了解一些名词概念
1. Spring的事务分类:
> 编码式事务
> 声明式事务

2. 事务的属性
> 传播行为: 传播行为定义了客户端与被调方法之间的事务边界。(Spring定义了7中不同的传播行为)
> 隔离级别:定义一个事务可能受到其他并发事务的影响程度。
    * 脏读:一个事务读取了另一个事务尚改写但尚未提交的数据
    * 不可重复读:一个事务执行相同的查询多次,每次得到不同数据。(并发访问造成的)
    * 幻读:类似不可重复读。一个事务读取时遇到另一个事务的插入,则这个事务就会读取到一些原本不存在的记录。
> 只读:事务只读时,数据库就可以对其进行一些特定的优化。
> 事务超时:事务运行时间过长。
> 回滚原则:定义那些异常会导致事务回滚。(默认情况只有运行时异常才事务回滚)

3. Spring框架的事务管理相关的类和API
> 1.PlatformTransactionManager接口:平台事务管理器(真正管理事务的类)
>       该接口的实现类:
>           1.`DataSourceTransactionManager`:用在当使用Spring的JDBC模板类或Mybatis框架时
>           2.`HibernateTransactionManager`:使用Hibernate框架时
> 2.TransactionDefinition接口:事务定义信息(事务隔离级别、传播行为、超时、只读)
> 3.TransactionStatus接口:事务的状态


## 声明式事务
Spring有两种事务,但是我们这里只介绍声明式事务,编码式事务不常用,所以这里不再介绍。
在案例之前我们了解一下2种常用连接池的不同写法:
**2种连接池**
1. **c3p0**
```xml
   <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <!--配置链接属性-->
            <property name="driverClass" value="${jdbc.driver}"/>
            <property name="jdbcUrl" value="${jdbc.url}"/>
            <property name="user" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.username}"/>
     </bean>
```
2. **druid** 阿里的连接池
```xml
   <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <!--配置链接属性-->
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
    </bean>
```
注意:采用`${jdbc.xx}`的写法前提是在外边定义了`xx.properties`文件,并在`spring.xml`中引入了该配置文件
<br/>
### 在XML中定义事务
下面开始我们的案例
1. 首先我们要引入Spring提供的`tx`命名空间
```xml
<?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:aop="http://www.springframework.org/schema/aop"
       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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
```
**注意**
不但需要`tx`的命名空间,还需要`aop`的命名空间,以为Spring的声明式事务式的支持是通过Spring AOP框架实现的。
通过一个XML片段了解一下`<tx:advice>`是如何声明事务性策略的:
```xml
<tx:advice id="txAdvice">
  <tx:attributes>
    <tx:method name="save*" propagation="REQUIRED"/>
    <tx:method name="*" propagation="SUPPORTS" read-only="true"/>
  </tx:attributes>
</tx:advice>
```
**解释:**
`<tx:advice>`实现声明事务性策略,所有的事务配置都在改元素下定义。`<tx:method>`元素为name属性指定的方法定义事务参数。我们看一下`<tx:method>`的属性:
隔离级别|含义
-|:-:|-:
isolation|指定事务的隔离级别
propagation|定义事务的传播规则
read-only|指定事务为只读
回滚规则:<br>rollback-for<br>no-rollback-for|rollback-for指定了事务对于那些检查型异常应当回滚而不提交<br/>no-rollback-for指定事务对于那些异常应当继续执行而不回滚
timeout|对于长时间运行的事务定义超时时间

当使用`<tx:advice>`来声明事务时,我们还需要一个事务管理器(常用`DataSourceTransactionManager`),然后使用`transaction-manager`属性指定事务管理器的id:
```xml
<tx:advice id="txAdvice" transaction-manager="txManager">
  ...
</tx:advice>
```

`<tx:advice>`仅定义了AOP通知,用于把事务边界通知给方法。但这些只是事务通知,而不是完成的事务性切面。所以我们还需要使用`<aop:config>`定义一个通知器(advisor)
```xml
<aop:config>
  <aop:advisor pointcut="execution(* xx.xx.xxx(..))" advice-ref="txAdvice">
</aop:config>
```

看了上面的解释你是否有一些思路了呢?下面我们通过案例(转账)来体会一下:
1. 目录结构:

初始数据:

2. `AccountService.java`
```java
package spring_2;
public interface AccountService {
    void pay(String out, String in, double money);
}
```
3. `AccountServiceImp.java`
```java
package spring_2;
import javax.annotation.Resource;
public class AccountServiceImp implements AccountService {
    @Autowired
    private AccountDao accountDao;

    public void pay(String out, String in, double money) {
        //扣钱
        accountDao.outMoney(out,money);
        //模拟异常
        //int a = 10/0;
        //加钱
        accountDao.inMoney(in,money);
    }
}
```
4. `AccountDao.java`
```java
package spring_2;
public interface AccountDao {
    void outMoney(String out,double money);
    void inMoney(String in,double money);
}
```
5. `AccoundDaoImp.java`
```java
package spring_2;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
public class AccountDaoImp extends JdbcDaoSupport implements AccountDao {
    public void outMoney(String out,double money){
        this.getJdbcTemplate().update("update t_account set money = money - ? where name = ?",money,out);
    }
    public void inMoney(String in,double money){
        this.getJdbcTemplate().update("update t_account set money = money + ? where name = ?",money,in);
    }
}
```
6. `spring.xml`
```xml
<?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:aop="http://www.springframework.org/schema/aop"
       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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

    <!-- 使用c3p0的连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///springlearn"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 注入Bean -->
    <bean id="accountService" class="spring_2.AccountServiceImp"/>
    <bean id="accountDao" class="spring_2.AccountDaoImp">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务增强 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--
                name            :绑定事务的方法名,可以使用通配符,可以配置多个
                propagation     :传播行为
                isolation       :隔离级别
                read-only       :是否只读
                timeout         :超时信息
                rollback-for    :发生哪些异常回滚
                no-rollback-for :发生哪些异常不回滚
            -->
            <tx:method name="pay" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置AOP切面代理 -->
    <aop:config>
        <!-- 如果是自己写的切面,使用<aop:aspect>标签;如果是系统的,用<aop:advisor> -->
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* spring_2.AccountServiceImp.pay(..))"/>
    </aop:config>
</beans>
```
7. `Test测试类`
```java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class AccountTest {
    @Autowired
    private AccountService accountService;

    @Test
    public void run3(){
        accountService.pay("TyCoding","tutu",100);
    }
}
```




### 定义注解驱动的事务
我们不但可以使用XML定义事务驱动,还可以用使用注解`@Transaction`
秩序要改动`spring.xml`和`AccountServiceImp.java`即可以大大简化代码,如下:
1. `AccountServiceImp.java`
```java
@Transactional
public class AccountServiceImp implements AccountService {

    @Autowired
    private AccountDao accountDao;

    public void pay(String out, String in, double money) {
        //扣钱
        accountDao.outMoney(out,money);
        //int a = 10/0;
        //加钱
        accountDao.inMoney(in,money);
    }
}
```
2. `spring.xml`
```xml
<?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:aop="http://www.springframework.org/schema/aop"
       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-3.0.xsd
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
          http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

    <context:component-scan base-package="spring_2"/>

    <!-- 使用c3p0的连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql:///springlearn"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 注入Bean -->
    <bean id="accountService" class="spring_2.AccountServiceImp"/>
    <bean id="accountDao" class="spring_2.AccountDaoImp">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!--&lt;!&ndash; 配置事务增强 &ndash;&gt;
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            &lt;!&ndash;
                name            :绑定事务的方法名,可以使用通配符,可以配置多个
                propagation     :传播行为
                isolation       :隔离级别
                read-only       :是否只读
                timeout         :超时信息
                rollback-for    :发生哪些异常回滚
                no-rollback-for :发生哪些异常不回滚
            &ndash;&gt;
            <tx:method name="pay" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    &lt;!&ndash; 配置AOP切面代理 &ndash;&gt;
    <aop:config>
        &lt;!&ndash; 如果是自己写的切面,使用<aop:aspect>标签;如果是系统的,用<aop:advisor> &ndash;&gt;
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* spring_2.AccountServiceImp.pay(..))"/>
    </aop:config>-->
</beans>
```
如图所示,我们只需要写一个`<tx:annotation-driven>`即可代替如上的XML配置,它允许在最有意义的位置声明事务规则:在事务性方法上。

`<tx:annotation-driven>`元素告诉Spring检查上下文中所有的Bean并查找使用`@Transaction`注解的Bean,而不管这个注解是用在类级方法上还是方法级别上。
对于每一个使用`@Transactional`注解的Bean,`<tx:annotation-driven>`会自动为它添加事务通知。通知的事务属性是通过`@Transactional`注解的参数定义的。



如上无论哪种方式,当我们把`AccountServiceImp.java`中的`int a = 10 / 0;`以上都会报错,事务并回滚。





<br/>

# 交流

如果大家有兴趣,欢迎大家加入我的Java交流群:671017003 ,一起交流学习Java技术。博主目前一直在自学JAVA中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法,当然群里的大佬都会积极给新手答疑的。所以,别犹豫,快来加入我们吧!

<br/>

# 联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

- [Blog@TyCoding's blog](http://www.tycoding.cn)
- [GitHub@TyCoding](https://github.com/TyCoding)
- [ZhiHu@TyCoding](https://www.zhihu.com/people/tomo-83-82/activities)



猜你喜欢

转载自blog.csdn.net/TyCoding/article/details/80922422
今日推荐