@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
之后,又执行了finally
的commit
的方法。所以报错.
解决方案
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;
}