【Spring从入门到实战教程】第六章 Spring 事务管理详解

六、Spring 事务管理

6.1 事务简介

    事务(Transaction)是基于关系型数据库(RDBMS)的企业应用的重要组成部分。在软件开发领域,事务扮演者十分重要的角色,用来确保应用程序数据的完整性和一致性。

    事务就是一系列的动作,当做一个独立的工作单元,这些动作要么全部成功,要么全部失败。

    事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。
        原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的动作要么都做要么都不做。
        一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态,一致性和原子性是密切相关的。
        隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
        持久性(Durability):持久性也称为永久性,指一个事务一旦提交,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。
        
    事务允许我们将几个或一组操作组合成一个要么全部成功、要么全部失败的工作单元。如果事务中的所有的操作都执行成功,那自然万事大吉。但如果事务中的任何一个操作失败,那么事务中所有的操作都会被回滚,已经执行成功操作也会被完全清除干净,就好像什么事都没有发生一样。

    在现实世界中,最常见的与事务相关的例子可能就是银行转账了。假设我们需要将 1000 元从 A 账户中转到 B 账户中,这个转账操作共涉及了以下两个操作。
        从 A 账户中扣除 1000 元;
        往 B 账户中存入 1000 元。

    如果 A 账户成功地扣除了 1000 元,但向 B 账户存入时失败的话,那么我们将凭空损失 1000 元;如果 A 账户扣款时失败,但却成功地向 B 账户存入 1000 元的话,我们的账户就凭空多出了 1000 元,那么银行就会遭受损失。因此我们必须保证事务中的所有操作要么全部成功,要么全部失败,理解了这一点,我们也就抓住了事务的核心。

    作为一款优秀的开源框架和应用平台,Spring 也对事务提供了很好的支持。Spring 借助 IoC 容器强大的配置能力,为事务提供了丰富的功能支持。

6.2 原生JDBC事务管理的问题

原生JDBC事务管理实现:

//1、获取Connection对象
Connection conn = DBUtil.getConnection();

//2、获取执行SQL语句的Statement
PreparedStatement stmt = null;

try {
    //3、修改事务的提交方式为手动提交
    conn.setAutoCommit(false);
    
    //4、执行SQL
    String sql = "insert into ...";
    stmt = conn.prepareStatement(sql);
    //设置占位符
    stmt.setString(1, ...);
    stmt.executeUpdate();
    
    //5、手动提交
    conn.commit();
} catch (SQLException e) {
    e.printStackTrace();
    try{
        //6、出现异常回滚
        conn.rollback();
    } catch (SQLException e1) {
        e1.printStackTrace();
    }
} finally {
    //7、释放资源
    DBUtil.closeAll(conn, stmt, null);
}

Spring原生JDBC事务管理实现:

public class JdbcTest {

    @Test
    public void jdbcTemplateTxTest() {
        //1、获取IOC容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        //2、获取数据源
        DataSource dataSource = ac.getBean("dataSource", DataSource.class);


        //3、获取连接
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            connection = dataSource.getConnection();

            //4、修改jdbc事务的提交方式为手动提交
            connection.setAutoCommit(false);

            //5、获取执行SQL的Statement,并执行
            String sql = "delete from student where id = 3";
            preparedStatement = connection.prepareStatement(sql);
            int result = preparedStatement.executeUpdate();

            System.out.println("执行成功:" + result);

            //6、手动提交
            connection.commit();
        } catch (SQLException throwables) {
            // 7、出现异常回滚
            try {
                connection.rollback();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            throwables.printStackTrace();
        } finally {
            //8、释放资源
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }

            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
        }
    }
}

问题:

    必须为不同的方法重写类似的样板代码。并且这段代码是特定于JDBC的,一旦选择类其它数据库存取技术(比如Mybatis使用SqlSession来提交和回滚),代码需要作出相应的修改。

6.3 Spring中的事务管理

  • 作为企业级应用程序框架,Spring在不同的事务管理API之上定义了一个抽象层。而应用程序开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。

  • Spring既支持编程式事务管理,也支持声明式的事务管理。

  • 编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码。

  • 声明式事务管理:大多数情况下比编程式事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过AOP方法模块化。Spring通过AOP框架支持声明式事务管理。

