(02)验证Spring的事务及其7种传播机制真实案例

  原文:

  https://blog.csdn.net/soonfly/article/details/70305683

  https://www.cnblogs.com/dennyzhangdd/p/9602670.html

  https://blog.csdn.net/fly910905/article/details/80000242

  说起Spring的事务,仿佛是很熟悉的老朋友了,然鹅。。。现实却很残酷。。。起初我以 Spring、Mybatis、druid、Mysql尝试,发现其中一个问题,无论数据源的defaultAutoCommit设置为true或者false,事务总会自动提交。确定配置无误后,发现网上有一种说法是把数据库的autocommit设置为OFF,即关闭数据库的自动提交,且不说这样是否可以,这种方法本身就有问题。因为在代码中获取到一个Connection,执行commit即可提交,不执行commit就不会提交,完全可以由代码控制,去设置数据库本身,这很不合理。经过一番周折还是没有搞定这个问题。

  无奈之下我把Mybatis换成JdbcTemplate,终于正常了。下面基于Spring+JdbcTemplate+druid+Mysql说说事务。

  1、事务、事务传播机制的简单说明

  事务是一个单体行为,只有提交了事务,数据才会保存到数据库,否则不会保存到数据库中。事务传播行要求至少有两个东西,才可以发生传播。指的是当一个事务方法被另一个事务方法调用时,这个被调用方法的事务方法应该如何进行。例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

  2、defaultAutoCommit与Transactional的关系

  配置数据源时参数defaultAutoCommit设置为ture、false代表自动、不自动提交。Transactional注解也控制事务,他们有什么关系?下面用例子说明。

  (1)defaultAutoCommit为false,不使用Transactional注解。结论:不会提交

<property name="defaultAutoCommit" value="false" />
public void save() {
  testDao.save();
}

   (2)defaultAutoCommit为false,使用Transactional注解。结论:会提交

<property name="defaultAutoCommit" value="false" />
@Transactional
public void save() {   testDao.save(); }

  (3)defaultAutoCommit为true,不使用Transactional注解。结论:会提交

<property name="defaultAutoCommit" value="true" />
public void save() {
  testDao.save();
}

  (4)defaultAutoCommit为true,使用Transactional注解。结论:会提交

<property name="defaultAutoCommit" value="true" />
@Transactional
public void save() {
  testDao.save();
}

  总结:只要defaultAutoCommit或者Transactional有一项设置为可提交即可。

  3、Transactional与异常自动回滚的关系

  在项目中希望当方法产生异常时自动回滚事务,下面我们在defaultAutoCommit设置为false的情况下进行验证

<property name="defaultAutoCommit" value="false" />

  (1)使用Transactional的默认配置,抛出检查型异常。事务不会回滚

@Transactional
public void save () throws Exception {
  testDao.save();
  throw new ClassNotFoundException();
}

  (2)使用Transactional的默认配置,抛出运行时异常。事务会回滚

@Transactional
public void save (){
  testDao.save();
  throw new RuntimeException();
}

  (3)使用Transactional注解,指定rollbackFor为抛出的异常或其父类时,检查型异常会回滚

@Transactional(rollbackFor=Exception.class)
public void save () throws Exception {
  testDao.save();   
throw new ClassNotFoundException(); }

  (4)使用Transactional注解,指定rollbackFor不是抛出的异常或其父类时,运行时异常会回滚(运行时异常与rollbackFor无关,肯定回滚)

@Transactional(rollbackFor=FileNotFoundException.class)
public void save () throws Exception {
  testDao.save();
  throw new RuntimeException();
}

  (5)使用Transactional注解,捕获异常,事务不会回滚

@Transactional
public void save () throws Exception {
  try {
    testDao.save();
    throw new RuntimeException();
  } catch (Exception e) {
  // TODO: handle exception
  }
}

@Transactional
public void save () throws Exception {
  try {
    testDao.save();
    throw new ClassNotFoundException();
  } catch (Exception e) {
    // TODO: handle exception
  }
}

  (6)捕获的异常需要手动回滚,手动回滚时检查型异常可以不指定rollbackFor

@Transactional
public void save () {
  try {
    testDao.save();
    throw new ClassNotFoundException();
  } catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
  }
}

