聊聊Spring事务失效的10种场景,太坑人了

2024-05-03 21:58

本文主要是介绍聊聊Spring事务失效的10种场景,太坑人了,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

最近在看spring事务源码,时不时回想起前几年面试YY的场景,面试官拿出下面的一道面试题问我,updateStatus方法会不会生成事务。我心想,这么简单的问题还要问我,这是瞧不起我的水平吗?但是我仔细看了看,想了想,这道题还真不容易回答,如果以前没有特别注意或者研究过,很容易掉坑里。还好我当时认真想了想,回答了不会生成事务,但后面面试官继续追问为什么不产生事务,我就把具体的原因说明了一下,事后回到家我再想了想这个问题,发现我回答的并不是很好。

@Service 
public class UserService { @Autowired private UserMapper userMapper; @Transactional public void add(UserModel userModel) { userMapper.insertUser(userModel); updateStatus(userModel); } @Transactional public void updateStatus(UserModel userModel) { doSameThing(); } 

}

对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了,上述的事务场景相信大家在工作中会遇到过,但是我们有没有认真研究呢,有多少人能够讲清楚updateStatus的事务问题呢,至少我当时回答的就不是很好。

确实,spring事务用起来贼爽,我们一般就用一个简单的注解:@Transactional,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。

但如果你使用不当,它也会坑你于无形。

今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。不信,让我们一起看看。
在这里插入图片描述一、事务不生效
1.访问权限问题

众所周知,java的访问权限主要有四种:public、protected、default、private,它们的权限从左到右,依次变小。

但如果我们把某些事务方法,定义了错误的访问权限级别,就会导致事务功能出问题,例如:

@Service 
public class UserService { @Transactional private void add(UserModel userModel) { saveData(userModel); updateData(userModel); } 
}

上面定义add方法的访问权限为private,这样最终会导致事务失效,spring要求被代理方法必须是public的。

想要了解代理方法必须是public的,我们需要看spring事务的源码,在
AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) { /* Don't allow no-public methods as required. */if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); //First try is the method in the target class. TransactionAttribute txAttr = findTransactionAttribute(specificMethod); if (txAttr != null) { return txAttr; } ...................................return null; }

因此如果我们自定义方法,它的访问权限不是public,而是protected、default或者private,spring则不会提供事务功能。

2. 方法用final修饰

如果某个方法不想被子类重写,这时我们可以将该方法定义成final。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:

@Service 
public class UserService { @Transactional public final void add(UserModel userModel){ saveData(userModel); updateData(userModel); } 
}

我们可以看到add方法被定义成了final的,这样会导致事务失效。

为什么?

我看过spring事务的源码,就会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而无法添加事务功能。

注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

3.方法内部调用

有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:

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

我们看到在事务方法add中,直接调用事务方法updateOrder。从前面介绍的内容可以知道,updateOrder方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateOrder方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

3.1 新加一个Service方法

这个方法非常简单,、需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

 @Servcie public class ServiceA { @Autowired prvate ServiceB serviceB; public void save(User user) { queryData1(); queryData2(); serviceB.doSave(user); } } @Servcie public class ServiceB { @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }

3.2 在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

@Servcie 
public class ServiceA { @Autowired prvate ServiceA serviceA; public void save(User user) { queryData1(); queryData2(); serviceA.doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

答案:不会。

其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。但有些坑,如果你想进一步了解循环依赖问题,可以在网上查找spring是如何解决循环依赖的。

3.3 通过AopContent类

在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

@Servcie 
public class ServiceA { public void save(User user) { queryData1(); queryData2(); ((ServiceA)AopContext.currentProxy()).doSave(user); } @Transactional(rollbackFor=Exception.class) public void doSave(User user) { addData1(); updateData2(); } }

4.未被spring容器管理

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

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

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

5.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?

@Slf4j 
@Service 
public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); new Thread(() -> { roleService.doOtherThing(); }).start(); } 
} @Service 
public class RoleService { @Transactional public void doOtherThing() { System.out.println("保存role表数据"); } 
}

从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

二、事务不回滚
1.错误的传播特性

其实,我们在使用@Transactional注解时,是可以指定propagation参数的,扩展其配置不支持事务

@Service 
public class OrderServiceImpl implements OrderService{ @Transactionalpublic void update(Order order) {updateOrder(order);}@Transactional(propagation = Propagation.NOT_SUPPORTED)public void updateOrder(Order order) {// update order}
}

我们可以看到add方法的事务传播特性定义成了Propagation.NOT_SUPPORTED,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:NESTED,REQUIRES_NEW,REQUIRED。

2.自己吞了异常

这个也是出现比较多的场景:把异常吃了,然后又不抛出来,事务也不会回滚!比如:

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

3.手动抛了别的异常

即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

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

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

4.自定义了回滚异常

在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。

但如果这个参数的值设置错了,就会引出一些莫名其妙的问题,例如:

@Slf4j 
@Service 
public class UserService { @Transactional(rollbackFor = BusinessException.class) public void add(UserModel userModel) throws Exception { saveData(userModel); updateData(userModel); } 
}

如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

这是为什么呢?

因为如果使用默认值,一旦程序抛出了Exception,事务不会回滚,这会出现很大的bug。所以,建议一般情况下,将该参数设置成:Exception或Throwable。

5.嵌套事务回滚多了

public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); roleService.doOtherThing(); } 
} @Service 
public class RoleService { @Transactional(propagation = Propagation.NESTED) public void doOtherThing() { System.out.println("保存role表数据"); } 
}

这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。但事实是,insertUser也回滚了。

why?

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

@Slf4j 
@Service 
public class UserService { @Autowired private UserMapper userMapper; @Autowired private RoleService roleService; @Transactional public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel); try { roleService.doOtherThing(); } catch (Exception e) { log.error(e.getMessage(), e); } } 
}

可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

沐子总结了10种事务失效的场景,而在我们工作中经常发生的场景是方法内部调用、自已吞了异常、手动抛出了别的异常,大家可以着重关注这三种场景,多看几遍。

最后,如果我的文章对你有所帮助或者有所启发,欢迎关注公众号(微信搜索公众号:首席架构师专栏),里面有许多技术干货,也有我对技术的思考和感悟,还有作为架构师的验验分享;关注后回复 【面试题】,有我准备的面试题、架构师大型项目实战视频等福利 , 小编会带着你一起学习、成长,让我们一起加油!!!

这篇关于聊聊Spring事务失效的10种场景,太坑人了的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/957738

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2