Bootstrap

@Transactional注解管理事务和Spring手动提交事务(二)

概述

通过 @Transactional 注解方式,也可将事务织入到相应方法中。而使用注解方式,只需在配置文件中加入一个 tx 标签,以告诉 Spring 使用注解来完成事务的织入。该标签只需指定一个属性,事务管理器。

<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:tx="http://www.springframework.org/schema/tx"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
 http://www.springframework.org/schema/tx
 http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

<!-- 加载配置属性文件 -->
    <context:property-placeholder ignore-unresolvable="true" location="classpath:jdbc.properties"/>

    <!-- 数据源配置, 使用 Druid 数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
        <property name="driverClassName" value="${jdbc.driverClass}"/>

        <!-- 基本属性 url、user、password -->
        <property name="url" value="${jdbc.connectionURL}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${jdbc.pool.init}"/>
        <property name="minIdle" value="${jdbc.pool.minIdle}"/>
        <property name="maxActive" value="${jdbc.pool.maxActive}"/>

        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="60000"/>

        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>

        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>

        <property name="validationQuery" value="${jdbc.testSql}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>

        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="stat"/>
    </bean>


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

@Transactional 注解简介

@Transactional 的所有可选属性:

  • propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED
  • isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举 ,默认值为 Isolation.DEFAULT
  • readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值为 false。
  • timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。
  • rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
  • noRollbackForClassName: 指定不需要回滚的异常类类名。类型为 - String[],默认值为空数组。当然,若只有一个异常类时,可以不使用数组。
    需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解 @Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该方法中。因为 Spring 会忽略掉所有非 public 方法上的 @Transaction 注解。

@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务。

使用 @Transaction 注解

使用起来很简单,只需要在需要增加事务的业务类上增加 @Transaction 注解即可,添加在类上,表示整个类的方法都进行事务处理,写在方法上同理,会在方法进入开启事务,结束提交事务,异常回滚事务

且在代码中也加了try catch相当于没有加事务注解, 所以事务不起作用, 此时就需要在catch里面手动添加事务的回滚,或者抛出异常,事务生效

package com.hello.spring.transaction.aspectsj.aop.service.impl;

import com.hello.spring.transaction.aspectsj.aop.dao.TbContentCategoryDao;
import com.hello.spring.transaction.aspectsj.aop.domain.TbContent;
import com.hello.spring.transaction.aspectsj.aop.domain.TbContentCategory;
import com.hello.spring.transaction.aspectsj.aop.service.TbContentCategoryService;
import com.hello.spring.transaction.aspectsj.aop.service.TbContentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service(value = "tbContentCategoryService")
public class TbContentCategoryServiceImpl implements TbContentCategoryService {

    @Autowired
    private TbContentCategoryDao tbContentCategoryDao;

    @Autowired
    private TbContentService tbContentService;

    public void save(TbContentCategory tbContentCategory, TbContent tbContent) {
    	// try catch吃掉异常,相当于没有加事务注解, 所以事务不起作用.
    	// 抛出异常事务生效
    	try {
	    	tbContentCategoryDao.insert(tbContentCategory);
	        tbContentService.save(tbContent);
    	}   
    	// 异常回滚
	    catch (Exception e) {
	       log.error("saveUser异常 fail:{}", e);
	       // 手动回滚事务  start
		   TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		   // 手动回滚事务  end
		     
		   throw e;
	   }
        
    }
}

手动提交事务

手动提交事务的运用场景:
1、异步处理业务时,此时业务的事务已经脱离正常的aop机制了,所以需要手动提交事务,来保持业务中多个事务的一致性。
2、在线程中我们也有可能需要事务,这个事务可以使用手动事务。

