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 事务有新的理解。

相关推荐
我的golang之路果然有问题5 分钟前
案例速成GO+redis 个人笔记
经验分享·redis·笔记·后端·学习·golang·go
碎叶城李白10 分钟前
NIO简单群聊
java·nio
嘻嘻嘻嘻嘻嘻ys16 分钟前
《Vue 3.3响应式革新与TypeScript高效开发实战指南》
前端·后端
暮乘白帝过重山25 分钟前
路由逻辑由 Exchange 和 Binding(绑定) 决定” 的含义
开发语言·后端·中间件·路由流程
xxjiaz29 分钟前
水果成篮--LeetCode
java·算法·leetcode·职场和发展
CHQIUU29 分钟前
告别手动映射:在 Spring Boot 3 中优雅集成 MapStruct
spring boot·后端·状态模式
广西千灵通网络科技有限公司39 分钟前
基于Django的个性化股票交易管理系统
后端·python·django
CodeFox1 小时前
动态线程池 v1.2.1 版本发布,告警规则重构,bytebuddy 替换 cglib,新增 jmh 基准测试等!
java·后端
tonydf1 小时前
0帧起手本地跑一下BitNet
后端·ai编程
ℳ₯㎕ddzོꦿ࿐1 小时前
Java集成Zxing和OpenCV实现二维码生成与识别工具类
java·opencv