@Transactional(rollbackFor=FileNotFoundException.class)
public void save () {
  try {
    testDao.save();
    throw new ClassNotFoundException();
  } catch (Exception e) {
  TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
  }
}

@Transactional
public void save () {
  try {
    testDao.save();
    throw new RuntimeException();
  } catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 
  }
}

  (7) Transactional要加在主动(直接)调用的方法上面,以下事务不会提交,没有开启事务(spring容器管理的类直接调用test)

public void test(){
    save();
}

@Transactional
public void save () {
    try {
        testDao.save();
        throw new RuntimeException();
    } catch (Exception e) {
    }
}    

  4、spring中的事务传播行为

  spring中共有7种事务传播行为,分别介绍如下:

  (1)PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。

  方法A加注解,方法B也加注解,当方法A运行时会开启事务A,调用方法B时,方法B也加入到事务A中

 @Transactional(propagation = Propagation.REQUIRED)
 public void methodA() {
   methodB();
   testDao.methodA();
 }

 @Transactional(propagation = Propagation.REQUIRED)
 public void methodB() {
   testDao.methodB();
 }

   如上图,总共开启了一个事务。

  (2)PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务,如果没有事务,则不会开启事务。

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
  methodB();
  testDao.methodA();
}

@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
  testDao.methodB();
}

  如果直接调用methodA,methodA会开启一个事务,methodA调用methodB,则methodB支持当前methodA开启的事务,如下图:

  如果直接调用methodB,不会开启事务,如下图:

  如果直接调用methodA,由于methodA是SUPPORTS,不会开始事务,methodB不是直接调用,也不会开启事务

@Transactional(propagation = Propagation.SUPPORTS)
public void methodA() {
  methodB();
  testDao.methodA();
} @Transactional(propagation
= Propagation.REQUIRED)   public void methodB() {   testDao.methodB(); }

  (3)PROPAGATION_MANDATORY:必须在一个事务中运行,否则报异常

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
methodB();
testDao.methodA();
}

@Transactional(propagation
= Propagation.MANDATORY)   public void methodB() {   testDao.methodB(); }

  直接调用methodA,开启一个事务,methodB也在该事务中运行

  直接调用methodB,报异常 No existing transaction found for transaction marked with propagation 'mandatory'

 @Transactional(propagation = Propagation.MANDATORY)
 public void methodB() {
   testDao.methodB();
 }

  (4)PROPAGATION_REQUIRES_NEW:开启一个新事务。如果一个事务已经存在,则先将存在的事务挂起,执行完这个新事务,再执行挂起的事务,两个事务的成功或失败没有联系。

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
  methodB();
  testDao.methodA();
}
@Transactional(propagation
= Propagation.REQUIRES_NEW) public void methodB() {   testDao.methodB(); }

  从上图中看到,并没有挂起旧事务,先执行新事务,因为只有使用JtaTransactionManager作为事务管理器时才生效。后面再研究。。。

  (5)PROPAGATION_NOT_SUPPORTED:在非事务中运行。如果有事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行。

 @Transactional(propagation = Propagation.REQUIRED)
 public void methodA() {
   methodB();
    testDao.methodA();
 }

 @Transactional(propagation = Propagation.NOT_SUPPORTED)
 public void methodB() {
   testDao.methodB();
 }

  直接调用methodA,运行到methodB,事务应该挂起,即methodB对应的数据不会保存到数据库。

  但上图与预期的不一致,因为也需要JtaTransactionManager作为事务管理器 。

  直接调用methodB不会开启事务,可以自己尝试一下。

  (6)PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常。