@Service
@Slf4j
//@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

	@Resource
	private DataSourceTransactionManager transactionManager;
	
	@Override
	public Boolean saveUser(User user) {
	   // 手动开启事务  start
	   DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
	   definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
	   TransactionStatus transaction = transactionManager.getTransaction(definition);
	   // 手动开启事务  end
	
	   log.info("[UserService] start insert saveUser");
	   // 保存用户
	   Boolean result = false;
	   try {
	       if (Objects.nonNull(user)) {
	           // 保存用户
	           result = this.save(user);
	           // 模拟异常回滚
	           int a = 1 / 0;
	       }
	       log.info("[UserService] finish  insert saveUser");
	       // 手动提交事务  end
	       transactionManager.commit(transaction);
	       // 手动提交事务  start
	   }
	   // 异常回滚
	   catch (Exception e) {
	       log.error("saveUser异常 fail:{}", e);
	       // 手动回滚事务  start
	       transactionManager.rollback(transaction);
	       // 手动回滚事务  end
	   }
	   return result;
	}
}	

@Transactional不生效的场景

1、没有被 Spring 容器管理

如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

// @Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

2、用在非public修饰的方法

@Transactional是基于动态代理的,Spring的代理工厂在启动时会扫描所有的类和方法,并检查方法的修饰符是否为public,非public时不会获取@Transactional的属性信息,这时@Transactional的动态代理对象为空。

3、多线程调用

Spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,同一个事务中数据库操作使用同一个jdbc connection,新开启的线程获取不到当前jdbc connection

    @Transactional
    public void queryForObject() {
        System.out.println("1.====更新开始====");
        userDao.update();
        System.out.println("1.====更新结束====");

        new Thread(() -> {
            System.out.println("2.====更新开始====");
            userDao.update();
            System.out.println("3.====更新结束====");
        }).start();

        try {
            // 主线程睡60s
            Thread.sleep(60000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("====主线程结束====");
    }
2024-01-26 17:00:54.206  INFO 23536 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-01-26 17:00:54.206  INFO 23536 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-01-26 17:00:54.213  INFO 23536 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 7 ms
2024-01-26 17:00:54.249  INFO 23536 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2024-01-26 17:00:54.989  INFO 23536 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
1.====更新开始====
2024-01-26 17:00:55.003 DEBUG 23536 --- [nio-8080-exec-1] o.s.jdbc.core.JdbcTemplate               : Executing SQL update [update t_order_1 set amount = 100 where id = 1]
1.====更新结束====
2.====更新开始====
2024-01-26 17:00:55.012 DEBUG 23536 --- [       Thread-9] o.s.jdbc.core.JdbcTemplate               : Executing SQL update [update t_order_1 set amount = 100 where id = 1]
Exception in thread "Thread-9" org.springframework.dao.CannotAcquireLockException: StatementCallback; SQL [update t_order_1 set amount = 100 where id = 1]; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:263)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
	at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1443)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:388)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:523)
	at cn.zysheep.dao.UserDao.update(UserDao.java:31)
	at cn.zysheep.dao.UserDao$$FastClassBySpringCGLIB$$2d4b89d6.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
	at cn.zysheep.dao.UserDao$$EnhancerBySpringCGLIB$$929e1600.update(<generated>)
	at cn.zysheep.service.UserServiceImpl.lambda$queryForObject$0(UserServiceImpl.java:37)
	at java.lang.Thread.run(Thread.java:750)
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1335)
	at com.mysql.cj.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2108)
	at com.mysql.cj.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1245)
	at com.zaxxer.hikari.pool.ProxyStatement.executeUpdate(ProxyStatement.java:120)
	at com.zaxxer.hikari.pool.HikariProxyStatement.executeUpdate(HikariProxyStatement.java)
	at org.springframework.jdbc.core.JdbcTemplate$1UpdateStatementCallback.doInStatement(JdbcTemplate.java:511)
	at org.springframework.jdbc.core.JdbcTemplate$1UpdateStatementCallback.doInStatement(JdbcTemplate.java:508)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:376)
	... 14 more
====主线程结束====

在这里插入图片描述
查看日志很明显在进行第二次更新的时候锁住了,一直等待锁。因为两次更新不在一个事务里。第二次一直等待第一次更新释放锁。

