Spring事务管理:ACID的概念,Spring事务管理核心接口,基于XML方式的声明式事务、基于注解(Annotation)方式的声明式事务

一、事务的概念可以描述为具有以下四个关键属性,也就是 ACID

  • 原子性(Atomicity):事务应该当作一个单独单元的操作,这意味着整个序列操作要么是成功,要么是失败;

  • 一致性(Consistency):这表示数据库的引用完整性的一致性,表中唯一的主键等;

  • 隔离性(Isolation):可能同时处理很多有相同的数据集的事务,每个事务应该与其他事务隔离,以防止数据损坏;

  • 持久性(Durability):一个事务一旦完成全部操作,这个事务的结果必须是永久性的,不能因系统故障而从数据库中删除。

二、Spring 事务管理的核心接口有三个

1. PlatformTransactionManager:平台事务管理器,主要具有以下三个方法

  • TransactionStatus getTransaction(TransactionDefinition definition);  用于获取事务状态信息
  • void commit(TransactionStatus status);  用于提交事务
  • void rollback(TransactionStatus status);  用于回滚事务

 PlatformTransactionManager接口只是代表事务管理的接口,常见的几个实现类如下:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager  用于配置JDBC数据源的事务管理器
  • org.springframework.orm.hibernate4.HibernateTransactionManager  用于配置Hibernate的事务管理器
  • org.springframework.transaction.jta.JtaTransactionManager  用于配置全局事务管理器

2. TransactionDefinition:事务定义

TransactionDefinition接口是事务定义(描述)的对象,该对象中定义了事务规则,并提供了获取事务相关信息的方法,具体如下表所示:

方法 说明
String getName( ); 获取事务对象名称
int getIsolationLevel( ); 获取事务的隔离级别
int getPropagationBehavior( ); 获取事务的传播行为
int getTimeout( ); 获取事务的超时时间
boolean isReadOnly( ); 获取事务是否只读

上述方法中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务传播行为有很多种,具体如下图所示:

 

 

 

 

 

 

 

 

 

 

 

 

在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务,通常情况下,数据的查询不会影响原数据的改变,所以不需要进行事务管理,而对于数据的插入、更新和删除操作,必须进行事务管理。如果没有指定事务的传播行为,Spring默认传播行为是REQUIRED。

3. TransactionStatus:事务状态

它描述某一时间点上事务在状态信息,具体如下图所示:

三、Spring 支持两种类型的事务管理

  • 编程式事务管理 :这意味着你在编程中管理事务,它给你极大的灵活性,但却很难维护。本篇不对此种方式的实现展开讨论。
  • 声明式事务管理 :这意味着你可以从业务代码中分离事务管理,它可以使用XML方式或注解方式来管理事务。本篇仅对XML方式和注解方式实现事务展开讨论。

四、基于XML方式的声明式事务

1. 创建表(MySQL数据库)

create table account(id int primary key auto_increment,username varchar(50),balance double);

2. 创建实体类

package com.itheima.jdbc;

public class Account {
    private Integer id; // 账户id
    private String username; // 用户名
    private Double balance; // 账户余额

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    public String toString() {
        return "Account [id=" + id + ", " + "username=" + username + ", balance=" + balance + "]";
    }
}

3. 创建接口

package com.itheima.jdbc;

import java.util.List;

public interface AccountDao {
    // 添加
    public int addAccount(Account account);
    // 更新
    public int updateAccount(Account account);
    // 删除
    public int deleteAccount(int id);
    // 通过id查询
    public Account findAccountById(int id);
    // 查询所有账户
    public List<Account> findAllAccount();
    // 转账
    public void transfer(String outUser,String inUser,Double money);
}

4. 创建实现类

