目录
1.代理失效
1.1 非public方法
spring 要求被代理方法必须是public的。也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是 private、default 或 protected 的话,spring 则不会提供事务功能。
@Service
public class UserService {
@Transactional
private void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
1.2 方法用final或者static修饰
有时候,某个方法不想被子类重写,这时可以将该方法定义成 final 的。普通方法这样定义是没问题的,但如果将事务方法定义成 final,例如:
@Service
public class UserService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
updateData(userModel);
}
}
spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现了事务功能。 但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而无法添加事务功能。
注意:如果某个方法是 static 的,同样无法通过动态代理,因为它是属于类的,并不是对象的,所以无法被AOP
1.3 方法内部调用
有时候我们需要在某个 Service 类的某个方法中,调用另外一个事务方法,比如:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
我们看到在非事务方法 add 中,直接调用事务方法 updateStatus。从前面介绍的内容可以知道,updateStatus 方法拥有事务的能力是因为 spring aop 生成代理了对象,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。
解决办法
1.3.1 通过AopContext拿到代理对象,来调用
注意:(需要在配置类中开启 @EnableAspectJAutoProxy(exposeProxy = true))
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
((UserService) AopContext.currentProxy()).updateStatus(userModel);
}
@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}
1.3.2 依赖注入自己(代理)来调用
其实本质就是将调用的方法移动到另一个Bean中
@Service
public class UserService {
@Autowired
private UserService1 proxy;
@Autowired
private UserMapper userMapper;
public void add(UserModel userModel) {
userMapper.insertUser(userModel);
proxy.updateStatus(userModel);
}
}
@Service
public class UserService1 {
@Transactional
public void updateStatus(UserModel userModel){
doSameThing();
}
}
1.3.3 使用编程式事务来管理
如果无法避免使用 this 调用,并且需要确保事务的正确管理,可以考虑使用编程式事务管理,手动控制事务的开启、提交和回滚。虽然相对于声明式事务管理来说更为复杂,但是可以绕过Spring代理的限制。
1.4 类未被Spring管理
在我们平时开发过程中,有个细节很容易被忽略,即使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。通常情况下,我们通过 @Controller、@Service、@Component、@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。
如果有一天,你匆匆忙忙地开发了一个 Service 类,但忘了加 @Service 注解,比如:
//@Service
public class UserService {
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
从上面的例子,我们可以看到 UserService 类没有加@Service注解,那么该类不会交给 spring 管理,所以它的 add 方法也不会生成事务。
2.@Transaction使用问题
关于@Transaction注解相关参数问题可以看下写的这篇博客:Spring事务中的@Transactional注解中的参数说明-CSDN博客
2.1 用错注解
使用的不是org.springframework.transaction.annotation包下的注解
2.2 设置了错误的传播特性
其实,我们在使用@Transactional注解时,是可以指定propagation参数的。
如果我们在手动设置 propagation 参数的时候,把传播特性设置错了,比如:
@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.3 rollbackFor属性设置错误
在使用 @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 有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。
这是为什么呢?
因为如果使用默认值 (rollbackFor默认值为UncheckedException,包括了RuntimeException和Error.),一旦程序抛出了 Exception,事务不会回滚,这会出现很大的 bug。所以,建议一般情况下,将该参数设置成:Exception 或 Throwable。
3.其他
3.1 数据库引擎不支持事务
众所周知,在 mysql5 之前,默认的数据库引擎是myisam。
它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比 innodb 更好。有些老项目中,可能还在用它。在创建表的时候,只需要把ENGINE参数设置成MyISAM即可:
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 好用,但有个很致命的问题是:不支持事务。
如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。
3.2 自己捕获了异常
事务不会回滚,最常见的问题是:开发者在代码中手动 try...catch 了异常。比如:
@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 事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则 spring 认为程序是正常的。
3.3 多线程调用
如果是使用的@Transactional这种声明式事务的话,在多线程情况下是无法生效的,主要是因为这种方式是通过ThreadLocal来存储事务上下文的,而ThreadLocal是线程隔离的,即每个线程都有自己的事务上下文,因为在多线程情况下这种方式会失效,即新线程中的操作不会被包含在原有的事务中,不过,如果需要管理跨线程的事务,我们可以使用编程式事务,即自己用TransactionTemplate来控制事务的提交