Spring框架自学之路——事务管理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_15096707/article/details/79828290

目录

前言

  文中主要介绍的是Spring的事务管理,还包括了事务的隔离级别、Spring事务配置的属性介绍等,内容很多参考了网上的文章或资料,在这里我使用了一个案例。对Spring事务管理或者说事务等相关知识点进行了整理。在此,感谢前辈们在网上无私地传播自己学到的技术知识。

介绍

  事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。
  事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
  
  在数据库操作中考虑事务是一件尤为重要的事,那对于Spring,我们应如何使用Spring实现事务呢?在Spring中事务管理分为两种:1)编程式事务管理,即在程序中通过Spring提供的接口或类,调用管理事务的相关方法,如开启事务,提交事务,事务回滚等,实现对事务的管理;2)声明式事务管理,包括基于XML配置文件实现和基于注解实现两种方式。对于这两种Spring事务管理的方式,我们建议在实际的开发中采用声明式事务管理的方式,因为声明式事务管理这一方式的代码侵入性最小。因此本文讲解的内容也主要是讲Spring声明式事务管理的使用。

案例说明

  为简单说明Spring声明式事务管理的使用,我们以一个案例为例,即银行转账,如A给B转X元。简单的过程如下:
1. A的余额扣除X元;
2. B的余额增加X元。
  显然这一过程我们需要当作一个事务来实现,否则可能出现“A的余额扣除成功,但B的余额增加失败”这一问题。实际上当B的余额增加失败的时候,为保证数据的一致性,A的余额扣除操作应该回滚,即回到A未扣除余额的状态。
  那么,我们首先演示在未加入事务管理前,“银行转账”这一案例可能出现的问题;而后使用Spring的事务管理,加入事务,了解Spring中事务的管理或使用。

案例准备及问题分析

  (对于数据库的创建,以及表的创建在这里就不一一赘述,从案例中我们可以知道数据库名,以及表名,除此之外,所使用到的表也只有一个即user表,其中包含的字段为id和balance。)
  创建一个新的工程,导入相关jar包,除包括Spring基础jar包外,还需要导入JDBC模板开发包和对应的数据库驱动,此外为了方便测试还需引入junit相关的jar包,为后续Spring事务管理的使用,在这里也导入事务相关的jar包,包括如下:

  Spring基础jar包:
1. spring-beans
2. spring-context
3. spring-core
4. spring-expression
5. commons-logging-1.2.jar
6. log4j-1.2.17.jar
  Spring JDBC模板开发包:
1. spring-jdbc
2. spring-tx
  MySQL数据库驱动jar:
1. mysql-connector-java-5.1.46.jar
  c3p0相关的jar包:
1. c3p0-0.9.2.1.jar
2. mchange-commons-java-0.2.3.4.jar
  Spring AOP开发包:
1. aopalliance-1.0.jar
2. aspectjweaver-1.8.7.jar
3. spring-aop
4. spring-aspects
  junit相关的jar包:
1. junit-4.12.jar
2. hamcrest-core-1.3.jar

(1)创建Dao层BankDao类,使用JdbcTemplate实现为某一用户增加余额和减少余额的方法,如下:

package com.wm103.tx;

import org.springframework.jdbc.core.JdbcTemplate;

/**
 * Created by DreamBoy on 2018/4/5.
 */
public class BankDao {
    JdbcTemplate jdbcTemplate;

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

    public void addBalance(long id, double money) {
        String sql = "UPDATE user SET balance = balance + ? WHERE id = ?";
        jdbcTemplate.update(sql, money, id);
    }

    public void deductBalance(long id, double money) {
        String sql = "UPDATE user SET balance = balance - ? WHERE id = ?";
        jdbcTemplate.update(sql, money, id);
    }
}

(2)创建Service层的BankService类,实现从某一用户到另一用户的转账操作,如下:

package com.wm103.tx;

/**
 * Created by DreamBoy on 2018/4/5.
 */
