原文:
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>
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>
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>
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
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
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(); } }
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(); }
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(); } }
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); } }