spring事务失效的12种场景
引言
原文:
聊聊spring事务失效的12种场景,太坑了
读书笔记:担心大佬文章搬家,故整理此学习笔记
spring事务底层使用了aop,也就是通过jdk动态代理
或者cglib
,帮我们生成了代理类,在代理类中实现的事务功能。
一 . 事务不生效
1. 访问权限问题
自定义的事务方法),它的访问权限不是public
,而是private
、default
或protected
的话,spring
则不会提供事务功能
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
2. 方法用final,static修饰
如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能;
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
如果某个方法用static修饰了,同样无法通过动态代理,变成事务方法;(参考)
@Service
public class UserService {
@Transactional
public static void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
3. 方法内部调用
3.1 在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解是不会生效的
spring
在扫描bean
的时候会扫描方法上是否包含@Transactional
注解,如果包含,spring会为这个bean动态地生成
一个子类(即代理类,proxy
),代理类是继承
原来那个bean的。此时,当这个有注解的方法被调用
的时候,实际上是由代理类来调用
的,代理类在调用之前就会启动transaction
。然而,如果这个有注解的方法是被同一个类
中的其他方法调用的,那么该方法的调用并没有通过
代理类,而是直接通过
原来的那个bean,所以就不会启动
transaction,我们看到的现象就是@Transactional注解无效
。(参考:Spring事务管理嵌套事务详解)
3.1.1 事务方法之间的嵌套调用
@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) {
doSomeThing();
}
}
从spring容器
获取到的UserService对象
实际是包装好的proxy对象
,因此调用add方法
的对象是动态代理对象
。而在类内部add调用updateStatus
的过程中,实质执行的代码是this.updateStatus
,此处this对象是实际的serviceImpl对象
而不是本该生成的代理对象,因此直接调用了updateStatus
方法。所以updateStatus
方法的@Transactional
不生效。(参考:关于加@Transactional注解的方法之间调用,事务是否生效的问题)
3.1.2 普通方法和事务方法之间的嵌套调用
@Service
class A{
method a(){ //标记1
b();
}
@Transactinal
method b(){...}
}
伪代码表示代理类
//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:
class proxy$A{
A objectA = new A();
method a(){ //标记3
objectA.a(); //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
}
method b(){ //标记2
startTransaction();
objectA.b();
}
}
当我们调用
A的bean的a()
方法的时候,也是被proxy
A
‘
拦截
‘
,执行
‘
p
r
o
x
y
A`拦截`,执行`proxy
A‘拦截‘,执行‘proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是
objectA.a(),也就是由
原来的bean`来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。(参考:Spring事务管理嵌套事务详解)
3.2 如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
3.2.1 把这两个方法分开到不同的类中
@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.2 在该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:我是如何解决循环依赖的?)
3.2.3 通过AopContent类
@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
、@Repository
、@Component
等注解可以自动实现bean实例化和依赖注入的功能。
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
亲测:此时编译不会报错,但是启动springBoot项目时会报错
5.多线程调用
@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表数据");
}
}
spring的事务
是通过数据库连接
来实现的。当前线程中保存了一个map,key是数据源
,value是数据库连接
。
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务
,其实是指同一个数据库连接
,只有
拥有同一个
数据库连接才能
同时提交和回滚。如果在不同的线程
,拿到的数据库连接肯定是不一样
的,所以是不同的事务
。如上Demo的多线程
导致add和doOtherThing
两个方法不在同一个线程
中,获取到的数据库连接
不一样,从而是两个不同的事务
。如果想doOtherThing
方法中抛了异常
,add
方法也回滚
是不可能的
。
6.表不支持事务
mysql
数据库引擎myisam 不支持事务
,某张表的事务一直都没有生效,那不一定是spring事务的锅,最好确认一下你使用的那张表,是否支持事务
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
7.未开启事务
7.1 springBoot 项目
因为springBoot
通过DataSourceTransactionManagerAutoConfiguration
类,已经默默的开启了事务。我们所要做的事情很简单,只需要配置spring.datasource
相关参数即可。
7.2 传统的spring项目
需要在applicationContext.xml
文件中,手动配置
事务相关参数。如果忘了配置
,事务肯定是不会生效的。如果在pointcut标签
中的切入点匹配规则
,配错
了的话,有些类的事务也不会生效。
<!-- 配置事务管理器 -->
<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>
二. 事务不回滚
1.错误的传播特性
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
2.自己吞了异常
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
如果想要spring事务能够正常回滚,必须抛出
它能够处理的异常。如果没有抛异常
,则spring认为程序是正常的。
3.手动抛了别的异常
@Slf4j
@Service
public class UserService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateData(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
因为spring事务,默认情况下只
会回滚RuntimeException
(运行时异常)和Error
(错误),对于普通的Exception
(非运行时异常),它不会回滚。
4.自定义了回滚异常
@Slf4j
@Service
public class UserService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) throws Exception {
saveData(userModel);
updateData(userModel);
}
}
报错的异常不是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);
}
}
}
@Service
public class RoleService {
@Transactional(propagation = Propagation.NESTED)
public void doOtherThing() {
System.out.println("保存role表数据");
}
}
三 其他
1 大事务问题
2.编程式事务
@Autowired
private TransactionTemplate transactionTemplate;
...
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
相较于@Transactional注解声明式事务,建议使用基于TransactionTemplate的编程式事务。主要原因如下:
- 避免由于spring aop问题,导致事务失效的问题。
- 能够更小粒度的控制事务的范围,更直观。
如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。