文章目录
- 1、前言
- 2、失效场景
- 2.1、Service没有被Spring管理
- 2.2、事务方法被final、static关键字修饰
- 2.3、同一个类中,方法内部调用
- 2.4、方法的访问权限不是public
- 2.5、数据库的存储引擎不支持事务
- 2.6、@Transactional 注解配置错误
- 2.7、使用了错误的事务传播机制
- 2.8、rollbackFor属性配置错误
- 2.9、异常被捕获并处理了,没有抛出
- 2.10、手动抛了别的异常
- 2.11、多线程调用场景
- 3、总结
1、前言
作为后端程序员,在日常开发中,经常会遇到事务处理的场景,在Spring中,为了更好的支撑我们进行数据库操作,它提供了两种事务管理的方式:
- 编程式事务
- 声明式事务
那众所周知,我们平时用的最多的就是声明式事务,也就是使用**@Transactional**注解的方式了
但是在日常开发中,如果对注解@Transactional使用不当的话,可能会导致事务失效,所以今天我们一起来总结梳理一下常见的一些失效场景,我这里梳理了下面这些场景:
2、失效场景
2.1、Service没有被Spring管理
看如下代码:
package org.wujiangbo.service.impl.user;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author bobo(weixin:javabobo0513)
*/
//@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Resource
private UserMapper userMapper;
@Resource
private SysOperLogMapper logMapper;
@Override
@Transactional
public JSONResult addUser(User user, SysOperLog log) {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;//制造异常:发生算数异常
return JSONResult.success("操作成功");
}
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
上面例子中, @Service
注解注释之后,spring
事务(@Transactional
)没有生效,因为Spring
事务是由AOP
机制实现的,也就是说从Spring IOC
容器获取bean
时,Spring
会为目标类创建代理来支持事务。但是@Service
被注释后,你的service
类都不是spring
管理的,那怎么创建代理类来支持事务呢,所以此种场景事务注解会失效,大家在开发过程中要仔细了,不要忘记,将@Transactional所在的类,交给Spring管理
2.2、事务方法被final、static关键字修饰
看如下代码:
package org.wujiangbo.service.impl.user;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author bobo(weixin:javabobo0513)
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Resource
private UserMapper userMapper;
@Resource
private SysOperLogMapper logMapper;
@Override
@Transactional
public final JSONResult addUser(User user, SysOperLog log) {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
return JSONResult.success("操作成功");
}
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
如果一个方法被声明为final
或者static
,则该方法不能被子类重写,也就是说无法在该方法上进行动态代理,这会导致Spring
无法生成事务代理对象来管理事务
2.3、同一个类中,方法内部调用
看下面代码:
package org.wujiangbo.service.impl.user;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author bobo(weixin:javabobo0513)
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Resource
private UserMapper userMapper;
@Resource
private SysOperLogMapper logMapper;
@Override
public JSONResult addUser(User user, SysOperLog log) {
doSomething(user, log);
return JSONResult.success("操作成功");
}
@Transactional
public void doSomething(User user, SysOperLog log){
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
}
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
事务是通过Spring AOP
代理来实现的,而在同一个类中,一个方法调用另一个方法时,调用方法直接调用目标方法的代码,而不是通过代理类进行调用。即以上代码,调用目标doSomething
方法不是通过代理类进行的,因此事务不生效
2.4、方法的访问权限不是public
看下面代码:
package org.wujiangbo.service.impl.user;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.wujiangbo.domain.log.SysOperLog;
import org.wujiangbo.domain.user.User;
import org.wujiangbo.mapper.log.SysOperLogMapper;
import org.wujiangbo.mapper.user.UserMapper;
import org.wujiangbo.query.user.UserQuery;
import org.wujiangbo.result.JSONResult;
import org.wujiangbo.service.user.UserService;
import org.wujiangbo.utils.StringUtils;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 用户表 服务实现类
* </p>
*
* @author bobo(weixin:javabobo0513)
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Resource
private UserMapper userMapper;
@Resource
private SysOperLogMapper logMapper;
@Override
@Transactional
private JSONResult addUser(User user, SysOperLog log) {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
return JSONResult.success("操作成功");
}
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
spring
事务方法addUser
的访问权限不是public
,所以事务就不生效了,因为Spring
事务是由AOP
机制实现的,AOP
机制的本质就是动态代理,而代理的事务方法不是public
的话,computeTransactionAttribute()
就会返回null,也就是这时事务属性不存在了
大家可以看下AbstractFallbackTransactionAttributeSource
的源码:
2.5、数据库的存储引擎不支持事务
Spring事务的底层,还是依赖于数据库本身的事务支持。在MySQL
中,MyISAM
存储引擎是不支持事务的,InnoDB
引擎才支持事务。因此开发阶段设计表的时候,必须要确认你的选择的存储引擎是支持事务的
比如下面的SQL创建用户表时,就采用的是InnoDB存储引擎:
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
2.6、@Transactional 注解配置错误
看如下代码:
@Transactional(readOnly = true)
public JSONResult addUser(User user, SysOperLog log) {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
return JSONResult.success("操作成功");
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
虽然使用了@Transactional
注解,但是注解中的readOnly=true
属性指示这是一个只读事务,因此在保存数据时会抛出如下异常:
我们使用@Transactional注解时,一般不需要跟后面的readOnly属性
2.7、使用了错误的事务传播机制
看如下代码:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public JSONResult addUser(User user, SysOperLog log) {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
return JSONResult.success("操作成功");
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
这里事务失效的原因是:Propagation.NOT_SUPPORTED
表示传播特性不支持事务
我们一起来回顾下Spring
提供了七种事务传播机制。它们分别是:
REQUIRED
(默认):如果当前存在一个事务,则加入该事务;否则,创建一个新事务。该传播级别表示方法必须在事务中执行。SUPPORTS
:如果当前存在一个事务,则加入该事务;否则,以非事务的方式继续执行。MANDATORY
:如果当前存在一个事务,则加入该事务;否则,抛出异常。REQUIRES_NEW
:创建一个新的事务,并且如果存在一个事务,则将该事务挂起。NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在一个事务,则将该事务挂起。NEVER
:以非事务方式执行操作,如果当前存在一个事务,则抛出异常。NESTED
:如果当前存在一个事务,则在嵌套事务内执行。如果没有事务,则按REQUIRED
传播级别执行。嵌套事务是外部事务的一部分,可以在外部事务提交或回滚时部分提交或回滚。
2.8、rollbackFor属性配置错误
看如下代码:
@Transactional(rollbackFor = Error.class)
public JSONResult addUser(User user, SysOperLog log) throws Exception {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
if(1 == 1){
//模拟抛出异常
throw new Exception();
}
return JSONResult.success("操作成功");
}
分析:
rollbackFor
属性指定的异常必须是Throwable
或者其子类。默认情况下,RuntimeException
和Error
两种异常都是会自动回滚的。但是因为以上的代码例子,指定了rollbackFor = Error.class
,但是抛出的异常又是Exception
,而Exception和Error
没有任何什么继承关系,因此事务就不生效
大家可以看一下Transactional
注解源码:
2.9、异常被捕获并处理了,没有抛出
看如下代码:
@Transactional
public JSONResult addUser(User user, SysOperLog log){
try {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
}
catch (Exception e){
e.printStackTrace();
}
return JSONResult.success("操作成功");
}
代码执行结果:
虽然发生了算数异常,但是用户数据和日志数据还是会存到数据库之中
分析:
事务中的异常已经被业务代码捕获并处理,而没有被正确地传播回事务管理器,事务将无法回滚
我们可以从spring
源码(TransactionAspectSupport
这个类)中找到答案:
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
//省略其他代码,只留了下面核心代码
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
//Spring AOP中MethodInterceptor接口的一个方法,它允许拦截器在执行被代理方法之前和之后执行额外的逻辑。
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
//用于在发生异常时完成事务(如果Spring catch不到对应的异常的话,就不会进入回滚事务的逻辑)
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//用于在方法正常返回后提交事务。
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
在invokeWithinTransaction
方法中,当Spring
catch到Throwable
异常的时候,就会调用completeTransactionAfterThrowing()
方法进行事务回滚的逻辑。但是在我们测试代码中,直接把异常catch
住了,并没有重新throw
出来,因此 Spring
自然就catch
不到异常啦,因此事务回滚的逻辑就不会执行,事务就失效了
解决方案
在spring
事务方法中,当我们使用了try-catch
,如果catch住异常,记录完异常日志,一定要重新把异常抛出来,正例如下:
@Transactional
public JSONResult addUser(User user, SysOperLog log){
try {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
int i = 1/0;
}
catch (Exception e){
e.printStackTrace();
throw e;
}
return JSONResult.success("操作成功");
}
在catch中添加:throw e;
2.10、手动抛了别的异常
看下面代码:
@Transactional
public JSONResult addUser(User user, SysOperLog log) throws Exception {
//新增用户信息
userMapper.insert(user);
//新增日志记录
logMapper.insert(log);
if(1 == 1){
//模拟抛出异常
throw new Exception();
}
return JSONResult.success("操作成功");
}
分析:
Spring默认只处理RuntimeException和Error
或其子类,对于普通的Exception
是不会回滚的,但是上面的代码例子中,手动抛了Exception
异常,所以是不会回滚,除非用rollbackFor
属性指定,如下:
@Transactional(rollbackFor = Exception.class)
2.11、多线程调用场景
看下面代码:
@Transactional
public JSONResult addUser(User user, SysOperLog log){
try {
//新增用户信息
userMapper.insert(user);
//多线程调用
new Thread(() -> {
//新增日志记录
logMapper.insert(log);
}).start();
//模拟异常
int i = 1/0;
}
catch (Exception e){
e.printStackTrace();
throw e;
}
return JSONResult.success("操作成功");
}
代码执行结果:
虽然发生了算数异常,但是日志数据还是会存到数据库之中,只有用户数据会回滚
分析:
这是因为Spring
事务是基于线程绑定的,每个线程都有自己的事务上下文,而多线程环境下可能会存在多个线程共享同一个事务上下文的情况,导致事务不生效
我们可以进入到TransactionAspectSupport类的prepareTransactionInfo方法中看一下,有一个解释如下:
简单翻译:
从这里我们得知,事务信息是跟线程绑定的。
因此在多线程环境下,事务的信息都是独立的,将会导致Spring在接管事务上出现差异
3、总结
经过这样的总结梳理,相信你应该已经对@Transactional 注解使用的一些坑有所了解了,以后在开发过程中就要格外注意了