@Transactional(propagation = Propagation.NEVER)
public void methodB() {
  testDao.methodB();
}

  直接调用methodB,不会开启事务

 @Transactional(propagation = Propagation.REQUIRED)
 public void methodA() {
   methodB();
   testDao.methodA();
 }

 @Transactional(propagation = Propagation.NEVER)
 public void methodB() {
   testDao.methodB();
 }

  直接调用methodA,报异常,发现下面日志没有报异常,,,是版本问题还是我的理解有误???先留个疑问吧

  (7) PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按PROPAGATION_REQUIRED属性执行。

  附 相关配置文件和代码

  pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.sl</groupId>
  <artifactId>spring-web-transaction</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  
  <!-- 项目属性 -->
  <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <tomcat.version>2.2</tomcat.version>
    
      <spring.version>5.1.0.RELEASE</spring.version>
      <!-- 声明式事务 -->
      <aspectjweaver.version>1.7.4</aspectjweaver.version>
      <druid.version>1.1.11</druid.version>
      <mysql.driver.version>5.1.30</mysql.driver.version>
      
    <jackson.version>2.5.4</jackson.version>
      <slf4j.version>1.7.7</slf4j.version>
  </properties>
  
  <!-- 依赖 -->
  <dependencies>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    
    <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectjweaver.version}</version>
        </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>
      
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>${druid.version}</version>
    </dependency>
    
    <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.driver.version}</version>
            <scope>runtime</scope>
        </dependency>
    
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    
  </dependencies>
  
  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <webappDirectory>${project.build.directory}/${project.artifactId}</webappDirectory>
                    <warName>${project.artifactId}</warName>
                </configuration>
            </plugin>    
            <!-- tomcat7插件 -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>${tomcat.version}</version> 
                <configuration>
                    <port>8080</port>
                    <path>/${project.artifactId}</path>
                    <uriEncoding>${project.build.sourceEncoding}</uriEncoding>
                </configuration>
            </plugin>    
            
        </plugins>
    </build>
  
</project>
View Code

  spring-context.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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                  http://www.springframework.org/schema/beans/spring-beans.xsd
                  http://www.springframework.org/schema/aop
                  http://www.springframework.org/schema/aop/spring-aop.xsd
                  http://www.springframework.org/schema/context
                  http://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/task  http://www.springframework.org/schema/task/spring-task-3.1.xsd
                  ">
                 
    <!-- 启用注解 -->
        <context:component-scan base-package="com.sl.*">
            <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
        </context:component-scan>
         <tx:annotation-driven transaction-manager="transactionManager"/>  

    <!--读取配置文件;可以读取多个-->
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:db.properties</value>
            </list>
        </property>
    </bean>
    
    <!-- 阿里 druid数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <!-- 数据库基本信息配置 -->
        <property name="url" value="${url}" />
        <property name="username" value="${username}" />
        <property name="password" value="${password}" />
        <property name="driverClassName" value="${driverClassName}" />
        <property name="defaultAutoCommit" value="false" />
        <property name="filters" value="${filters}" />
        <!-- 最大并发连接数 -->
        <property name="maxActive" value="${maxActive}" />
        <!-- 初始化连接数量 -->
        <property name="initialSize" value="${initialSize}" />
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${maxWait}" />
        <!-- 最小空闲连接数 -->
        <property name="minIdle" value="${minIdle}" />
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
        <property name="validationQuery" value="${validationQuery}" />
        <property name="testWhileIdle" value="${testWhileIdle}" />
        <property name="testOnBorrow" value="${testOnBorrow}" />
        <property name="testOnReturn" value="${testOnReturn}" />
        <property name="maxOpenPreparedStatements" value="${maxOpenPreparedStatements}" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="${removeAbandoned}" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="${logAbandoned}" />
    </bean>
    
        <!--配置数据库连接-->
    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> 
 <property name="dataSource" ref="dataSource"></property>
</bean>
   

</beans>
View Code

  spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">


    <context:component-scan base-package="com.sl.controller" />
     <mvc:default-servlet-handler/>

    <!-- 对静态资源文件的访问  restful-->

    <!--  -->
    <mvc:annotation-driven>
    </mvc:annotation-driven>
    
    <!-- 配置SpringMVC的视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/"/>
        <property name="suffix" value=".jsp"/>
    </bean>


</beans>
View Code

  log4j.properties

# DEBUG,INFO,WARN,ERROR,FATAL
log4j.rootLogger=DEBUG,CONSOLE,FILE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} %C{1}@(%F:%L):%m%n

