Spring中进行事务管理的两种方式

1、Spring中事务管理的API

事务是指逻辑上要么全部成功、要么全部失败的一组操作。例如用户A给用户B转账,则用户A账户余额减少、用户B账户增加这两个操作就是一组事务,必须全部成功或失败撤回操作,不能出现A账户余额减少,B增加失败的情况。事务具有如下几个特性:

  • 原子性:事务的操作不可分割
  • 一致性:账户A的减少和B的增加一起发生
  • 隔离性:在多个事务操作时,彼此之间互不干扰
  • 持久性:事务提交到数据库之后永久的改变

spring提供了三个接口用于实现事务的管理,在进行事务管理时,spring首先会读取TransactionDefinition中隔离、传播、超时等事务定义信息,然后PlatformTransactionManager会根据这些信息进行事务管理,然后将产生的事务状态保存在TransactionStatus中

PlatformTransactionManager

spring为不同的持久层框架提供了相应的PlatformTransactionManager接口实现,spring JDBC和MyBatis对应DataSourceTransactionManager、Hibernate对应HibernateTransactionManager,还有JPA、Jdo、JTA等,不同的持久层使用不同的实现类

org.springframework.jdbc.datasource.DataSourceTransaction.Manager
Spring JDBC或iBatis逬行持久化数据时使用
org.springframework.orm.hibernate3.HibernateTransactionManager
Hibernate3.0版本进行持久化数据时使用
org.springframework.orm.jpa.JpaTransactionManager
JPA进行持久化时使用
org.springframework.jdo.JdoTransaction Manager
当持久化机制是Jdo时使用
org.springframework.transactionjta.JtaTransactionManager
JTA管理事务,在一个事务跨越多 个资源时必须使用

TransactionDefinition

事务定义信息接口中ISOLATION开头的常量用于定义事务的隔离级别、PROPAGATION开头定义事务传播行为、TIMEOUT_DEFAULT定义超时时间。

隔离级别用于解决多个事务提交时可能出现的脏读、不可重复读、

  • 脏读:一个事务读取了另一个事务还未提交的数据
  • 不可重复读:一个事务读取了另一个事务更新(update)前、后的数据,导致前后两次读取数据不一致
  • 幻读:一个事务读取时,另一个事务进行插入(insert),导致读取到了之前没有的记录

事务的隔离级别有如下四种类型,如果为DEFAULT则使用后端数据库默认的隔离级别,例如MySQL使用的是repeatable_read,Oracle数据库使用的是read_committed级别

READ_UNCOMMITED 允许你读取还未提交的改变了的数据。可能导致脏、幻、不可重复读
READ_COMMITTED 允许在并发事努已经提交后读取。可防止脏读,但幻读和不可重复读仍可发生
REPEATABLE_READ 对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可重复读,但幻读仍可能发生。
SERIALIZABLE 完全服从ACID的隔离级别,通过完全锁定事务中涉及的数据表来确保不发生脏、幻、不可重复读,但执行效率最低

事务的传播行为用于解决业务层方法互相调用时如何传递事务的问题。例如方法a和b中都用到了事务T,那么a在调用b时是新建一个事务T还是使用b中的事务T呢?有如下七种传播方式

PROPAGATION_REQUIRED 支持当前事务,如果不存在就新建一个
PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务
PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常
PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事劳,创建一个新的事务
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务
PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常
PROPAGATION_NESTED 如果当前事务存在,则嵌套执行事务

TransactionStatus

用于记录事务是否完成、是否产生保存点、是否可回滚等状态。

2、编程式事务管理

在Spring中使用事务有两种方式,第一种是通过手动编写来实现一个事务。如下左图所示为一个用户账户的数据表,通过事务管理实现根据id对账户的money进行转账,以实现一个账户money减少的同时另一个增加。

   

