Spring AOP(四)spring事务管理

1,110 阅读7分钟

spring事务管理

在了解事务的特点之后,会发现事务其实学问很多,但是疑惑的就是一般使用springboot在开发项目的使用,我们几乎自己很少写事务相关的东西,这是因为spring帮我们做了事务的管理,但是spring的事务管理可以帮助我们做到最基础的事务管理,如果我们的业务越来越复杂,出现多数据源的场景的时候或者出现需要精细化事务管理的时候,我们要是要自己进行处理。

其实spring在处理事务的时候采用了事务传播行为来管理事务,关于事务本身的介绍可以参考事务隔离级别和事务并发问题介绍。接下来就主要看一下spring的事务传播行为,事务传播行为是spring为了管理事务提出的概念,并非是数据库的事务概念这点需要清楚。事务传播行为的解释示例

spring事务传播行为

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

spring事务

在早期使用springmvc的时候,也是采用AOP的方式进行事务处理,但都是基于xml的方式,这种方式需要太多配置工作量大,并且在springboot应用之后,可以更好的使用注解来处理,因此xml的方式就不在使用了。 image.png xml配置代码

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    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-4.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        ">

    <!-- 加载资源文件  其中包含变量信息,必须在Spring配置文件的最前面加载,即第一个加载-->
    <context:property-placeholder location="classpath:mysql.properties" />
    
    <bean id="sessionFactory" 
        class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan">
            <list>
                <value>com.troyqu.entity</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
            </props>
        </property>
    </bean>
    
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <property name="driverClassName" value="${jdbc.driverClassName}" />
      <property name="url" value="${jdbc.url}" />
      <property name="username" value="${jdbc.user}" />
      <property name="password" value="${jdbc.pass}" />
   </bean>
   
    <!-- 配置Hibernate事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.orm.hibernate4.HibernateTransactionManager">
      <property name="sessionFactory" ref="sessionFactory" />
   </bean>
   
   <!--配置事务管理器为transactionManager -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED" />
            <tx:method name="get*" propagation="REQUIRED" />
            <tx:method name="*" read-only="true" />
        </tx:attributes>
    </tx:advice>
    
    <aop:config expose-proxy="true">
        <!-- 配置切点 -->
        <aop:pointcut id="txPointcut" expression="execution(* com.troyqu.service..*.*(..))" />
        <!-- 配置切点和advice -->
        <aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
    </aop:config>
    
</beans>

基于aop的spring事务

在springboot中我们也可以使用自定义注解结合AOP来实现事务管理,AOP的方式会比XML的方式更加简单和节省代码量,这里的AOP配置不是指使用@Transactional的声明式注解。这里指的是结合@Aspect的AOP方式。我们可以看一下关键代码。

数据库连接类MyConnection用来管理数据库连接,使用ThreadLocal来管理数据库连接同一个线程中可以不需要创建多个数据库连接。

package com.troyqu.aop;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Component
public class MyConnection {

    @Autowired
    DataSourceTransactionManager dataSourceTransactionManager;

    ThreadLocal<Connection> connection = new NamedThreadLocal<>("MyConnection");

    public Connection getConnection(){
        Connection conn = connection.get();
        if (conn != null) {
            return conn;
        }
        try {
             conn = dataSourceTransactionManager.getDataSource().getConnection();
             connection.set(conn);
            return conn;
        } catch (SQLException e) {
            e.printStackTrace();
            return null;
        }
    }


    public void closeConnection(){
        Connection conn = connection.get();
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                connection.remove();
            }
        }
        connection.remove();
    }
}

自定义注解作为事务声明切点

public @interface MyTransactional {
}

创建事务处理AOP类TransactionAop,用来处理标注了@MyTransactional的方法

package com.troyqu.aop;

import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.SQLException;

@Aspect
@Component
public class TransactionAop {

    @Autowired
    MyConnection myConnection;

    @Around("@annotation(com.anchnet.smartops.cmp.aop.MyTransactional)")
    public Object txAround(ProceedingJoinPoint pjp) throws Throwable {
        Connection conn = myConnection.getConnection();
        Object output = null;
        try {
            beginTransaction(conn);
            output = pjp.proceed();
            commitTransaction(conn);
        } catch (Throwable e) {
            rollBackTransaction(conn);
            System.out.println("Error happened inside TX");
            e.printStackTrace();
        }finally {
            myConnection.closeConnection();
            return output;
        }
    }

    private void beginTransaction(Connection conn){
        try {
            conn.setAutoCommit(false);
        } catch (SQLException e) {
            System.out.println("Start TX failed");
            e.printStackTrace();
        }
    }

    private void commitTransaction(Connection conn){
        try {
            conn.commit();
        } catch (SQLException e) {
            System.out.println("Commit failed");
            e.printStackTrace();
            myConnection.closeConnection();
        }
    }

    private void rollBackTransaction(Connection conn){
        try {
            conn.rollback();
        } catch (SQLException e) {
            System.out.println("Rollback failed");
            e.printStackTrace();
            myConnection.closeConnection();
        }
    }
}

声明式事务

目前可以使用更为简单的声明式事务来进行事务处理,只要开启了@EnableTransactionManagement 就可以使用声明式事务,spring-boot-autoconfigure中会引入相关Bean我们只需要使用就可以,开启事务支持只需要在对应的方法和类名上添加@Transactional就可以使用。@Transactional可以设置很多属性,包括事务管理器(transactionManager),事务传播级别(propagation)等。当我们使用单数据元的时候事务管理器可以选择默认的事务管理器,但是如果我们使用多数据源的时候,就需要管理好对应的事务管理器。 image.png