log4j.appender.FILE=org.apache.log4j.DailyRollingFileAppender
log4j.appender.FILE.File=${catalina.base}/logs/spring-web.log
log4j.appender.FILE.Encoding=utf-8
log4j.appender.FILE.DatePattern='.'yyyy-MM-dd
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss} %C{1}@(%F:%L):%m%n

log4j.logger.com.mybatis=DEBUG
log4j.logger.com.mybatis.common.jdbc.SimpleDataSource=DEBUG 
log4j.logger.com.mybatis.common.jdbc.ScriptRunner=DEBUG 
log4j.logger.com.mybatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG 
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
View Code

  db.properties

url:jdbc:mysql://localhost:3309/mytest?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 
driverClassName:com.mysql.jdbc.Driver
username:root
password:123456
       
filters:stat
   
maxActive:20
initialSize:1
maxWait:60000
minIdle:10
maxIdle:15
   
timeBetweenEvictionRunsMillis:60000
minEvictableIdleTimeMillis:300000
   
validationQuery:SELECT 'x'
testWhileIdle:true
testOnBorrow:false
testOnReturn:false

maxOpenPreparedStatements:20
removeAbandoned:true
removeAbandonedTimeout:1800
logAbandoned:true
View Code

  TestController.java

package com.sl.controller;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sl.service.TestService;

@Controller
@RequestMapping("/test")
public class TestController {

    @Autowired
    private TestService testService;
    
    @RequestMapping("/save")
    @ResponseBody
    public void save(){
        //testService.save();
        testService.methodA();
    }
    
    @RequestMapping("/del")
    @ResponseBody
    public void del() throws Exception {
        testService.del();
    }
    
    @RequestMapping("/get")
    @ResponseBody
    public String get(){
        String str= "...";
        List<Map<String, Object>> list = testService.get();
        for(Map<String, Object> map : list) {
            str = map.get("name").toString();
        }
        return str;
    }
    
    @RequestMapping("/update")
    @ResponseBody
    public void update() throws Exception {
        testService.update();
    }
}
View Code  

  TestService.java

package com.sl.service;

import java.util.List;
import java.util.Map;

public interface TestService {
    public void save() throws Exception;
    public void del();
    public void test();
    public List<Map<String, Object>> get();
    public void update();
    public void methodA();
    public void methodB();
}
View Code

  TestServiceImpl.java

package com.sl.service.impl;

import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.sl.dao.TestDao;
import com.sl.service.TestService;

@Service
public class TestServiceImpl implements TestService{

private static Logger logger=LoggerFactory.getLogger(TestServiceImpl.class);

    @Autowired
    private TestDao testDao;
    

    public void test(){
        save();
    }

    @Transactional
    public void save () {
        try {
            testDao.save();
            throw new RuntimeException();
        } catch (Exception e) {
        }
    }
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
     methodB();
     testDao.methodA();
    }

    @Transactional(propagation = Propagation.NEVER)
    public void methodB() {
        testDao.methodB();
    }

    @Override
    @Transactional(propagation=Propagation.NEVER)
    public void del() {
        // TODO Auto-generated method stub
        testDao.del();
    }

    @Override
    public List<Map<String, Object>> get() {
        // TODO Auto-generated method stub
        return testDao.get();
    }

    @Override
    @Transactional(propagation=Propagation.REQUIRED)
    public void update() {
        // TODO Auto-generated method stub
        testDao.update();
    }

}
View Code

  TestDao.java

package com.sl.dao;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class TestDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public void save() {
        String sql="insert into t_testa(name) values('11111')";
        jdbcTemplate.execute(sql);
    }
    
    public void methodA() {
        String sql="insert into t_testa(name) values('11111')";
        jdbcTemplate.execute(sql);
    }
    
    public void methodB() {
        String sql="insert into t_testb(name) values('11111')";
        jdbcTemplate.execute(sql);
    }
    
    public void del() {
        String sql="delete from t_testa";
        jdbcTemplate.execute(sql);
    }
    
    public List<Map<String, Object>> get() {
        String sql="select * from t_testa";
        List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
        return list;
    }
    
    public void update() {
        String sql="update t_testa set name = 'asdfg'";
        jdbcTemplate.execute(sql);
    }
}
View Code
原文地址:https://www.cnblogs.com/javasl/p/12334583.html