1、引入jar包。上面右图所示为项目的目录结构,lib文件夹中包含了项目依赖的jar包,包括Spring基础包commons-logging、spring-core、spring-beans、spring-context、spring-expression、spring-test,以及连接数据库的spring-jdbc、mysql-connector,事务管理spring-tx,此外要用到c3p0对数据库连接池进行管理,除了引入c3p0包之外还需要引入mchange-commons包才可以使用。

2、配置数据源连接,在jdbc.properties文件中完成数据库连接的配置

jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=1234

接着在配置文件spring-transaction.xml中引入properties并将属性注入c3p0的配置中,完成dataSource的配置

    <!-- 引入属性文件jdbc.properties -->
    <context:property-placeholder location="jdbc.properties"/>

    <!-- 配置c3p0连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="user" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

3、实现数据对象的DAO类,在AccountDao类中通过继承spring的JdbcDaoSupport类,可以使用JdbcTemplate中的update()方法完成对数据库的更新操作。由于JdbcDaoSupport需要DataSource获取数据库的连接,因此在AccountDao的Bean配置中通过属性注入c3p0数据源dataSource

package com.transaction;

import org.springframework.jdbc.core.support.JdbcDaoSupport;

public class AccountDao extends JdbcDaoSupport {
    public void transferIn(int id,double money){
        String sql="UPDATE customers SET money = money + ? WHERE id =?";
        this.getJdbcTemplate().update(sql,money,id);        //使用JdbDaoSupport类的方法操作数据库
    }
    public void transferOut(int id,double money){
        String sql="UPDATE customers SET money = money - ? WHERE id =?";
        this.getJdbcTemplate().update(sql,money,id);
    }
}
    <!-- DAO类 -->
    <bean class="com.transaction.AccountDao" id="accountDao">
        <!-- SpringJdbc需要注入连接池DataSource作为参数来初始化 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

4、实现事务操作Service类。在AccountService类中完成事务操作,spring编程式的事务管理通过Spring的TransactionTemplate类来实现,将一个事务操作放在其execute()方法内完成,execute()需要传入一个TransactionCallback类作为参数,这里采用匿名内部类的方式实现。在内部类中doIntransaction()方法中执行事务操作,通过调用DAO层的方法完成具体数据库操作

package com.transaction;

import org.springframework.transaction.support.TransactionTemplate;

public class AccountService {
    private AccountDao accountDao;
    private TransactionTemplate transactionTemplate;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
        this.transactionTemplate = transactionTemplate;
    }

    public void transfer(int inAccount, int outAccount, double money) {
        //使用spring的TransactionTemplate进行事务操作
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                accountDao.transferIn(inAccount, money);
                int i = 1/0;            //除数为0会抛出异常
                accountDao.transferOut(outAccount, money);
                return null;
            }
        });
    }
}

AccountService类中用到了accountDao和transactionTemplate两个属性,因此在配置Bean时需要诸如两个bean对象。accountDao类是之前创建的,那么transactionTemplate类哪里来的呢?transactionTemplateDataSourceTransactionManager创建的,之前提到它是PlatformTransactionManager接口用于spring jdbc的一个实现类。所以需要先创建一个transactionManager的bean,而要创建它还需要注入DataSource作为属性。

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

    <!-- 事务管理模板 -->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>

    <!-- 业务层类 -->
    <bean id="accountService" class="com.transaction.AccountService">
        <property name="accountDao" ref="accountDao"/>
        <property name="transactionTemplate" ref="transactionTemplate"/>
    </bean>

项目的数据流动如下:

最后测试AccountService类,调用accountService对象的transfer()方法从id为2的向1转账50。由于在事务中进行了除以0的操作,事务执行失败并且回滚,数据库操作不生效

class AccountServiceTest {
    @org.junit.jupiter.api.Test
    void transfer() {
        ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
        AccountService accountService= appCtx.getBean("accountService",AccountService.class);
        accountService.transfer(2,1,50);
    }
}

3、声明式事务管理

Spring基于声明式的事务管理是利用AOP的思想,因此需要额外引入spring-aop和aopalliance两个jar包。由于SpringAOP的使用有三种方式,所以声明式事务管理也有三种:通过spring的TransactionProxy、使用<aop>标签、使用注解的方式