选择编程式事务还是声明式事务,很大程度上就是在控制权细粒度和易用性之间进行权衡。

  • 编程式对事物控制的细粒度更高,我们能够精确的控制事务的边界,事务的开始和结束完全取决于我们的需求,但这种方式存在一个致命的缺点,那就是事务规则与业务代码耦合度高,难以维护,因此我们很少使用这种方式对事务进行管理。

  • 声明式事务易用性更高,对业务代码没有侵入性,耦合度低,易于维护,因此这种方式也是我们最常用的事务管理方式。

Spring 的声明式事务管理主要通过以下 2 种方式实现:

  • 基于 XML 方式的声明式事务管理

  • 基于注解方式的声明式事务管理

6.4 事务管理器

  • Spring从不同的事务管理API中抽象了一整套的事务机制。开发人员不必了解底层的事务API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了。

  • Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。Spring的核心事务管理器是PlatformTransactionManager接口。它为事务管理封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。

  • PlatformTransactionManager 接口中各方法说明如下:

名称 说明
TransactionStatus getTransaction(TransactionDefinition definition) 用于获取事务的状态信息
void commit(TransactionStatus status) 用于提交事务
void rollback(TransactionStatus status) 用于回滚事务
  • Spring 为不同的持久化框架或平台(例如 JDBC、Hibernate、JPA 以及 JTA 等)提供了不同的 PlatformTransactionManager 接口实现,这些实现类被称为事务管理器实现。

实现类 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 使用 Spring JDBC 或 iBatis 进行持久化数据时使用。
org.springframework.orm.hibernate3.HibernateTransactionManager 使用 Hibernate 3.0 及以上版本进行持久化数据时使用。
org.springframework.orm.jpa.JpaTransactionManager 使用 JPA 进行持久化时使用。
org.springframework.jdo.JdoTransactionManager 当持久化机制是 Jdo 时使用。
org.springframework.transaction.jta.JtaTransactionManager 使用 JTA 来实现事务管理,在一个事务跨越多个不同的资源(即分布式事务)使用该实现。
  • 这些事务管理器的使用方式十分简单,我们只要根据持久化框架(或平台)选用相应的事务管理器实现,即可实现对事物的管理,而不必关心实际事务实现到底是什么。

6.5 事务案例

6.5.1 命名空间和标签规范

在spring的配置文件中加入tx命名空间和标签规范:

xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd

6.5.2 Spring的配置

<?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:util="http://www.springframework.org/schema/util"
    xmlns:p="http://www.springframework.org/schema/p"
    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/util
    http://www.springframework.org/schema/util/spring-util.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 组件扫描 -->
    <context:component-scan base-package="com.newcapec"/>

    <!-- 读取资源文件 -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <!-- 连接数据库的基础信息 -->
        <property name="driverClassName" value="${jdbc.mysql.driver}"/>
        <property name="url" value="${jdbc.mysql.url}"/>
        <property name="username" value="${jdbc.mysql.username}"/>
        <property name="password" value="${jdbc.mysql.password}"/>
    </bean>

    <!-- 配置JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

6.5.3 建立表结构

CREATE TABLE T_ACCOUNT(
  id      INT PRIMARY KEY AUTO_INCREMENT,
  name    VARCHAR(20),
  balance DECIMAL(8,2)
);

insert into T_ACCOUNT (id, name, balance) values (1, '小明', 1000);
insert into T_ACCOUNT (id, name, balance) values (2, '淘宝', 0);
insert into T_ACCOUNT (id, name, balance) values (3, '京东', 0);

6.5.4 实体类

public class Account {
    private Integer id;
    private String name;
    private Double balance;

    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 getBalance() {
        return balance;
    }

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

6.5.5 Dao

接口:

public interface AccountDao {

    void updateBalance(Account account);

