深入浅出 Spring 事务:从青铜到王者之路

93 阅读6分钟

前言

在日常的开发工作里,数据库操作是绕不开的步骤。事务能保证我们的数据操作,要么全部成功,要么全部失败。在 Spring 框架里,事务管理功能更是强大且便捷。

什么是事务

在数据库操作中,事务是由一组操作构成的逻辑单元,这组操作要么全部成功执行,要么全部不执行。事务有四个重要的特性,简称 ACID:

  • 原子性(Atomicity) :事务就像一个不可分割的原子,里面包含的操作要么全部完成,要么全部不做。比如你去银行转账,从 A 账户转 100 元到 B 账户,这个操作包含从 A 账户扣款和给 B 账户加款两个步骤,这两个步骤必须作为一个整体,要么都成功,要么都失败。
  • 一致性(Consistency) :事务执行前后,数据库的数据要保持一致性状态。还是拿转账来说,转账前 A 和 B 账户的总金额是一定的,转账后这个总金额也不能变。
  • 隔离性(Isolation) :多个事务并发执行时,相互之间不能干扰。比如有两个用户同时给同一个账户转账,这两个事务应该相互隔离,不能出现数据混乱。
  • 持久性(Durability) :一旦事务提交成功,它对数据库的改变就是永久性的,即使系统崩溃也不会丢失。

Spring 事务管理概述

Spring 为我们提供了方便的事务管理机制,主要有两种实现方式:编程式事务管理和声明式事务管理。

编程式事务管理

编程式事务管理就是在代码里手动管理事务的开启、提交和回滚。虽然这种方式比较灵活,但代码会显得比较繁琐(基本不用这个)下面是一个简单的示例:

@Service
public class UserService {
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void transferMoney() {
        TransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            // 执行数据库操作
            // ...
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            throw e;
        }
    }
}

在这个例子中,我们手动获取事务状态,执行操作,成功就提交,失败则回滚。

声明式事务管理

声明式事务管理是 Spring 推荐的方式,它通过 AOP(面向切面编程)实现,将事务管理代码和业务逻辑代码分离,让代码更加简洁。声明式事务管理又分为基于 XML 配置和基于注解两种方式,下面重点介绍基于注解的方式。

基于注解的声明式事务管理

开启事务支持

要使用基于注解的事务管理,首先需要在 Spring 配置类上添加 @EnableTransactionManagement 注解:

@Configuration
@EnableTransactionManagement
public class AppConfig {
    // 数据源、事务管理器等配置
}

使用 @Transactional 注解

@Transactional 注解可以用在类或者方法上。用在类上时,表示这个类的所有公共方法都开启事务;用在方法上时,只对该方法开启事务。示例如下:

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    @Transactional
    public void createOrder(Order order) {
        // 执行数据库操作
        orderRepository.save(order);
        // 模拟异常
        if (true) {
            throw new RuntimeException("模拟异常");
        }
    }
}

在这个例子中,createOrder 方法被 @Transactional 注解标记,当方法正常执行时,事务会自动提交;如果抛出异常,事务会自动回滚。

@Transactional 注解的属性

@Transactional 注解有很多属性,下面介绍几个常用的:

  • propagation:事务传播行为,定义了事务方法和调用它的方法之间事务的关系。常见的传播行为有 REQUIRED(默认值,如果当前没有事务,就新建一个事务;如果有,就加入到当前事务中)、REQUIRES_NEW(总是新建一个新事务,挂起当前事务)等。
  • isolation:事务隔离级别,用来解决多个事务并发访问时可能出现的问题,如脏读、不可重复读、幻读等。常见的隔离级别有 READ_COMMITTED(读已提交,避免脏读)、REPEATABLE_READ(可重复读,避免脏读和不可重复读)等。
  • rollbackFor:指定哪些异常会触发事务回滚,默认情况下,只有 RuntimeException 和 Error 会触发回滚。
  • noRollbackFor:指定哪些异常不会触发事务回滚。
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public void updateOrder(Order order) {
    // 执行数据库操作
}

事务的传播行为

事务的传播行为定义了在一个事务方法调用另一个事务方法时,事务该如何处理。Spring 定义了 7 种传播行为,下面介绍几种常用的:

  • REQUIRED:如果当前没有事务,就创建一个新事务;如果有,就加入到当前事务中。这是最常用的传播行为。
  • REQUIRES_NEW:总是创建一个新事务,挂起当前事务,直到新事务执行完毕。
  • SUPPORTS:如果当前有事务,就加入到当前事务中;如果没有,就以非事务方式执行。
  • NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • MANDATORY:如果当前存在事务,就加入该事务;如果当前没有事务,就抛出异常。
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    @Transactional
    public void methodA() {
        // 执行操作
        serviceB.methodB();
        // 继续执行操作
    }
}

@Service
public class ServiceB {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 执行操作
    }
}

methodA 开启了一个事务,调用 methodB 时,methodB 会创建一个新的事务,与 methodA 的事务相互独立。

事务的隔离级别

事务的隔离级别用来解决多个事务并发访问时可能出现的问题,主要有以下几种:

  • READ_UNCOMMITTED:允许读取未提交的数据,可能会出现脏读、不可重复读和幻读问题。
  • READ_COMMITTED:只能读取已经提交的数据,避免了脏读,但可能会出现不可重复读和幻读问题。
  • REPEATABLE_READ:确保在同一个事务中多次读取同一数据的结果是一样的,避免了脏读和不可重复读,但可能会出现幻读问题。
  • SERIALIZABLE:最高的隔离级别,通过强制事务串行执行,避免了所有并发问题,但会影响性能。
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void readData() {
    // 执行读取操作
}

总结

Spring 事务管理为我们提供了强大而灵活的功能,通过编程式和声明式两种方式,满足不同场景的需求。声明式事务管理使用起来更加方便,结合 @Transactional 注解的属性,可以灵活控制事务的传播行为和隔离级别。在实际开发中,我们要根据业务需求合理选择事务的传播行为和隔离级别,确保数据的一致性和完整性。

希望通过这篇文章,大家对 Spring 事务能有更深入的理解,能够在开发中熟练运用 Spring 事务管理功能。