
聊聊@Transactional注解和事务的使用
聊聊@Transactional注解和事务的使用
@Transactional
注解相信大家并不陌生,平时开发中很常用的一个注解,它能保证方法内多个数据库操作要么同时成功、要么同时失败。使用@Transactional
注解时需要注意许多的细节,不然你会发现@Transactional
总是莫名其妙的就失效了。
一、事务
事务管理在系统开发中是不可缺少的一部分,Spring
提供了很好事务管理机制,主要分为**编程式事务
和声明式事务
**两种。
1.编程式事务
是指在代码中手动的管理事务的提交、回滚等操作,代码侵入性比较强。
编程式事务方式需要开发者在代码中手动的管理事务的开启、提交、回滚等操作。
package com.hanserwei.transactiondemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import javax.sql.DataSource;
@Component
public class TransactionDemo {
@Autowired
private DataSource dataSource;
public void demo() {
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
try {
// 业务逻辑1
// 业务逻辑2
// 业务逻辑3
// 没有异常,提交事务
transactionManager.commit(transactionStatus);
} catch (Exception e) {
// 出现异常,回滚事务
transactionManager.rollback(transactionStatus);
}
}
}
如以上代码,开发者可以通过API自己控制事务。
2.声明式事务
基于AOP
面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。声明式事务也有两种实现方式,一是基于TX
和AOP
的xml配置文件方式,二种就是基于@Transactional注解了。
声明式事务管理方法允许开发者配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。
在配置类开启事务选项
/**
* Spring核心配置类
* 用于配置Spring容器的组件扫描、属性源、数据库配置导入和事务管理
*/
@Configuration
@ComponentScan(basePackages = {"com.hanserwei"}) // 配置组件扫描包路径
@PropertySource({"classpath:dao/db.properties"}) // 加载数据库配置文件
@Import({DruidConfig.class, MyBatisConfig.class}) // 导入Druid数据源配置和MyBatis配置
@EnableTransactionManagement() // 开启事务管理功能
public class SpringConfig {
}
然后使用注解即可!
@Transactional
public void demo2() {
// 业务逻辑1
// 业务逻辑2
// 业务逻辑3
}
Note that it does not apply to ancestor classes up the class hierarchy!
这个是官方给出的,不要加在它的祖宗类上,也就是说如果你要对一个接口所有方法事务管理,请加在该接口的实现类上,而不是加载该接口上面。
声明式事务对代码没有侵入性,方法内只需要写业务逻辑就可以了,帮助我们节省了很多代码,他会自动帮我们进行事务的开启、提交以及回滚等操作,把程序员从事务管理中解放出来。可以说优点很明显,但是这种方式的缺点也很明显。
问题一:声明式事务的粒度问题
首先,*声明式事务有一个局限,那就是他的最小粒度要作用在方法上。*
也就是说,如果想要给一部分代码块增加事务的话,那就需要把这个部分代码块单独独立出来作为一个方法。
在大事务耗时过长需要高并发优化的情况下不建议使用声明式事务,建议采用上面提到的编程式事务方式。
问题二:声明式事务容易被开发者忽略
而事务一旦被忽略就容易造成很多故障
首先,如果开发者没有注意到一个方法是被事务嵌套的,那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。
我们知道,这些操作如果被包在事务中,有两个问题:
1、这些操作自身是无法回滚的,这就会导致数据的不一致。可能RPC调用成功了,但是本地事务回滚了,可是PRC调用无法回滚了(这里不讨论分布式事务)。
2、在事务中有远程调用,就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用,那么如果类似操作过多,就会导致数据库连接池耗尽。
有些时候,即使没有在事务中进行远程操作,但是有些人还是可能会不经意的进行一些内存操作,如运算。或者如果遇到分库分表的情况,有可能不经意间进行跨库操作。
问题三:声明式事务用不对在某些场景下容易失效
这个问题后面我们着重讲述一下失效的几种场景。
二、@Transactional介绍
1、@Transactional 可以作用在接口
、类
、类方法
作用于类:当把@Transactional 注解放在类上时,表示所有该类的
public
方法都配置相同的事务属性信息。作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。(反正别用,除非你强制开启CGLIB代理)
2、@Transactional 有哪些属性
propagation属性
propagation
代表事务的传播行为,默认值为 Propagation.REQUIRED
,其他的属性信息如下:
Propagation.REQUIRED
:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。( 也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法的事务合并为一个事务 )Propagation.SUPPORTS
:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。Propagation.MANDATORY
:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。Propagation.REQUIRES_NEW
:重新创建一个新的事务,如果当前存在事务,暂停当前的事务。( 当类A中的 a 方法用默认Propagation.REQUIRED
模式,类B中的 b方法加上采用Propagation.REQUIRES_NEW
模式,然后在 a 方法中调用 b方法操作数据库,然而 a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW
会暂停 a方法的事务 )Propagation.NOT_SUPPORTED
:以非事务的方式运行,如果当前存在事务,暂停当前的事务。Propagation.NEVER
:以非事务的方式运行,如果当前存在事务,则抛出异常。Propagation.NESTED
:和Propagation.REQUIRED
效果一样。
isolation 属性
isolation
:事务的隔离级别,默认值为 Isolation.DEFAULT
。
Isolation.DEFAULT
:使用底层数据库默认的隔离级别。Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
timeout 属性
timeout
:事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
@Override
@Transactional(propagation = Propagation.REQUIRED, timeout = 5)
public void addUser(User user) {
System.out.println("---- UserServiceImpl addUser方法开始 ");
usermapper.addUser(user);
try {
Thread.sleep(7000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("---- UserServiceImpl addUser方法结束 ");
}
似乎这样貌似在最新版本的spring里面不行,其实可以更简单方法,我们直接在数据库层面动手。
package com.hanserwei.mapper;
import com.hanserwei.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface UserMapper {
@Update("SELECT SLEEP(10) FROM user")
void addUser(User user);
}
这里我直接使用MySQL内置的函数SLEEP
,让它在数据库睡10秒。这样就可以完美触发报错!
Exception in thread "main" org.springframework.dao.QueryTimeoutException:
最终,由于事务超时,事务回滚。
readOnly 属性
readOnly
:指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor 属性
rollbackFor
:用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
noRollbackFor属性
noRollbackFor
:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
三、@Transactional失效的场景
接下来我们结合具体的代码分析一下哪些场景下,@Transactional 注解会失效。
1、@Transactional 应用在非 public 修饰的方法上
如果Transactional
注解应用在非public
修饰的方法上,Transactional将会失效。
在这里插入图片描述
之所以会失效是因为在Spring AOP 代理时,如上图所示 TransactionInterceptor
(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor
(CglibAopProxy
的内部类)的 intercept 方法或 JdkDynamicAopProxy
的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource
的 computeTransactionAttribute
方法,获取Transactional 注解的事务配置信息。
/**
* Same signature as {@link #getTransactionAttribute}, but doesn't cache the result.
* {@link #getTransactionAttribute} is effectively a caching decorator for this method.
* <p>As of 4.1.8, this method can be overridden.
* @since 4.1.8
* @see #getTransactionAttribute
*/
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 关键在这,过滤掉非public的方法
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
注意:protected
、private
修饰的方法上使用 @Transactional
注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
2、@Transactional 注解属性 propagation 设置错误
这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。
Propagation.NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。我这里来模拟一下这个操作!
首先我定义一个保存日志的业务类:
package com.hanserwei.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class LogService { @Autowired private LogRepository logRepository; // 错误配置:该方法不应该在事务中执行 @Transactional(propagation = Propagation.NOT_SUPPORTED) public <T> void log(T t) { logRepository.log(t.toString()); // 模拟异常 throw new RuntimeException(); } }
保存日志的数据访问层
package com.hanserwei.mapper; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; @Mapper public interface LogRepository { @Insert("insert into log(id,log) values(null ,#{log} )") void log(String log); }
然后我在插入用户的时候同时调用日志业务保存日志:
package com.hanserwei.service.impl; import com.hanserwei.entity.User; import com.hanserwei.mapper.UserMapper; import com.hanserwei.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper usermapper; @Autowired private LogService logService; @Override @Transactional public void addUser(User user) { usermapper.addUser(user); logService.log(user); } }
测试类:
package com.hanserwei.test; import com.hanserwei.config.SpringConfig; import com.hanserwei.controller.UserController; import com.hanserwei.entity.User; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class UserControllerTest { public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class); UserController userController = applicationContext.getBean(UserController.class); User user = new User(); user.setName("Hanserwei"); userController.addUser(user); } }
运行结果:
再看看数据库:
可以看到,日志插入了,但是用户没有插入!我来分析一下这个原因:
addUser
方法开启事务 当UserServiceImpl.addUser()
方法被调用时,@Transactional
注解会启动一个新事务。usermapper.addUser(user)
操作usermapper.addUser(user)
在这个事务中执行,将用户数据插入到数据库中。此时,这个操作并未提交。logService.log(user)
方法调用addUser
方法接着调用logService.log()
。log()
方法上的@Transactional(propagation = Propagation.NOT_SUPPORTED)
注解会暂停addUser
方法的事务。日志操作在非事务环境中执行
logService.log()
方法在非事务环境中执行了logRepository.log()
。由于没有事务管理,这个日志操作会立即提交到数据库。异常的发生与传播
logService.log()
方法接着抛出了一个RuntimeException
。这个异常会传播回UserServiceImpl.addUser()
。addUser
方法事务回滚addUser
方法捕获到这个RuntimeException
。因为它被@Transactional
注解修饰,Spring 事务管理器会执行回滚操作,撤销usermapper.addUser(user)
之前的操作。
Propagation.SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。还是刚刚的添加用户和保存日志的操作,做出一点小修改:
package com.hanserwei.service.impl; import com.hanserwei.entity.User; import com.hanserwei.mapper.UserMapper; import com.hanserwei.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper usermapper; @Autowired private LogService logService; @Override // 这是一个没有事务的方法 public void addUser(User user) { usermapper.addUser(user); logService.log(user); } }
package com.hanserwei.service.impl; import com.hanserwei.mapper.LogRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class LogService { @Autowired private LogRepository logRepository; // 错误配置:如果该方法被非事务方法调用,它将不具备事务能力 @Transactional(propagation = Propagation.SUPPORTS) public <T> void log(T t) { logRepository.log(t.toString()); // 模拟异常 throw new RuntimeException(); } }
运行我们来看结果:
异常正常抛出,我们来查看一些数据库
可以看到,由于两个数据库都插入了,事务失效了。原因在于,
addUser
是一个没有事务的方法,但是它区调用了一个@Transactional(propagation = Propagation.SUPPORTS)
标记的方法,这个标记会让log
这个方法也加入无事务的环境,所以事务就失效了。TransactionDefinition.PROPAGATION_NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。最后这个例子是最简单的例子,与其说它是事务失效,不如说是spring的一种提醒,提醒你不要在一个已经有事务的环境中执行某个方法。
package com.hanserwei.service.impl; import com.hanserwei.entity.User; import com.hanserwei.mapper.UserMapper; import com.hanserwei.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper usermapper; @Autowired private LogService logService; @Transactional @Override public void addUser(User user) { usermapper.addUser(user); logService.log(user); } }
package com.hanserwei.service.impl; import com.hanserwei.mapper.LogRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Service public class LogService { @Autowired private LogRepository logRepository; @Transactional(propagation = Propagation.NEVER) public <T> void log(T t) { logRepository.log(t.toString()); // 模拟异常 throw new RuntimeException(); } }
运行直接报错!
Propagation.REQUIRES_NEW
当带有Propagation.REQUIRES_NEW
注解的方法被调用时,Spring 会立即暂停当前正在运行的事务(如果存在的话),然后开启一个新的事务。这个新事务是完全独立的。它的提交和回滚不会影响到外部的那个被暂停的事务。@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void addUserAndAddress(User user, Address address) throws Exception { userMapper.save(user); addAddress(address); } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void addAddress(Address address) { addressMapper.save(address); }
这里配置了
Propagation.REQUIRES_NEW
,这意味着每当调用这两个方法时,都会启动一个新的独立事务。因此,addUserAndAddress
的事务和addAddress
的事务是完全独立的两个事务,它们之间无法共享数据的一致性。如果其中一个事务失败回滚,另一个事务不会受到影响,这可能导致数据不一致。
3、@Transactional 注解属性 rollbackFor 设置错误
rollbackFor
可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked
异常(继承自 RuntimeException
的异常)或者 Error
才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
// 希望自定义的异常可以进行回滚
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
源码如下:(递归找吗,有意思)
private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) {
if (exceptionToMatch.equals(declaredException)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionToMatch == Throwable.class) {
return Integer.MAX_VALUE;
}
return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1);
}
4、同一个类中方法调用,导致@Transactional失效
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
还是做一下实验:
package com.hanserwei.service.impl;
import com.hanserwei.entity.User;
import com.hanserwei.mapper.UserMapper;
import com.hanserwei.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper usermapper;
@Autowired
private LogService logService;
@Override
public void addUser(User user) {
usermapper.addUser(user);
saveLog(user);
}
@Transactional
protected void saveLog(User user) {
logService.log(user);
}
}
异常还是会正常抛出:
但是两个数据库都被插入了,事务失效了!
那为啥会出现这种情况?其实这还是由于使用Spring AOP
代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring
生成的代理对象来管理。而且我还发现一点,就是说如果要加@Transactional
注解,那么该方法必须是可以被重写的,所以私有不行!
5、异常被你的 catch“吃了”导致@Transactional失效
这种情况是最常见的一种@Transactional注解失效场景
直接来试一把:
package com.hanserwei.service.impl;
import com.hanserwei.entity.User;
import com.hanserwei.mapper.UserMapper;
import com.hanserwei.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper usermapper;
@Autowired
private LogService logService;
@Override
@Transactional
public void addUser(User user) {
try {
usermapper.addUser(user);
int i = 1 / 0;
} catch (Exception e) {
// 抛可以,抛了被spring接住了还是可以回滚
// throw new RuntimeException(e);
// 打印不行,或者吞掉也不行
e.printStackTrace();
}
}
}
会抛出异常吗?会的。会回滚事务吗?答案是不会!
在业务方法中一般不需要catch异常,如果非要catch一定要手动抛出throw new RuntimeException()
,否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。
6、@Transactional
注解应用于 final 和 static 方法
由于 AOP(面向切面编程)的限制,@Transactional
注解无法应用于 final 和 static 方法。
final 方法:Spring Boot 2.x 默认使用 cglib 作为代理,而 cglib 的工作原理是生成一个子类来覆盖目标方法。由于 final 方法无法被子类覆盖,因此无法创建代理,
@Transactional
也便无法生效。static 方法:
static
方法是属于类的,而不是类的实例。AOP 代理是基于实例的,所以无法对静态方法进行代理。
6、数据库引擎不支持事务
这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb
引擎。一旦数据库引擎切换成不支持事务的myisam
,那事务就从根本上失效了。
7、多线程环境下也会失效
因为@Transactional
是基于ThreadLocal
储存上下文的,你多线程,每个线程都有一个单独的ThreadLocal
,自然无法保持事务同步!