4、自身调用问题

@Service
public class OrderServiceImpl implements OrderService {

    public void update(Order order) {
        updateOrder(order);
    }
    
    @Transactional
    public void updateOrder(Order order) {
        // update order
    } 
}

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?

再来看下面这个例子:

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void update(Order order) {
        updateOrder(order);
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order) {
        // update order
    }   
}

这两个例子的答案是:不管用!

因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。


代码中避免不了会把方法写在同一个类中,不想重新写一个service,在注入调用,有什么方法可以解决?
1、使用@Autowired注解或者@Resource注解注入自己,然后使用注入的代理类去调用事务方法
在这里插入图片描述
2、注入BeanFactory容器,使用容器获取代理类,然后使用代理类调用事务方法。
在这里插入图片描述
在这里插入图片描述

3、类上面添加注解@EnableAspectJAutoProxy(exposeProxy = true),然后在类中获取当前类的代理类,使用代理类调用事务方法。如果不添加注解的话,可能会出现错误
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5、异常被你的 catch“吃了”导致@Transactional失效

Spring是根据抛出的异常来回滚的,如果异常被捕获了没有抛出的话,事务就不会回滚。

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {
            
        }
    }
}

把异常吃了,然后又不抛出来,事务怎么回滚

6、@Transactional 注解属性 rollbackFor 异常类型设置错误

Spring默认抛出RuntimeException异常或Error时才会回滚事务,要想其他类型异常(Exception)也回滚则需要设置rollbackFor属性的值。代码示例

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        try {
            // update order
        } catch {
            throw new Exception("更新错误");
        }
    }
   }

这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class)

7、@Transactional 注解属性 propagation 传播行为设置不支持事务

Propagation.NOT_SUPPORTED: 表示不以事务运行,当前若存在事务则挂起,都主动不支持以事务方式运行了,那事务生效也是白搭。

@Service
public class OrderServiceImpl implements OrderService {

    @Transactional
    public void update(Order order) {
        updateOrder(order);
    }
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void updateOrder(Order order) {
        // update order
    }
}

8、 数据库引擎不支持事务

这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。

发生最多就是自身调用、异常被吃、异常抛出类型不对这三个了

Transaction is already completed - do not call commit or rollback more than once per transaction报错

17:29:47 ERROR c.z.p.c.t.s.i.TmStatDetailServiceImpl - Transaction is already completed - do not call commit or rollback more than once per transaction
org.springframework.transaction.IllegalTransactionStateException: Transaction is already completed - do not call commit or rollback more than once per transaction
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:704) ~[spring-tx-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at 
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_60]
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_60]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_60]
        at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_60]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) [spring-tx-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:281) [spring-tx-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) [spring-tx-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207) [spring-aop-4.2.1.RELEASE.jar:4.2.1.RELEASE]
        at com.sun.proxy.$Proxy223.downloadUsTmOfficialFile(Unknown Source) [na:na]

异常处理代码块如下所示

DefaultTransactionDefinition definition=new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus transactionStatus=transactionManager.getTransaction(definition);
try{
	.....
}catch ( Exception e){
    transactionManager.rollback(transactionStatus);
}finally {
    transactionManager.commit(transactionStatus);
}

原因

经过查资料分析得知,出现该问题的主要原因是在系统出现异常rollback之后,又执行了finallycommit的方法。所以报错.

解决方案

1、rollback 执行完成之后,直接抛出异常退出当前方法
2、去掉finaly模块,将commit前移到catch之前 或者catch之后都可以。

DefaultTransactionDefinition definition=new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus transactionStatus=transactionManager.getTransaction(definition);
try{
.....
    // 去掉finaly模块,将commit前移到catch之前 或者catch之后都可以。
	transactionManager.commit(transactionStatus);
}catch ( Exception e){
    transactionManager.rollback(transactionStatus);
    // 1、rollback 执行完成之后,直接抛出异常退出当前方法
    throw e;
}
;