对于从事 java 开发工作的同学来说,spring 的事务肯定再熟悉不过了。在某些业务场景下,如果同时有多张表的写入操作,为了保证操作的原子性(要么同时成功,要么同时失败)避免数据不一致的情况,我们一般都会使用 spring 事务。没错,spring 事务大多数情况下,可以满足我们的业务需求。但是今天我要告诉大家的是,它有很多坑,稍不注意事务就会失效。
1. 错误的访问权限
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
private void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我们可以看到 add
方法的访问权限被定义成了 private
,这样会导致事务失效,spring 要求被代理方法必须是 public
的。AbstractFallbackTransactionAttributeSource
类的 computeTransactionAttribute
方法中有个判断,如果目标方法不是 public
,则 TransactionAttribute
返回 null
,即不支持事务。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
// 不支持非public方法
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;
}
2. 方法被定义成 final 的
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public final void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我们可以看到 add
方法被定义成了 final
的,这样会导致 spring aop 生成的代理对象不能复写该方法,而让事务失效。
3. 方法内部调用
@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();
}
}
我们看到在事务方法 add
中,直接调用事务方法 updateStatus
。从前面介绍的内容可以知道,updateStatus
方法拥有事务的能力是因为 spring aop 生成代理了对象,但是这种方法直接调用了 this
对象的方法,所以 updateStatus
方法不会生成事务。
4. 当前实体没有被 spring 管理
//@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我们可以看到 UserService
类没有定义@Service
注解,即没有交给 spring 管理 bean 实例,所以它的 add
方法也不会生成事务。
5. 错误的 spring 事务传播特性
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
}
}
我们可以看到 add
方法的事务传播特性定义成了 Propagation.NEVER
,这种类型的传播特性不支持事务,如果有事务则会抛异常。只有这三种传播特性才会创建新事务:PROPAGATION_REQUIRED
,PROPAGATION_REQUIRES_NEW
,PROPAGATION_NESTED
。
6. 数据库不支持事务
msql8 以前的版本数据库引擎是支持 myIsam
和 innoDB
的。我以前也用过,对应查多写少的单表操作,可能会把表的数据库引擎定义成 myIsam
只支持表锁,并且不支持事务。所以,对这类表的写入操作事务会失效。
7. 自己吞掉了异常
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) {
try {
userMapper.insertUser(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
这种情况下事务不会回滚,因为开发者自己捕获了异常,又没有抛出。事务的 AOP 无法捕获异常,导致即使出现了异常,事务也不会回滚。
8. 抛出的异常不正确
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void add(UserModel userModel) throws Exception {
try {
userMapper.insertUser(userModel);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new Exception(e);
}
}
}
这种情况下,开发人员自己捕获了异常,又抛出了异常:Exception
,事务也不会回滚。因为 spring 事务,默认情况下只会回滚 RuntimeException
(运行时异常)和 Error
(错误),不会回滚 Exception
。
9. 多线程调用
@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");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
10. 嵌套事务多回滚了
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
中,并且不继续往抛异常。