package com.itheima.jdbc;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class AccountDaoImpl implements AccountDao {
    // 声明JdbcTemplate属性及其setter方法
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 添加账户
    public int addAccount(Account account) {
        // 定义SQL
        String sql = "insert into account(username,balance) value(?,?)";
        // 定义数组来存放SQL语句中的参数
        Object[] obj = new Object[] { account.getUsername(), account.getBalance() };
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, obj);
        return num;
    }

    // 更新账户
    public int updateAccount(Account account) {
        // 定义SQL
        String sql = "update account set username=?,balance=? where id = ?";
        // 定义数组来存放SQL语句中的参数
        Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() };
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, params);
        return num;
    }

    // 删除账户
    public int deleteAccount(int id) {
        // 定义SQL
        String sql = "delete  from account where id = ? ";
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, id);
        return num;
    }

    // 通过id查询账户数据信息
    public Account findAccountById(int id) {
        // 定义SQL语句
        String sql = "select * from account where id = ?";
        // 创建一个新的BeanPropertyRowMapper对象
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 将id绑定到SQL语句中,并通过RowMapper返回一个Object类型的单行记录
        return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
    }

    // 查询所有账户信息
    public List<Account> findAllAccount() {
        // 定义SQL语句
        String sql = "select * from account";
        // 创建一个新的BeanPropertyRowMapper对象
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 执行静态的SQL查询,并通过RowMapper返回结果
        return this.jdbcTemplate.query(sql, rowMapper);
    }

    /**
     *  转账
     *  inUser:收款人
     *  outUser:汇款人
     *  money:收款金额
    */
    public void transfer(String outUser, String inUser, Double money) {
        // 收款时,收款用户的余额=现有余额+所汇金额
        this.jdbcTemplate.update("update account set balance = balance +? "
                + "where username = ?",money, inUser);
        // 模拟系统运行时的突发性问题
        int i = 1/0;
        // 汇款时,汇款用户的余额=现有余额-所汇金额
        this.jdbcTemplate.update("update account set balance = balance-? "
                + "where username = ?",money, outUser);
    }
}

5. 创建配置文件(applicationContext.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: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-4.3.xsd
    http://www.springframework.org/schema/tx 
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置数据源 -->
    <bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--数据库驱动 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!--连接数据库的url -->
        <property name="url" value="jdbc:mysql://localhost:3306/xuejia" />
        <!--连接数据库的用户名 -->
        <property name="username" value="root" />
        <!--连接数据库的密码 -->
        <property name="password" value="admin" />
   </bean>
   <!-- 2.配置JDBC模板 -->
   <bean id="jdbcTemplate" 
          class="org.springframework.jdbc.core.JdbcTemplate">
         <!-- 默认必须使用数据源 -->
         <property name="dataSource" ref="dataSource" />
   </bean>
   <!--3.定义id为accountDao的Bean -->
   <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl">
         <!-- 将jdbcTemplate注入到AccountDao实例中 -->
         <property name="jdbcTemplate" ref="jdbcTemplate" />
   </bean>    
   <!-- 4.事务管理器,依赖于数据源 -->
   <bean id="transactionManager" class=
   "org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
   </bean>    
   <!-- 5.编写通知:对事务进行增强(通知),需要编写对切入点和具体执行事务细节 -->
   <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- name:*表示任意方法名称 -->
            <tx:method name="*" propagation="REQUIRED" 
                           isolation="DEFAULT" read-only="false" />
        </tx:attributes>
    </tx:advice>
    <!-- 6.编写aop,让spring自动对目标生成代理,需要使用AspectJ的表达式 -->
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut expression="execution(* com.itheima.jdbc.*.*(..))"
            id="txPointCut" />
        <!-- 切面:将切入点与通知整合 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
    </aop:config>
</beans>

6. 创建测试程序

package com.itheima.jdbc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

//测试类
public class TransactionTest {
    @Test
    public void xmlTest() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        // 获取AccountDao实例
        AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao");
        // 增加两个用户
        Account account1 = new Account();
        account1.setUsername("Jack");
        account1.setBalance(1000.00);
        int num1 = accountDao.addAccount(account1);
        
        Account account2 = new Account();
        account2.setUsername("Rose");
        account2.setBalance(500.00);
        int num2 = accountDao.addAccount(account2);

        // 调用实例中的转账方法
        accountDao.transfer("Jack", "Rose", 100.0);
        
        // 输出提示信息
        System.out.println("转账成功!");
    }
}

7. 运行

系统扔出异常:java.lang.ArithmeticException: / by zero

如果查询数据库,会发现两条记录已经添加。因为添加记录与转账不在一个事务里面。

8. 修改程序,删除实现类中的如下代码

        // 模拟系统运行时的突发性问题
        int i = 1/0;

9. 再次运行,结果如下

转账成功!

10. 查询数据库表中的数据

 

 注意,表中出现两个Jack,两个Rose,并且他们的余额都发生改变。因为addAccount执行了两次,每次增加两个人。

五、基于注解声明式的事务

对上面的程序做如下修改:

1. 修改接口实现类