基于TransactionProxy的方式

在accountService类的基础上通过TransactionProxyBeanFactory产生代理类serviceProxy来对事务进行管理,由于是通过AOP引入的方式,所以原来的accountService类不需要增加transactionTemplate

public class AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void transfer(int inAccount, int outAccount, double money) {
        accountDao.transferIn(inAccount, money);
                int i = 1/0;            //除数为0会抛出异常
        accountDao.transferOut(outAccount, money);
    }
}

xml文件中也不需要对TransactionTemplate进行配置,只需要保留TransactionManager,但是需要新配置代理类serviceProxy,代理配置时需要注入属性--代理目标对象、事务管理器和事务属性。在transactionAttributes中通过<prop>键值对标签配置具体transfer()方法的事务属性,其中PROPAGATION_开头的是传播行为,ISOLATION_开头的是隔离级别,+开头的定义发生哪些异常后事务不回滚,-开头定义发生哪些异常回滚。

    <!-- 业务层代理 -->
    <bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <!-- 代理目标 -->
        <property name="target" ref="accountService"/>
        <!-- 注入事务管理器 -->
        <property name="transactionManager" ref="transactionManager"/>
        <!-- 配置事务属性 -->
        <property name="transactionAttributes">
            <props>
                <prop key="transfer">PROPAGATION_REQUIRED,ISOLATION_REPEATABLE_READ,+ArithmeticException</prop>
            </props>
        </property>
    </bean>

在使用时通过serviceProxy获取代理Bean,通过增强后的代理调用transfer()

    @Test
    void transfer() {
        ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
        AccountService accountService= appCtx.getBean("serviceProxy",AccountService.class);
        accountService.transfer(1,2,50);
    }

基于AspectJ的<aop>标签

由于上面的方法需要为每一个事务对象单独配置一个代理类,较为繁琐,所以实际中不常用。使用AspectJ不产生代理类,而是直接织入到accountService中。由于需要使用aspectJ进行织入,所以需要引入aspectjweaver.jar包。此外在xml配置文件中使用到了<tx>和<aop>标签,需要在<beans>声明。然后通过<tx>标签定义事务通知,其中的<tx:method>中可以对具体事务方法进行配置,包括隔离级别isolation、传播属性propagation、不回滚的异常。在<aop:config>标签值对切面进行配置,包括切入点和通知。

<?xml version="1.0" encoding="UTF-8"?>
<!-- 声明tx、aop等标签 -->
<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"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    ......

    <!-- 配置事务AOP的通知-->
    <tx:advice id="serviceAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" no-rollback-for="ArithmeticException"/>
        </tx:attributes>
    </tx:advice>

    <!-- 配置切面 -->
    <aop:config>
        <!-- 配置切入点 -->
        <aop:pointcut id="transferPointcut" expression="execution(* com.transaction.AccountService.*(..))"/>
        <!-- 配置通知 -->
        <aop:advisor advice-ref="serviceAdvice" pointcut-ref="transferPointcut"/>
    </aop:config>

</beans>

由于不产生代理类,可以直接使用accountService对象

    @Test
    void transfer() {
        ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
        AccountService accountService= appCtx.getBean("accountService",AccountService.class);
        accountService.transfer(1,2,50);
    }

基于注解的方式

通过注解来使用spring事务管理代码更为简洁,只需要在xml配置文件中配置事务管理器并开启事务注解即可

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

接着在具体要使用到事务的类上添加@Transactional注解即可。在注解中可以指定隔离级别、传播属性、回滚/不回滚异常等属性

package com.transaction;

import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,
        rollbackFor = java.lang.ArithmeticException.class)
public class AccountService {
    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public void transfer(int inAccount, int outAccount, double money) {
        accountDao.transferIn(inAccount, money);
        int i = 1/0;            //除数为0会抛出异常
        accountDao.transferOut(outAccount, money);
    }
}
发布了124 篇原创文章 · 获赞 65 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/theVicTory/article/details/105455500
今日推荐