Spring事务失效的9大场景,你踩过几个?

前言

在日常开发中,我们经常使用Spring事务。最近,一个朋友去面试,被问到了这样一个面试题:在什么情况下,Spring 事务会失效?

今天,我将和大家聊聊Spring事务失效的 9 种场景。

1. 抛出检查异常(checked exceptions)

例如,你的事务控制代码如下:

java 复制代码
@Transactional
public void transactionTest() throws IOException {
    User user = new User();
    UserService.insert(user);
    throw new IOException();
}

如果没有特别指定@Transactional,Spring 默认只会在遇到运行时异常RuntimeException或错误时回滚,而检查异常如IOException不会触发回滚。

typescript 复制代码
public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

解决方案:

知道原因后,解决方案也很简单。配置rollbackFor属性,例如:@Transactional(rollbackFor = Exception.class)

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
    User user = new User();
    UserService.insert(user);
    throw new IOException();
}

2. 业务方法本身捕获并处理了异常

ini 复制代码
@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
    try {
        User user = new User();
        UserService.insert(user);
        int i = 1 / 0;
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这个场景中,事务失效的原因也很简单。Spring 是否回滚事务取决于你是否抛出了异常。如果你自己捕获了异常,Spring 就无法处理事务了。

看了上面的代码,你可能会觉得这么简单的问题,自己不可能犯这种低级错误。但我想告诉你,我身边几乎有一半的人都曾因此困扰过。

在编写业务代码时,代码可能会更复杂,有很多嵌套的方法。稍不注意,就很容易触发这个问题。举个简单的例子,假设你有一个审计功能,每次方法执行完后,将审计结果保存到数据库中。那么代码可能会写成这样:

java 复制代码
@Service
public class TransactionService {
    @Transactional(rollbackFor = Exception.class)
    public void transactionTest() throws IOException {
        User user = new User();
        UserService.insert(user);
        throw new IOException();
    }
}

下面的切面会作用于TransactionService

ini 复制代码
@Component
publicclass AuditAspect {
    @Autowired
    private AuditService auditService;

    @Around(value = "execution (* com.dylan.service.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) {
        try {
            Audit audit = new Audit();
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            String[] strings = methodSignature.getParameterNames();
            audit.setMethod(signature.getName());
            audit.setParameters(strings);
            Object proceed = pjp.proceed();
            audit.success(true);
            return proceed;
        } catch (Throwable e) {
            log.error("{}", e);
            audit.success(false);
        }
        auditService.save(audit);
        returnnull;
    }
}

在上面的例子中,如果程序执行异常,事务也会失效。原因是Spring的事务切面优先级最低。如果异常被切面捕获,Spring 自然无法正确处理事务,因为事务管理器无法捕获到异常。

解决方案:

只需移除try-catch。虽然我们知道在处理事务时,业务代码不能自己捕获异常,但只要代码变得复杂,我们很容易不小心犯错。

3. 同一个类中的方法调用

java 复制代码
@Service
publicclass DefaultTransactionService implements Service {

    public void saveUser() throws Exception {
        // do something
        doInsert();
    }

    @Transactional(rollbackFor = Exception.class)
    public void doInsert() throws IOException {
        User user = new User();
        UserService.insert(user);
        thrownew IOException();
    }
}

这也是一个容易出错的场景。事务失效的原因也很简单。因为 Spring 的事务管理功能是通过动态代理实现的,而 Spring 默认使用 JDK 动态代理,JDK 动态代理通过接口实现,并通过反射调用目标类。简单理解,在saveUser()方法中,调用this.doInsert()时,this是真实对象,因此会直接执行doInsert的业务逻辑,而不是代理逻辑,从而导致事务失效。

解决方案:

方案 1:直接在saveUser方法上添加@Transactional注解。

方案 2:可以将这两个方法拆分到不同的类中。

方案 3:不使用注解实现事务,而是使用编程式事务来包裹需要开启事务的代码块。例如:transactionTemplate.execute()

java 复制代码
public void doInsert() throws IOException {
    transactionTemplate.execute(() -> {
        User user = new User();
        UserService.insert(user);
        throw new IOException();
    });
}

4. 方法使用了finalstatic关键字

如果 Spring 使用 Cglib 代理实现(当你的代理类没有实现接口时),而你的业务方法恰好使用了finalstatic关键字,那么事务控制也会失效。因为 Cglib 使用字节码增强技术生成被代理类的子类,并重写被代理类的方法来实现代理。如果被代理的方法使用了finalstatic关键字,子类就无法重写被代理的方法。

如果 Spring 使用 JDK 动态代理实现,JDK 动态代理是基于接口实现的,那么被finalstatic修饰的方法也无法被代理。

总之,如果方法连代理都没有,那么事务回滚肯定无法实现。

解决方案:

尽量移除方法上的finalstatic关键字。

5. 方法不是public

如果方法不是public,Spring 事务也会失效,因为在 Spring 事务管理的源码AbstractFallbackTransactionAttributeSource中,computeTransactionAttribute()方法会判断目标方法是否是public。如果不是public,则返回null

kotlin 复制代码
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null;
}