    Account findAccountById(Integer id);
}

实现类:

/**
 * 注意:事务不能添加在持久层,而是放在业务层
 * 持久层中的方法都是单sql,业务层可能由多sql组成
 */
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void updateBalance(Account account) {
        String sql = "update t_account set balance=? where id=?";
        jdbcTemplate.update(sql, account.getBalance(), account.getId());
        System.out.println("对账户的余额更新操作完成..........");
    }

    @Override
    public Account findAccountById(Integer id) {
        String sql = "select id,name,balance from t_account where id=?";
        RowMapper<Account> rowMapper = new BeanPropertyRowMapper<>(Account.class);
        return jdbcTemplate.queryForObject(sql, rowMapper, id);
    }
}

6.5.6 Service

接口:

public interface AccountService {

    void transferMoney(int fromId, int toId, double money) throws Exception;
}

实现类:

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Override
    public void transferMoney(int fromId, int toId, double money) throws Exception {
        /**
         * 在转账业务逻辑中包含以下的几个步骤
         * 1.查询转出账户的余额
         * 2.从转出账户中扣除转账金额
         * 3.查询转入账户的余额
         * 4.转入账户的余额+转账金额,并更新转入账户的余额
         */
        Account fromAccount = accountDao.findAccountById(fromId);
        fromAccount.setBalance(fromAccount.getBalance() - money);
        accountDao.updateBalance(fromAccount);

        //模拟程序执行过程中出现问题,异常出现导致程序终止(方法结束)
        //System.out.println(10/0);

        Account toAccount = accountDao.findAccountById(toId);
        toAccount.setBalance(toAccount.getBalance() + money);
        accountDao.updateBalance(toAccount);

        System.out.println("一次转账完成...");
    }
}

6.5.7 无事务测试

public class TXTest {

    @Test
    public void testTransfer() throws Exception {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = ac.getBean("accountService", AccountService.class);
        accountService.transferMoney(1,2,360);
    }
}

测试结果分析:

  • 当Service的方法中没有异常,程序正常执行完毕,数据库中的数据也是正常的。 小明账户中的余额为640,淘宝账户中的余额为360;

  • 当Service的方法中出现异常,程序因异常终止执行,数据库中的数据出现问题(数据不一致)。 小明账户中的余额为640,淘宝账户中的余额为0,转出的金额数据丢失;

6.5.8 配置事务管理

    Spring 声明式事务管理是通过 AOP 实现的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建(或加入)一个事务,在执行完目标方法后,根据执行情况提交或者回滚事务。

    声明式事务最大的优点就是对业务代码的侵入性低,可以将业务代码和事务管理代码很好地进行解耦。

配置事务管理器

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

在以上配置中,配置的事务管理器实现为 DataSourceTransactionManager,即为 JDBC 和 iBatis 提供的 PlatformTransactionManager 接口实现。

配置事务通知

    在 Spring 的 XML 配置文件中配置事务通知,指定事务作用的方法以及所需的事务属性。
    
    当我们使用 <tx:advice> 来声明事务时,需要通过 transaction-manager 参数来定义一个事务管理器,这个参数的取值默认为 transactionManager。

    如果我们自己设置的事务管理器恰好与默认值相同,则可以省略对改参数的配置。但如果我们自己设置的事务管理器 id 与默认值不同,则必须手动在 <tx:advice> 元素中通过 transaction-manager 参数指定。
    
    对于<tx:advice> 来说,事务属性是被定义在<tx:attributes> 中的,该元素可以包含一个或多个 <tx:method> 元素。

    <tx:method> 元素包含多个属性参数,可以为某个或某些指定的方法(name 属性定义的方法)定义事务属性,如下表所示。

事务属性 说明
propagation 指定事务的传播行为。
isolation 指定事务的隔离级别。
read-only 指定是否为只读事务。
timeout 表示超时时间,单位为“秒”;声明的事务在指定的超时时间后,自动回滚,避免事务长时间不提交会回滚导致的数据库资源的占用。
rollback-for 指定事务对于那些类型的异常应当回滚,而不提交。
no-rollback-for 指定事务对于那些异常应当继续运行,而不回滚。
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!--配置事务属性-->
    <tx:attributes>
        <!--配置那些方法要使用事务,事务的属性-->
        <!-- 所有的事务管理的方法中,采用默认的事务属性 -->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

