关于Spring中事务未生效的场景
对于Java开发的同学,相信对于Spring的事务在熟悉不过了。但是相信各位小伙伴们在工作中一定有类似于这样的需要:需要同时写入多张表的数据,为了保证操作的原子性(也就是所谓的要么同时成功,要么同时失败),避免产生数据不一致的情况,我们都会使用
Spring事务
,但是对于Spring事务相信大家也遇到过比较诡异的问题,会出现那种事务失效,或者就是不满足自己的业务场景,其实归根结底还是我们针对于Spring事务的某些特殊场景掌握的不够牢靠。接下来我总结了一下关于Spring事务在某些情况下失效的场景,不出意外的话相信你也遇到过。
Spring事务失效的12种场景总结图
pasted-image
Spring 事务不生效
1.访问权限问题
代码语言:javascript复制所谓的访问权限问题也就是开发中再熟悉不过的
private
,default
,protected
,public
,它们的访问权限从左到右,依次变大。如果我们在开发过程中国呢,把某些事务方法定义了错误的访问权限,就会导致事务功能出现问题,甚至失效。例如:
@Service
public class UserService {
@Transactionsl
private void add(User user){
saveUser(user);
updateUser(user);
}
}
上面代码中我们可以看到对于方法add
的访问修饰符被定义成了private
,这样会导致事务失效,原因是Spring 要求被代理的方法必须是 **public** 的
。简单粗暴来看源码是怎么搞的。如下:
/**
* 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) {
// 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;
}
从上述源码中可以看到AbstractFallbackTransactionAttributeSource
类的computeTransactionAttribute
方法中有一个判断,如果方法的修饰符不是public
的话,则返回null,并且不支持事务。
也就是说如果我们自定义的事务方法(即目标方法)它的访问权限不是public
,而是private
,defalut
,protected
修饰符的话,Spring都不会提供事务。
2.方法使用final修饰
代码语言:javascript复制在某些场景我们可能需要使用
final
修饰方法,为了不让子类重写等原因,但是针对普通方法而言这是没有任何问题的,但是针对需要加事务的方法则会导致事务失效。如下代码:
@Service
public class UserService {
@Transactionsl
private final void add(User user){
saveUser(user);
updateUser(user);
}
}
上述代码中的add
方法被final
修饰了,从而导致了事务失效。具体原因是什么的。这就要从Spring的源码开始说起了。相比应该是都知道Spring事务的底层其实是使用了AOP,也就是通过jdk动态代理
或者cglib
,帮我们生成了代理类,在代理类中实现的事务功能。
但是某个方法被final
修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能
注意:如果某个方法是
static
修饰的,同样无法通过动态代理,变成事务方法。
3.方法内部调用
代码语言:javascript复制在某些场景下,我们需要在某个service类中的某个方法中调用另外一个事务方法。比如:
@Service
public class UserService {
@Autowired
private UserMapper usermapper;
public void add(User user){
userMappper.insertUser(user);
updateStatus(user);
}
}
@Transactional
public void updateStatus(User user){
doSomeThing();
}
从上面的方法中我们可以看到add()方法中直接调用了事务方法updateStatus();从前面的介绍可以直达,updateStatus()方法拥有事务的能力是因为Spring AOP生成了代理对象,但是这种方法直接调用了this对象的方法,所以updateStatus()方法不会生成事务。
由此可见,在同一类中的方法直接调用,会导致事务失效。
那么我们如何解决在同一方法中调用自己类中的另外一个方法呢?方案如下
3.1新加一个Service方法
这个方法相对简单就是将同一类中调用与被调用的两个方法拆分为两个Service。代码如下:
代码语言:javascript复制@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
public void save(User user){
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Service
public class ServiceB{
@Transactional(rollbackFor = Exception.class)
public void doSave(User user) {
addData1(user);
updateData2(user):
}
}
3.2在该Service中注入自己
如果不想加一个新的类,其实也可以通过在该类中注入自己也可解决事务失效的问题。代码如下:
代码语言:javascript复制@Service
public class ServiceA(){
@Autowired
private ServiceA serviceA;
public void save(User user){
queryData1();
queryData2();
serviceA.doSave(user);
}
@Transactional(rollbackFor = Excetion.class)
public void doSave(User user){
addData1();
updateData2(user);
}
}
可能有人看到这里便会有这样一个疑问,这种做法不会导致循环依赖的问题吗:答案是:不会。
3.3 通过AopContent类
在该Service类中使用AopContent.currentProxy()
获取对象。虽然上述的方法2也是解决了该问题,但是代码看起来晦涩难懂。接下来我们可以通过AopContent
来获取代理对象从而实现相同的功能,代码如下:
@Service
public class ServiceA{
public void save(User user){
queryData1();
queryData2();
( (ServiceA)AopContent.currentProxy() ).doSave(user);
}
@Transactional(rollbackFor = Excetion.class)
public void doSave(User user){
addData1(user);
updateData2(user);
}
}
4.未被Spring管理
在我们市场开发中,还有一个细节很容易被忽略。就是如果需要使用Spring事务,是有一个前置条件,那就是对象需要交给Spring进行管理,需要创建bean实例。
通常情况下,我们通过@Contrlller
@Service
@Component
@Repository
等注解,实现将bean实例化喝依赖注入的功能。假设某个时候你再Service上没有添加@Service
注解,比如
//@Service
public class UserService{
@Transactional
public void add(User user){
saveData(user);
updateData(user);
}
}
上述代码的UserService类没有添加@Service
注解,那么该类就不会交给Spring进行统一管理,同时它的add()方法也不会生成事务。
5.多线程调用
在实际的应用开发中。我们通常使用多线程的场景还是很多的。如果Spring事务用在多线程场景下。同样会有问题。代码如下:
代码语言:javascript复制@Slf4j
@Service
public class UserService{
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(User user) throws Exception{
userMapper.insertUser(user);
new Thread( ()-> {
roleService.doOtherThing();
}).start();
}
}
@Service
public class RoleService{
@Transactional
public void doOtherThing(){
System.out.println("保存roles数据");
}
}
从上面的例子,我们可以看到事务方法add()中,调用了事务方法doOtherThing(),但是事务方法doOtherThing()是在另外一个线程中调用的。这样会导致两个方法不在一个线程中。获取的数据库连接也就不一致,从而是两个不同的事务。如果doOtherThing()方法中抛出了异常,add()方法是不可能回滚的。
如果看过Spring源码的小伙伴应该知道Spring的事务是通过数据库的连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
代码语言:javascript复制private static final ThreadLocal<Map<Object,Object>> resources = new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实指同一个数据库连接,只有拥有同一个数据库连接才能同事提交和回滚。如果在不同的线程中,拿到的数据库连接肯定是不一样的。所以事务也是不同的。
6.表不支持事务
众所周知,在MySQL 5.x之前,默认的数据库引擎是myisam
。
它的优缺点就不说了:索引文件和数据文件是分开存储的,对于查多写少的表操作,性能要比InnoDB要更好。在一些老的项目中用它的有很多。
创建一个MyIsam引擎的表:
代码语言:javascript复制CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
虽然MyIsam
引擎好用,但是有一个致命的缺点,那就是不支持事务。如果是单表操作还好,不会出现太大的问题,但是如果是跨表操作,由于其不支持事务,数据极有可能出现不完整的情况。
此外MyIsam
还不支持 行锁
和 外键
。
所以时间的业务场景中,MyIsam
的使用场景不多,在MySQL 5.x之后,MyIsam
已经逐渐的退出了历史舞台,取而代之的是引擎InnoDB
所以在实际的开发中如果事务没有生效,有可能就是因为你的表的引擎不支持事务。
7.未开启事务
有些时候,事务没有生效的根本原因是没有开启事务。
如果你使用的是Spring Boot项目,那么很幸运,因为Spring Boot已经通过DataSoureTransactionManagerAutoConfiguration
类,默认开启了事务,你只需要配置spring.datasource
的相关参数即可。
如果是Spring项目则需要一下配置信息:
代码语言:javascript复制<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config>
<aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/>
<aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>
注意:如果在pointcut标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。
以上说的都是单纯的事务没有生效。但是在实际的开发过程中还存在另外一种情况,就是事务生效了,但是没有回滚,或者说事务执行没有达到预期。
Spring中事务未生效的场景之事务未回滚
Spring的事务不回滚
1.错误的传播特性
说到事务的传播特性,首先应该知道事务的传播特性有哪些:
事务的传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中,这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,则以非事务的方式运行 |
PROPAGATION_MANDATORY | 使用当前事务,如果当前没有事务,就抛出异常 |
PROPAGATION_REQUIRED_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务的方式执行操作,如果当前存在事务,则把事务挂起 |
PROPAGATION_NEVER | 以非事务的方式执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,执行与PROPAGATION_REQUIRED类似的操作 |
如果在编写代码时将事务的传播特性编写出错。比如:
代码语言:javascript复制@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void doSave(User user){
saveData(user);
updateData(user);
}
}
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有三种事务传播特性才会新建事务REQUIRED
REQUIRED_NEW
NESTED
2.自己吞了异常
在开发过程中,有可能我们在事务中使用了try{}catch()了异常。比如:
代码语言:javascript复制
@Slf4j
@Service
public class UserService {
@Transactional
public void add(User user){
try{
saveData(user);
updateData(user);
} catch (Exception e){
log.error(e.getMessage(),e);
}
}
}
如果你的代码也是按照上述代码编写的,那么Spring事务是不会回滚的,因为开发者自己捕获了异常,同时没有手动抛出,欢聚还说就是自己把异常吞掉了。如果想要Spring能够正常回滚,则必须要抛出它能够处理的异常,如果没有抛出异常,Spring则会认为程序是正常的。
3.手动抛出了别的异常
即使开发者在编写过程中,没有手动抛出异常;但是如果出现的异常不正确,Spring事务也不会回滚。
代码语言:javascript复制@Slf4j
@Service
public class UserService{
@Transactional
public void add(User user) throws Exception {
try {
saveData(user);
updateData(user);
} catch(Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
上述这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。因为Spring事务,默认情况下只会回滚RunTimeException
,和Error
(错误),对于普通的Exception(非运行时异常),它是不会回滚的。
4.自定义了回滚异常
代码语言:javascript复制在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置
rollbackFor
参数,来完成这个功能。但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(User user) throws Exception {
saveData(user);
updateData(user);
}
}
如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。
即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
rollbackFor
默认值为UncheckedException,包括了RuntimeException和Error. 当我们直接使用@Transactional
不指定rollbackFor
时,Exception及其子类都不会触发回滚。
所以,建议一般情况下,将该参数设置成:Exception或Throwable。
5.嵌套事务回滚多了
代码语言:javascript复制public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel user) throws Exception {
userMapper.insertUser(user);
roleService.doOtherThing();
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing()方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。
这是为什么呢?
因为doOtherThing()方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
如何才能只回滚保存点呢?
代码语言:javascript复制@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(User user) throws Exception {
userMapper.insertUser(userModel);
try {
roleService.doOtherThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。
好了以上就是整理的Spring事务在开发过程中会出现的诡异的情况。