出现异常回滚事务

@Transactional使用,可以指定回滚的策略,例如出现下面代码当抛出Exception异常的时候进行回滚。 image.png

多事务管理器指定对应的事务管理器

分别给method1和method2指定不同的事务管理器。

    @Transactional(value="txManager1")
    public void method1(Connection conn){
        System.out.println("this is method1");
    }
    
    @Transactional(value="txManager2")
    public void method2(Connection conn){
        System.out.println("this is method2");
    }    

接下来举几个例子看下具体场景下事务的处理,这里为了简化代码都只是输出output模拟真正的数据库操作。 method1和method2中的数据库操作都会被回滚,因为method1和method2再同一个@Transactional声明的事务中。

    @Transactional
    public void tx(){
        method1();
        method2();
    }

    public void method1(){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    public void method2(){
        System.out.println("this is method2");
    }    

那么上面的场景因为method1和method2再同一个事务中,那么我们再method1和method2上面也添加上@Transactional注解这样不就各自都有独立的声明了,那么同样的场景method2的业务就可以正常操作了呢?这样也是不可以的因为spring事务默认的传播机制是: TransactionDefinition.PROPAGATION_REQUIRED如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务

    @Transactional
    public void tx(){
        method1();
        method2();
    }

    @Transactional
    public void method1(){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    @Transactional
    public void method2(){
        System.out.println("this is method2");
    }    

难道再使用@Transacal之后就没发办法做到事务隔离了吗?肯定不是的我们可以给method1和method2指定需要的事务传播机制就可以,给method1和method2配置Propagation.REQUIRES_NEW的传播机制后,那么即使再最外侧方法有@Transactional注解,但是Propagation.REQUIRES_NEW会给method1和method2各自创建一个新的事务,这时候method1会回滚,method2会正常处理。

    @Transactional
    public void tx(){
        method1();
        method2();
    }

    @Transactional(propagation= Propagation.REQUIRES_NEW)
    public void method1(){
        System.out.println("this is method1");
        int i = 10 / 0;
    } 

    @Transactional(propagation= Propagation.REQUIRES_NEW)
    public void method2(){
        System.out.println("this is method2");
    }    

到这里我们对于事务的控制已经有大致的了解,关于事务的具体控制一定是要结合我们的业务来灵活使用的,如果遇到多数据源的情况我们也需要将事务管理器考虑在内。

声明式事务@Transactional失效场景

  • 有异常需要抛出,如果catch异常后没有进行throw那么不会进行事务回滚,因为异常已经被捕获所以对于@Transactional而言方法最终无异常抛出;
@Transactional(rollbackFor = Exception.class)
@Override
public void unbind(Integer crmId) {
    try {
        checkIf(!UserUtils.isOpsUser(), AmsCode.USER_NOT_OPS);
        val crmCustomer = customerMapper.selectByPrimaryKey(crmId);
        checkIf(crmCustomer == null, AmsCode.CRM_ACCOUNT_NOT_FOUND);
        checkIf(crmCustomer.getStatus() != null && !CrmCustomerStatus.bound.name().equalsIgnoreCase(crmCustomer.getStatus()), AmsCode.CRM_CUSTOMER_NOT_BIND_ACCOUNT);
        val account = accountMapper.selectOne(new AmsAccount().setCrmId(crmId));
        if (account != null) {
            account.setCrmId(null);
            accountMapper.updateByPrimaryKey(account);
        }
        customerMapper.updateByPrimaryKey(crmCustomer.setStatus(CrmCustomerStatus.unbound.name()));
    } catch (Exception e) {
       //不会进行异常回滚,因为异常被捕获没有再抛出
       log.error("error happened {}", e);    
    }
}

以上代码中如果我们既要记录错误日志,也要处理事务那么可以在catch中再throw异常出来,关于catch的范围我们也要注意,如果多个业务操作的话要根据需要对独立的业务处理添加try-catch。

public class TestService {

    @Autowired
    TestServiceB b;

    @Autowired
    TestServiceC c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operator(){
        log.info("process test service");
        try {
            b.insertB();
            c.insertC();
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

public class TestService {

    @Autowired
    TestServiceB b;

    @Autowired
    TestServiceC c;

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
    public void operator1(){
        log.info("process test service");
        try {
            b.insertB();
        } catch (Exception e) {
            log.error(e.getMessage());
        }    
        
        try {
            c.insertC();
        } catch (Exception e) {
            log.error(e.getMessage());
        }         
    }
}
  • 方法必须是public修饰符,如果不是public修饰也不会报错,只是注解不会生效(因为动态代理jdk动态代理处理接口不包含非public方法,cglib处理public方法);
  • 通过this调用的方法注解是不生效的,通过this在当前类的内部进行调用不会创建新的代理,因此注解不会生效;

总结

  • springboot通过自动装配的方式提供了事务管理的相关依赖,但是如果我们业务场景复杂并且遇到多数据源场景,那么需要结合实际业务来灵活使用。
  • 使用声明式事务@Transactional的时候,可以通过配置事务管理器(value)和事务传播行为(propagation)的属性来控制满足自己的业务场景。