配置事务切入点

    <tx:advice> 元素只是定义了一个 AOP 通知,它并不是一个完整的事务性切面。我们在 <tx:advice> 元素中并没有定义哪些 Bean 应该被通知,因此我们需要一个切点来做这件事。

    在 Spring 的 XML 配置中,我们可以利用 Spring AOP 技术将事务通知(tx-advice)和切点配置到切面中,配置内容如下。

<!-- 配置切点和切面 -->
<aop:config><!--配置切点-->
    <aop:pointcut id="exp" expression="execution(* com.newcapec.service..*Impl.*(..))"/>
    <!-- 配置切面: 将切点表达式应用在事务上-->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="exp"/>
</aop:config>

事务测试

    当Service的方法中出现异常,程序因异常终止执行,数据库中的数据仍然为原有数据。小明账户中的余额为1000,淘宝账户中的余额为0,并没有实施金额的转出与转入(执行成功的部分数据回滚)。

6.6 事务的属性

6.6.1 事务的传播行为

  • 事务传播行为(propagation behavior)指的是,当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。例如,事务方法 A 在调用事务方法 B 时,B 方法是继续在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。事务方法指的是能让数据库表数据发生改变的方法,例如新增数据、删除数据、修改数据的方法。

  • 事务的传播行为可以由传播属性指定,在<tx:method>标签中配置propagation属性。

  • Spring定义了7种类传播行为:其中REQUIRED为默认值。

传播属性 描述
REQUIRED 默认传播行为,如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行。
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行。否则它可以不运行在事务中。
NOT_SUPPORTED 当前的方法不应该运行在事务中。如果有运行的事务,将它挂起。
MANDATORY 当前的方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常。
NEVER 当前的方法不应该运行在事务中。如果有运行的事务,就抛出异常。
NESTED 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行。
  • REQUIRED传播行为

     当事务method2()方法与method3()方法被另一个事务方法method1()调用时,它默认会在现有的事务内运行,这个默认的传播行为就是REQUIRED。因此在method1()方法的开始和终止边界内只有一个事务,这个事务只在method1()方法结束的时候被提交,如果执行过程中出现异常,导致3个方法的数据都要回滚。

  • REQUIRES_NEW传播行为

    传播行为REQUIRES_NEW是当事务method2()方法与method3()方法被另一个事务方法method1()调用时,method2()与method3()都必须启动一个新事务,并且在method2()与method3()的事务执行时,method1()方法的事务会先挂起,等待其他事务执行完成,再继续执行。执行过程中出现异常,如果method2()或method3()的事务已经执行完成,那么它们是不会回滚数据,只有method1()的数据要回滚。

    注意:Spring的事务管理通过切面实现,如果直接使用this.方法()或者方法(),不会触发切面中对事务的管理。需要将方法定义不同的类中,或者service自己注入自己,用这个注入对象来调用方法。

  • 传播行为测试

    在Service层添加payOrder的方法,实现批量转账功能。

接口:

public interface AccountService {

    void payOrder(int fromId, Map<Integer, Double> map) throws Exception;
}

实现类:

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    /**
     * 注意:
     * 同一个Service中不同方法在进行相互调用,不能另起事务,必须在同一个事务内运行
     * 如果需要单独启动事务,需要当前Service类定义一个自己类型属性,并将自己的对象注入
     * 通过该属性调用方法,即可
     */
     @Autowired
    private AccountService accountService;

    @Override
    public void payOrder(int fromId, Map<Integer, Double> map) throws Exception {

        Set<Integer> toIdSet = map.keySet();
        for (Integer toId : toIdSet) {
            double money = map.get(toId);

            //实施单账户转账
            accountService.transferMoney(fromId, toId, money);

            //模拟第一次转账之后,系统出现异常
            String s = null;
            s.trim();
        }
        System.out.println("订单支付成功...");
    }
}

事务属性配置:

<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!--配置事务属性-->
    <tx:attributes>
        <tx:method name="transferMoney" propagation="REQUIRES_NEW"/>
        <!--配置那些方法要使用事务,事务的属性-->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

