原文:
说起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注解。结论:不会提交
public voidsave() {
testDao.save();
}
(2)defaultAutoCommit为false,使用Transactional注解。结论:会提交
@Transactionalpublic voidsave() {
testDao.save();
}
(3)defaultAutoCommit为true,不使用Transactional注解。结论:会提交
public voidsave() {
testDao.save();
}
(4)defaultAutoCommit为true,使用Transactional注解。结论:会提交
@Transactionalpublic voidsave() {
testDao.save();
}
总结:只要defaultAutoCommit或者Transactional有一项设置为可提交即可。
3、Transactional与异常自动回滚的关系
在项目中希望当方法产生异常时自动回滚事务,下面我们在defaultAutoCommit设置为false的情况下进行验证
(1)使用Transactional的默认配置,抛出检查型异常。事务不会回滚
@Transactionalpublic void save () throwsException {
testDao.save();throw newClassNotFoundException();
}
(2)使用Transactional的默认配置,抛出运行时异常。事务会回滚
@Transactionalpublic voidsave (){
testDao.save();throw newRuntimeException();
}
(3)使用Transactional注解,指定rollbackFor为抛出的异常或其父类时,检查型异常会回滚
@Transactional(rollbackFor=Exception.class)public void save () throwsException {
testDao.save();throw newClassNotFoundException();
}
(4)使用Transactional注解,指定rollbackFor不是抛出的异常或其父类时,运行时异常会回滚(运行时异常与rollbackFor无关,肯定回滚)
@Transactional(rollbackFor=FileNotFoundException.class)public void save () throwsException {
testDao.save();throw newRuntimeException();
}
(5)使用Transactional注解,捕获异常,事务不会回滚
@Transactionalpublic void save () throwsException {try{
testDao.save();throw newRuntimeException();
}catch(Exception e) {//TODO: handle exception
}
}
@Transactionalpublic void save () throwsException {try{
testDao.save();throw newClassNotFoundException();
}catch(Exception e) {//TODO: handle exception
}
}
(6)捕获的异常需要手动回滚,手动回滚时检查型异常可以不指定rollbackFor
@Transactionalpublic voidsave () {try{
testDao.save();throw newClassNotFoundException();
}catch(Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
@Transactional(rollbackFor=FileNotFoundException.class)public voidsave () {try{
testDao.save();throw newClassNotFoundException();
}catch(Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
@Transactionalpublic voidsave () {try{
testDao.save();throw newRuntimeException();
}catch(Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
(7) Transactional要加在主动(直接)调用的方法上面,以下事务不会提交,没有开启事务(spring容器管理的类直接调用test)
public voidtest(){
save();
}
@Transactionalpublic voidsave () {try{
testDao.save();throw newRuntimeException();
}catch(Exception e) {
}
}
4、spring中的事务传播行为
spring中共有7种事务传播行为,分别介绍如下:
(1)PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
方法A加注解,方法B也加注解,当方法A运行时会开启事务A,调用方法B时,方法B也加入到事务A中
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.REQUIRED)public voidmethodB() {
testDao.methodB();
}
如上图,总共开启了一个事务。
(2)PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务,如果没有事务,则不会开启事务。
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.SUPPORTS)public voidmethodB() {
testDao.methodB();
}
如果直接调用methodA,methodA会开启一个事务,methodA调用methodB,则methodB支持当前methodA开启的事务,如下图:
如果直接调用methodB,不会开启事务,如下图:
如果直接调用methodA,由于methodA是SUPPORTS,不会开始事务,methodB不是直接调用,也不会开启事务
@Transactional(propagation =Propagation.SUPPORTS)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.REQUIRED)public voidmethodB() {
testDao.methodB();
}
(3)PROPAGATION_MANDATORY:必须在一个事务中运行,否则报异常
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.MANDATORY)public voidmethodB() {
testDao.methodB();
}
直接调用methodA,开启一个事务,methodB也在该事务中运行
直接调用methodB,报异常No existing transaction found for transaction marked with propagation 'mandatory'
@Transactional(propagation =Propagation.MANDATORY)public voidmethodB() {
testDao.methodB();
}
(4)PROPAGATION_REQUIRES_NEW:开启一个新事务。如果一个事务已经存在,则先将存在的事务挂起,执行完这个新事务,再执行挂起的事务,两个事务的成功或失败没有联系。
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.REQUIRES_NEW)public voidmethodB() {
testDao.methodB();
}
从上图中看到,并没有挂起旧事务,先执行新事务,因为只有使用JtaTransactionManager作为事务管理器时才生效。后面再研究。。。
(5)PROPAGATION_NOT_SUPPORTED:在非事务中运行。如果有事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行。
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.NOT_SUPPORTED)public voidmethodB() {
testDao.methodB();
}
直接调用methodA,运行到methodB,事务应该挂起,即methodB对应的数据不会保存到数据库。
但上图与预期的不一致,因为也需要JtaTransactionManager作为事务管理器 。
直接调用methodB不会开启事务,可以自己尝试一下。
(6)PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常。
@Transactional(propagation =Propagation.NEVER)public voidmethodB() {
testDao.methodB();
}
直接调用methodB,不会开启事务
@Transactional(propagation =Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.NEVER)public voidmethodB() {
testDao.methodB();
}
直接调用methodA,报异常,发现下面日志没有报异常,,,是版本问题还是我的理解有误???先留个疑问吧
(7) PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按PROPAGATION_REQUIRED属性执行。
附 相关配置文件和代码
pom.xml
4.0.0
com.sl
spring-web-transaction
0.0.1-SNAPSHOT
war
UTF-8
1.8
1.8
2.2
5.1.0.RELEASE
1.7.4
1.1.11
5.1.30
2.5.4
1.7.7
org.springframework
spring-webmvc
${spring.version}
org.aspectj
aspectjweaver
${aspectjweaver.version}
org.springframework
spring-jdbc
${spring.version}
com.alibaba
druid
${druid.version}
mysql
mysql-connector-java
${mysql.driver.version}
runtime
org.slf4j
slf4j-api
${slf4j.version}
org.slf4j
slf4j-log4j12
${slf4j.version}
org.apache.maven.plugins
maven-war-plugin
2.4
${project.build.directory}/${project.artifactId}
${project.artifactId}
org.apache.tomcat.maven
tomcat7-maven-plugin
${tomcat.version}
8080
/${project.artifactId}
${project.build.sourceEncoding}
View Code
spring-context.xml
classpath:db.properties
View Code
spring-mvc.xml
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-8log4j.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:123456filters:stat
maxActive:20initialSize:1maxWait:60000minIdle:10maxIdle:15timeBetweenEvictionRunsMillis:60000minEvictableIdleTimeMillis:300000validationQuery:SELECT'x'testWhileIdle:truetestOnBorrow:falsetestOnReturn:falsemaxOpenPreparedStatements:20removeAbandoned:trueremoveAbandonedTimeout:1800logAbandoned:true
View Code
TestController.java
packagecom.sl.controller;importjava.util.List;importjava.util.Map;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.ResponseBody;importcom.sl.service.TestService;
@Controller
@RequestMapping("/test")public classTestController {
@AutowiredprivateTestService testService;
@RequestMapping("/save")
@ResponseBodypublic voidsave(){//testService.save();
testService.methodA();
}
@RequestMapping("/del")
@ResponseBodypublic void del() throwsException {
testService.del();
}
@RequestMapping("/get")
@ResponseBodypublicString get(){
String str= "...";
List> list =testService.get();for(Mapmap : list) {
str= map.get("name").toString();
}returnstr;
}
@RequestMapping("/update")
@ResponseBodypublic void update() throwsException {
testService.update();
}
}
View Code
TestService.java
packagecom.sl.service;importjava.util.List;importjava.util.Map;public interfaceTestService {public void save() throwsException;public voiddel();public voidtest();public List>get();public voidupdate();public voidmethodA();public voidmethodB();
}
View Code
TestServiceImpl.java
packagecom.sl.service.impl;importjava.util.List;importjava.util.Map;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Propagation;importorg.springframework.transaction.annotation.Transactional;importcom.sl.dao.TestDao;importcom.sl.service.TestService;
@Servicepublic class TestServiceImpl implementsTestService{private static Logger logger=LoggerFactory.getLogger(TestServiceImpl.class);
@AutowiredprivateTestDao testDao;public voidtest(){
save();
}
@Transactionalpublic voidsave () {try{
testDao.save();throw newRuntimeException();
}catch(Exception e) {
}
}
@Transactional(propagation=Propagation.REQUIRED)public voidmethodA() {
methodB();
testDao.methodA();
}
@Transactional(propagation=Propagation.NEVER)public voidmethodB() {
testDao.methodB();
}
@Override
@Transactional(propagation=Propagation.NEVER)public voiddel() {//TODO Auto-generated method stub
testDao.del();
}
@Overridepublic List>get() {//TODO Auto-generated method stub
returntestDao.get();
}
@Override
@Transactional(propagation=Propagation.REQUIRED)public voidupdate() {//TODO Auto-generated method stub
testDao.update();
}
}
View Code
TestDao.java
packagecom.sl.dao;importjava.sql.SQLException;importjava.util.List;importjava.util.Map;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.jdbc.core.JdbcTemplate;importorg.springframework.stereotype.Repository;
@Repositorypublic classTestDao {
@AutowiredprivateJdbcTemplate jdbcTemplate;public voidsave() {
String sql="insert into t_testa(name) values('11111')";
jdbcTemplate.execute(sql);
}public voidmethodA() {
String sql="insert into t_testa(name) values('11111')";
jdbcTemplate.execute(sql);
}public voidmethodB() {
String sql="insert into t_testb(name) values('11111')";
jdbcTemplate.execute(sql);
}public voiddel() {
String sql="delete from t_testa";
jdbcTemplate.execute(sql);
}public List>get() {
String sql="select * from t_testa";
List> list =jdbcTemplate.queryForList(sql);returnlist;
}public voidupdate() {
String sql="update t_testa set name = 'asdfg'";
jdbcTemplate.execute(sql);
}
}
View Code