public class BankService {
    private BankDao bankDao;

    public void setBankDao(BankDao bankDao) {
        this.bankDao = bankDao;
    }

    public void transfer1(final long formId, final long toId, double amount) {
        bankDao.deductBalance(formId, amount);
        bankDao.addBalance(toId, amount);
    }
}

(3)创建Spring核心配置文件,在文件中引入相关依赖,并配置c3p0连接池,完成一些案例所需的注入操作,如下:

<?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.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">

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

    <!-- 创建JdbcTemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 创建service和dao对象,在service注入dao对象 -->
    <bean id="bankService" class="com.wm103.tx.BankService">
        <property name="bankDao" ref="bankDao"></property>
    </bean>

    <bean id="bankDao" class="com.wm103.tx.BankDao">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>
</beans>

(4)创建测试类TestTx,测试转账功能,如下:

package com.wm103.tx;

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

import java.io.IOException;

/**
 * Created by DreamBoy on 2018/4/5.
 */
public class TestTx {

    @Test
    public void run() throws IOException {
        ApplicationContext context = new ClassPathXmlApplicationContext("bean4.xml");
        BankService bankService = (BankService) context.getBean("bankService");
        bankService.transfer1(12, 13, 100);
    }
}

(5)运行测试类的测试方法,查看数据库中user表的修改结果,我们可以发现操作正常,达到预期效果。
(6)为模拟转账过程中出现的问题,我们修改BankService中的transfer1方法,如下:

public void transfer1(final long formId, final long toId, double amount) {
        bankDao.deductBalance(formId, amount);
        int err = 10 / 0;
        bankDao.addBalance(toId, amount);
    }

(7)重新运行测试类的测试方法,查看数据库中user表的修改结果,这时我们发现,用户A的余额被正确扣除,但用户B的余额并未增加。实际上当一个操作失败时,之前所有的操作应进行回滚。下面我们使用Spring的事务管理对该案例进行修改。

声明式事务管理

事务管理器

  实现Spring的事务管理,需要使用到事务管理器(PlatformTransactionManager)。Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现。如下:

事务 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 使用Spring JDBC或iBatis进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager 使用Hibernate5.0版本进行持久化数据时使用
org.springframework.orm.jpa.JpaTransactionManager 使用JPA进行持久化时使用
org.springframework.jdo.JdoTransactionManager 使用JDO进行持久化时使用
org.springframework.transaction.jta.JtaTransactionManager 使用一个JTA实现来管理事务,在一个事务跨多个资源时必须使用

  注:在本案例中采用的Spring JDBC操作数据库,即使用DataSourceTransactionManager这一事务管理器实现类。

基于XML配置声明式事务

XML配置

  在项目的Spring核心配置文件中进行配置,如下:
(1)配置事务管理器,即配置DataSourceTransactionManager,设置数据源;
(2)配置事务增强;
(3)配置切面,定义切入点,应用事务增强。
  具体如下:

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 设置进行事务操作的方法匹配规则 -->
        <!-- name: Service层中需要事务增强的方法名,其中这里表示名称以transfer开头的所有方法 -->
        <!-- propagation="REQUIRED"代表支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择 -->
        <tx:method name="transfer*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
<!-- 配置切面 -->
<aop:config>
    <!-- 切入点 -->
    <aop:pointcut id="bankPointcut" expression="execution(* com.wm103.tx.BankService.transfer*(..))"/>
    <!-- 切面 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="bankPointcut"/>
</aop:config>

事务配置说明

  tx:method的属性如下:
1. name,该属性必须设置值,其值表示与事务关联的方法(名)。该属性值可以使用通配符*,用来指定一批关联到相同的事务属性的方法,如get*insert*等。应用事务的方法需要存在满足该属性值的tx:method(事务属性配置),同时在定义切入点后该方法“受到”事务的增强,才能真正的实现事务;
2. propagation,默认值为REQUIRED,事务的传播行为(所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。),包括:REQUIREDREQUIRES_NEWSUPPORTSNOT_SUPPORTEDNEVERMANDATORYNESTED。这些属性值在TransactionDefinition中也对应定义了如下几个表示传播行为的常量:

常量名 说明
PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
PROPAGATION_REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起
PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起
PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常
PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

3. isolation,默认值为DEFAULT,表示事务隔离级别(事务隔离级别指若干个并发的事务之间的隔离程度),该属性值默认为对应数据库设置的隔离级别。具体如下:

隔离级别 含义
DEFAULT 使用后端数据库默认的隔离级别(Spring中的选择项),对大部分数据库而言,通常这值就是READ_COMMITTED;但是MySQL的默认隔离级别是REPEATABLE_READ
READ_UNCOMMITTED 一个事务可以读取另一个事务修改但还没有提交的数据,可能导致脏读、幻读、不可重复读,因此很少使用该隔离级别
READ_COMMITTED 一个事务只能读取另一个事务已经提交的数据,可防止脏读,但幻读和不可重复读仍可发生,这也是大多数情况下的推荐值
REPEATABLE_READ 一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同,除非数据被事务本身改变。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。可防止脏读、不可重复读,但幻读仍可能发生
SERIALIZABLE 所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,完全服从ACID的隔离级别,确保不发生脏读、幻读、不可重复读。这在所有的隔离级别中是最慢的,它是典型地通过完全锁定在事务中涉及的数据表来完成的,因此通常情况下也不会用到该级别

4. timeout,默认值为-1(表示永不超时),事务超时的时间,单位为秒;
5. read-only,默认值为false,即事务不是只读的,该属性表示事务是否只读。对于该属性值,针对事务内均为查询操作时才可以设置为true,对于增加删除修改的操作则不允许read-only设置为true。至于该字段的含义或者存在的意义,可以参考如下资料的说明:

百度知道 标题:spring事务管理属性为只读是什么意思?
  您好
  如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性;
  如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持read-only=”true”表示该事务为只读事务,比如上面说的多条查询的这种情况可以使用只读事务,
由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段,例如Oracle对于只读事务,不启动回滚段,不记录回滚log。
(1)在JDBC中,指定只读事务的办法为: connection.setReadOnly(true);
(2)在Hibernate中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER);
此时,Hibernate也会为只读事务提供Session方面的一些优化手段;
(3)在Spring的Hibernate封装中,指定只读事务的办法为: bean配置文件中,prop属性增加“read-Only”或者用注解方式@Transactional(readOnly=true)。Spring中设置只读事务是利用上面两种方式(根据实际情况)。
  在将事务设置成只读后,相当于将数据库设置成只读数据库,此时若要进行写的操作,会出现错误。
  希望对你有帮助。

  我们可以使用上述案例,在配置文件中进行如下修改(即加入read-only属性的设置,并设置true):

<tx:method name="transfer*" propagation="REQUIRED" read-only="true"/>

  再次运行测试类,我们会发现新的报错提示,如下:

org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [UPDATE user SET balance = balance - ? WHERE id = ?]; Connection is read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

  原因即设置read-only="true"后,我们在事务中进行了写操作,结果抛出异常。
6. rollback-for,可不设置,表示被触发进行回滚的异常,设置多个异常类则以逗号分隔,如:com.wm103.MyException,ServletException
7. no-rollback-for,可不设置,表示不被触发进行回滚的异常,设置多个异常类则以逗号分隔,如:com.wm103.MyException,ServletException
  涉及到这两个属性值,那么这里要对Spring声明式事务管理的默认回滚规则加以说明。在Spring事务管理中默认的回滚规则为:如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常(必检异常),则仍然提交事务。
  为了验证上述的结论,我们将对上述案例进行修改,修改Service类中的transfer1方法,让其抛出一个必检异常,这里我抛出一个IOException,如下:
  (实际上,在上述案例中模拟转账过程中出现的异常int err = 10 / 0即为运行异常,测试后,我们可以发现确实事务发生了回滚。)