解决方案:

将当前方法的访问级别改为public

6. 传播机制使用不当

Spring事务的传播机制指的是当多个事务方法相互调用时,事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED。如果你不了解这些传播策略的原理,很容易导致事务失效。

java 复制代码
@Service
publicclass TransactionsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AddressMapper addressMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void doInsert(User user, Address address) throws Exception {
        // do something
        userMapper.insert(user);
        saveAddress(address);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveAddress(Address address) {
        // do something
        addressMapper.insert(address);
    }
}

在上面的例子中,如果用户插入失败,不会导致saveAddress()回滚,因为这里使用的传播机制是REQUIRES_NEWREQUIRES_NEW的原理是,如果当前方法没有事务,则创建一个新事务。如果当前方法已经有事务,则挂起当前事务并创建一个新事务。父事务会等到当前事务完成后才提交。如果父事务发生异常,不会影响子事务的提交。

解决方案:

将事务传播策略改为默认值REQUIREDREQUIRED的原理是,如果当前有事务,则加入该事务。如果没有事务,则创建一个新事务。父事务和被调用的事务处于同一个事务中。即使被调用的事务捕获了异常,整个事务仍然会回滚。

7. 没有被 Spring 管理

typescript 复制代码
// @Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

如果此时@Service注解被注释掉,这个类就不会被 Spring 加载为 Bean,那么这个类就不会被 Spring 管理,事务自然也会失效。

解决方案:

确保每个使用事务注解的Service都被 Spring 管理。

8. 多线程调用

scss 复制代码
@Service
publicclass UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            try {
                test();
            } catch (Exception e) {
                roleService.doOtherThing();
            }
        }).start();
    }
}

@Service
publicclass RoleService {

    @Transactional
    public void doOtherThing() {
        try {
            int i = 1 / 0;
            System.out.println("save role table data");
        } catch (Exception e) {
            thrownew RuntimeException();
        }
    }
}

我们可以看到,在事务方法add中,调用了事务方法doOtherThing,但doOtherThing是在另一个线程中被调用的。

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

我们所说的同一个事务,实际上指的是同一个数据库连接。只有在同一个数据库连接下,才能同时提交和回滚。如果在不同的线程中,获取的数据库连接肯定不同,因此它们是不同的事务。

解决方案:

这有点像分布式事务。尽量确保在同一个事务中处理。

9. 没有配置开启事务

如果在项目中没有配置 Spring 的事务管理器,即使使用了 Spring 的事务管理功能,Spring 的事务也不会生效。例如,如果你是一个 Spring Boot 项目,并且没有在 Spring Boot 项目中配置以下代码:

css 复制代码
@EnableTransactionManagement

解决方案:

确保在项目中正确配置了事务管理器。

总结

本文简要阐述了 Spring 事务的实现原理,并列出了 9 种 Spring 事务失效的场景。相信很多朋友可能都遇到过这些问题。文章也详细解释了失效的原因,希望大家对 Spring 事务有新的理解。

相关推荐
岁忧7 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao11 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干1 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
春生野草2 小时前
关于SpringMVC的整理
spring
martinzh3 小时前
Spring AI 项目介绍
后端
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠3 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github