测试:

@Test
public void testPayOrder() throws Exception {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    AccountService accountService = ac.getBean("accountService", AccountService.class);
    Map<Integer, Double> map = new HashMap<>();
    map.put(2, 199.0);
    map.put(3, 260.0);

    accountService.payOrder(1, map);
}

测试结果分析:

  • 当payOrder方法中出现异常,传播行为是REQUIRED时,数据库中的数据仍然为原有数据,并没有发生变化。 第一次成功转账的数据也随着异常一起回滚;

  • 当payOrder方法中出现异常,传播行为是REQUIRES_NEW时,数据库中的数据发生变化。 第一次成功转账的数据提交并没有回滚;

6.6.2 事务的隔离级别

  • 当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时,可能导致的问题可以分为下面三种类型:

    • 脏读:对于两个事务TX1和TX2,TX1读取了已经被 TX2更新但还没有被提交的数据。之后,如果TX2回滚,TX1读取的内容就是临时且无效的;

    • 不可重复读:对于两个事务TX1和TX2,TX1读取了一个字段,然后TX2更新了该字段。之后,TX1再次读取同一个字段,值就不同了;

    • 幻读:对于两个事务TX1和TX2,TX1从一个表中读取了读取几行数据,然后TX2在该表中插入了一些新的记录。之后,如果TX1再次读取同一个表,就会多出几行;

  • 从理论上来说,事务应该彼此完全隔离,以避免并发事务所导致的问题。然而,那样会对性能产生极大的影响,因为事务必须按顺序运行。所以在实际开发中,为了提升性能,事务会以较低的隔离级别运行。

  • 事务的隔离级别可以通过隔离事务属性指定,在<tx:method>标签中配置isolation属性,其默认值为:DEFAULT。

隔离级别 描述
DEFAULT 使用底层数据库的默认隔离级别。对于大多数数据库来说,默认隔离级别都是READ_COMMITTED;
READ_UNCOMMITTED 允许事务读取未被其他事务提交的变更。脏读,不可重复读和幻读的问题都会出现;
READ_COMMITTED 只允许事务读取已经被其它事务提交的变更。可以避免脏读,但不可重复读和幻读问题仍然可能出现;
REPEATABLE_READ 确保事务可以多次从一个字段中读取相同的值。在这个事务持续期问,禁止其他事物对这个字段进行更新。可以避免脏读和不可重复读,但幻读的问题仍然存在;
SERIALIZABLE 确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入,更新和删除操作。所有并发问题都可以避免,但性能十分低下;
  • 事务的隔离级别要得到底层数据库引擎的支持,而不是应用程序或者框架的支持。

  • Oracle支持的2种事务隔离级别:READ_COMMITTED,SERIALIZABLE,默认为READ_COMMITTED。

  • Mysql支持4中事务隔离级别,默认为REPEATABLE_READ。

mysql事务隔离级别相关语句:

-- 事务自动提交
show variables like 'autocommit';

-- 关闭OFF
set autocommit = 0;

-- 开启ON
set autocommit = 1;

-- 查看系统级的隔离级别和会话级的隔离级别
select @@global.tx_isolation,@@tx_isolation;

-- 设置事务的隔离级别:set 作用域 transaction isolation level 事务隔离级别名称
-- set [session | global] transaction isolation level {read uncommitted | read committed | repeatable read | serializable}

-- 全局
set global transaction isolation level read committed;

-- 当前会话
set session transaction isolation level read uncommitted;

oracle事务隔离级别相关语句:

-- 设置一个事务的隔离级别:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 设置单个会话的隔离级别:
ALTER SESSION SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
ALTER SESSION SET TRANSACTION ISOLATION SERIALIZABLE;

-- 首先创建一个事务
declare
    trans_id Varchar2(100);
begin
    trans_id := dbms_transaction.local_transaction_id( TRUE );
end;

-- 查看事务隔离级别
SELECT s.sid, s.serial#,
  CASE BITAND(t.flag, POWER(2, 28))
    WHEN 0 THEN 'READ COMMITTED'
    ELSE 'SERIALIZABLE'
  END AS isolation_level