public void transfer1(final long formId, final long toId, double amount) throws IOException {
    bankDao.deductBalance(formId, amount);
    //int err = 10 / 0;
    if(true) {
        throw new IOException("TEST ROLLBACK");
    }
    bankDao.addBalance(toId, amount);
}

  修改后,再运行测试类的测试方法,我们可以发现用户A扣钱后,发生异常,到事务并未回滚。为使得发生这样的异常时也触发回滚操作,需要设置rollback-for属性,如下:

<tx:method name="transfer*" propagation="REQUIRED" rollback-for="Exception"/>

  相反地,如果想让发生运行时异常时不触发回滚操作,那可以设置属性no-rollback-for="RuntimeException"

基于注解实现声明式事务

注解配置

  在项目的Spring核心配置文件中进行配置,如下:
(1)配置事务管理器,即配置DataSourceTransactionManager,设置数据源;
(2)配置开启事务注解。
  具体如下(这里我们创建一个新的Spring核心配置文件bean5.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.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">

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

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 配置开启事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <!-- 创建JdbcTemplate对象 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 创建service和dao对象,在service注入dao对象 -->
    <bean id="bankService" class="com.wm103.tx.BankService">
        <property name="bankDao" ref="bankDao"></property>
    </bean>

    <bean id="bankDao" class="com.wm103.tx.BankDao">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>
</beans>

(3)在类或者方法上应用@Transactional注解,如:

import org.springframework.transaction.annotation.Transactional;

//@Transactional
@Transactional(rollbackFor = Exception.class)
public class BankService {

或者

@Transactional(rollbackFor = Exception.class)
public void transfer1(final long formId, final long toId, double amount) throws IOException {

  针对该案例,我们在此之前对方法做了如下修改,即抛出一个必检异常:

public void transfer1(final long formId, final long toId, double amount) throws IOException {
    bankDao.deductBalance(formId, amount);
    //int err = 10 / 0;
    if(true) {
        throw new IOException("TEST ROLLBACK");
    }
    bankDao.addBalance(toId, amount);
}

  因此为修改Spring默认事务的回滚规则,可以设置注解@TransactionalrollbackForException.class,即@Transactional(rollbackFor = Exception.class)。同样地,如果不希望运行时异常触发回滚操作,可以设置@Transactional(noRollbackFor = RuntimeException.class)
  其他关于@Transactional的属性在此将不做过多介绍,大体上XML配置时涉及到的属性一致。

@Transactional注意事项

摘录自“Transactional注解中常用参数说明”一文,链接在“知识点扩展或参考”这一部分
1. @Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
2. @Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。然而,请注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据,能够被可以识别 @Transactional 注解和上述的配置适当的具有事务行为的beans所使用。上面的例子中,其实正是 元素的出现 开启 了事务行为。
3. Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。你当然可以在接口上使用 @Transactional 注解,但是这将只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(将被确认为严重的)。因此,请接受Spring团队的建议并且在具体的类上使用 @Transactional 注解。  

知识点扩展或参考

  1. 全面分析 Spring 的编程式事务管理及声明式事务管理
  2. 数据库事务隔离级别– 脏读、幻读、不可重复读(清晰解释)
  3. MySQL的InnoDB默认隔离级别的幻读问题
  4. MySQL使用可重复读作为默认隔离级别的原因
  5. 检查异常和未检查异常不同之处
  6. 加上事务aop后项目启动报错解决方法参考
  7. Spring事务失效的原因
  8. spring 中常用的两种事务配置方式以及事务的传播性、隔离级别
  9. spring事务管理属性为只读是什么意思?
  10. Transactional注解中常用参数说明

猜你喜欢

转载自blog.csdn.net/qq_15096707/article/details/79828290