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

相关推荐
Java致死2 小时前
设计模式Java
java·开发语言·设计模式
源码方舟2 小时前
SpringBoot + Shiro + JWT 实现认证与授权完整方案实现
java·spring boot·后端
2401_cf5 小时前
为什么hadoop不用Java的序列化?
java·hadoop·eclipse
帮帮志5 小时前
idea整合maven环境配置
java·maven·intellij-idea
LuckyTHP5 小时前
java 使用zxing生成条形码(可自定义文字位置、边框样式)
java·开发语言·python
热河暖男5 小时前
【实战解决方案】Spring Boot+Redisson构建高并发Excel导出服务,彻底解决系统阻塞难题
spring boot·后端·excel
无声旅者8 小时前
深度解析 IDEA 集成 Continue 插件:提升开发效率的全流程指南
java·ide·ai·intellij-idea·ai编程·continue·openapi
Ryan-Joee8 小时前
Spring Boot三层架构设计模式
java·spring boot
Hygge-star8 小时前
【数据结构】二分查找5.12
java·数据结构·程序人生·算法·学习方法