package com.itheima.jdbc;

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public class AccountDaoImpl implements AccountDao {
    // 声明JdbcTemplate属性及其setter方法
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 添加账户
    public int addAccount(Account account) {
        // 定义SQL
        String sql = "insert into account(username,balance) value(?,?)";
        // 定义数组来存放SQL语句中的参数
        Object[] obj = new Object[] { account.getUsername(), account.getBalance() };
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, obj);
        return num;
    }

    // 更新账户
    public int updateAccount(Account account) {
        // 定义SQL
        String sql = "update account set username=?,balance=? where id = ?";
        // 定义数组来存放SQL语句中的参数
        Object[] params = new Object[] { account.getUsername(), account.getBalance(), account.getId() };
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, params);
        return num;
    }

    // 删除账户
    public int deleteAccount(int id) {
        // 定义SQL
        String sql = "delete  from account where id = ? ";
        // 执行添加操作,返回的是受SQL语句影响的记录条数
        int num = this.jdbcTemplate.update(sql, id);
        return num;
    }

    // 通过id查询账户数据信息
    public Account findAccountById(int id) {
        // 定义SQL语句
        String sql = "select * from account where id = ?";
        // 创建一个新的BeanPropertyRowMapper对象
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 将id绑定到SQL语句中,并通过RowMapper返回一个Object类型的单行记录
        return this.jdbcTemplate.queryForObject(sql, rowMapper, id);
    }

    // 查询所有账户信息
    public List<Account> findAllAccount() {
        // 定义SQL语句
        String sql = "select * from account";
        // 创建一个新的BeanPropertyRowMapper对象
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<Account>(Account.class);
        // 执行静态的SQL查询,并通过RowMapper返回结果
        return this.jdbcTemplate.query(sql, rowMapper);
    }

    @Transactional(propagation = Propagation.REQUIRED, 
            isolation = Isolation.DEFAULT, readOnly = false)
    public void transfer(String outUser, String inUser, Double money) {
        // 收款时,收款用户的余额=现有余额+所汇金额
        this.jdbcTemplate.update("update account set balance = balance +? "
                + "where username = ?",money, inUser);
        // 模拟系统运行时的突发性问题
        int i = 1/0;
        // 汇款时,汇款用户的余额=现有余额-所汇金额
        this.jdbcTemplate.update("update account set balance = balance-? "
                + "where username = ?",money, outUser);
    }
}

2. 新建配置文件(applicationContext-annotation.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: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-4.3.xsd
    http://www.springframework.org/schema/tx 
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置数据源 -->
    <bean id="dataSource" 
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!--数据库驱动 -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <!--连接数据库的url -->
        <property name="url" value="jdbc:mysql://localhost:3306/xuejia" />
        <!--连接数据库的用户名 -->
        <property name="username" value="root" />
        <!--连接数据库的密码 -->
        <property name="password" value="admin" />
    </bean>
    <!-- 2.配置JDBC模板 -->
    <bean id="jdbcTemplate" 
            class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 默认必须使用数据源 -->
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!--3.定义id为accountDao的Bean -->
    <bean id="accountDao" class="com.itheima.jdbc.AccountDaoImpl">
        <!-- 将jdbcTemplate注入到AccountDao实例中 -->
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>
    <!-- 4.事务管理器,依赖于数据源 -->
    <bean id="transactionManager" class=
     "org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>    
    <!-- 5.注册事务管理器驱动 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

3. 修改测试程序

package com.itheima.jdbc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

//测试类
public class TransactionTest {
    @Test
    public void annotationTest() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
        // 获取AccountDao实例
        AccountDao accountDao = (AccountDao) applicationContext.getBean("accountDao");
        // 调用实例中的转账方法
        accountDao.transfer("Jack", "Rose", 100.0);
        // 输出提示信息
        System.out.println("转账成功!");
    }
}

4. 运行

系统扔出异常:java.lang.ArithmeticException: / by zero

5. 修改程序,删除实现类中的如下代码

        // 模拟系统运行时的突发性问题
        int i = 1/0;

6. 再次运行,结果如下

转账成功!

7. 查询数据库表中的数据

 注意,表中出现两个Jack,两个Rose,并且他们的余额再次发生改变。

本文参考:《Java EE企业级应用开发教程》

posted @ 2021-07-26 10:29  那些年的事儿  阅读(283)  评论(0编辑  收藏  举报