FROM v$transaction t
JOIN v$session s ON t.addr = s.taddr AND s.sid = sys_context('USERENV', 'SID');

6.6.3 回滚事务属性

  • 默认情况下只有运行时异常(RuntimeException)会导致事务回滚,而编译期异常不会。

  • 事务的回滚规则可以通过,<tx:method>标签中的rollback-for和no-rollback-for属性来配置,如果有不止一种异常,用逗号分隔。

  • rollback-for:遇到时必须进行回滚。

  • no-rollback-for:遇到时必须不回滚。

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 事务属性-->
    <tx:attributes>
        <tx:method name="transferMoney" propagation="REQUIRED" isolation="READ_COMMITTED"
                    rollback-for="java.lang.ClassNotFoundException"
                    no-rollback-for="java.lang.NullPointerException"/>
        <!-- 所有的事务管理的方法中,采用默认的事务属性 -->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

AccountServiceImpl:

@Override
public void transferMoney(int fromId, int toId, double money) throws Exception {
    /**
         * 在转账业务逻辑中包含以下的几个步骤
         * 1.查询转出账户的余额
         * 2.从转出账户中扣除转账金额
         * 3.查询转入账户的余额
         * 4.转入账户的余额+转账金额,并更新转入账户的余额
         */
    Account fromAccount = accountDao.findAccountById(fromId);
    fromAccount.setBalance(fromAccount.getBalance() - money);
    accountDao.updateBalance(fromAccount);

    //模拟程序执行过程中出现问题,异常出现导致程序终止(方法结束)
    //System.out.println(10/0);

    //编译时异常,默认情况下不会导致事务回滚
    //Class.forName("abc");

    //运行时异常,默认情况下会导致事务回滚
    String str = null;
    str.trim();

    Account toAccount = accountDao.findAccountById(toId);
    toAccount.setBalance(toAccount.getBalance() + money);
    accountDao.updateBalance(toAccount);

    System.out.println("一次转账完成...");
}

测试:

@Test
public void testTransfer() throws Exception {
    ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
    AccountService accountService = ac.getBean("accountService", AccountService.class);
    accountService.transferMoney(1,2,360);
}

6.6.4 只读事务属性

    <tx:method>标签中的`read-only="true"`指定事务为只读事务,表示该事务只读取数据而不更新数据,可以帮助数据库优化事务。如果该方法中只是读取数据库中的数据,那么应该将其设置为只读事务。

6.6.5 事务超时属性

  • 事务在强制回滚之前可以保持多久,这样可以防止长期运行的事务占用资源。

  • <tx:method>标签中的timeout属性配置事务的超时,其值为int类型,单位为秒钟,如果值为-1表示永不过期。

  • 两个事务先后访问同一个数据,此时先到事务将数据隔离,那么后到的事务将无法操作数据,后到事务就会进入等待,等待以及执行时间为此事务的过期时间。如果事务过期那么此事务会强制回滚。

6.6.6 事务完整配置

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 事务属性-->
    <tx:attributes>
        <!--
            配置事务的方法名称如果以find开头,那么它的事务传播行为为:SUPPORTS
        -->
        <tx:method name="insert*" isolation="READ_COMMITTED"/>
        <tx:method name="add*" isolation="READ_COMMITTED"/>
        <tx:method name="save*" isolation="READ_COMMITTED"/>
        <tx:method name="update*" isolation="READ_COMMITTED"/>
        <tx:method name="edit*" isolation="READ_COMMITTED"/>
        <tx:method name="delete*" isolation="READ_COMMITTED"/>
        <tx:method name="remove*" isolation="READ_COMMITTED"/>
        <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="list*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="load*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
        <tx:method name="transferMoney" propagation="REQUIRED" isolation="READ_COMMITTED"
                   rollback-for="java.lang.ClassNotFoundException"
                   no-rollback-for="java.lang.NullPointerException"
                   timeout="4"/>
        <!-- 所有的事务管理的方法中,采用默认的事务属性 -->
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

猜你喜欢

转载自blog.csdn.net/ligonglanyuan/article/details/124809858