Spring 事务管理(Transaction Management):声明式事务与编程式事务

Spring 事务管理:声明式与编程式,一场关于优雅与控制的对话

各位看官,今天咱不聊风花雪月,也不谈人生理想,就聊聊Spring框架里一个至关重要,却又常常被忽略的小伙伴:事务管理。这玩意儿,就像你银行卡里的安全密码,保卫着数据的完整性,让你在数据世界里也能安心躺平。

说到事务管理,就不得不提“声明式事务”和“编程式事务”这对好基友。他们一个优雅懒散,一个勤劳能干,各有千秋,各有适用场景。今天,咱们就好好扒一扒他们的底裤,看看他们到底是怎么工作的,又该如何选择。

一、什么是事务?别告诉我你不知道!

在深入了解声明式和编程式事务之前,我们先来回顾一下什么是事务。

想象一下,你去银行转账,从你的账户扣钱,然后把钱加到对方账户。这是一个完整的业务流程,必须要么全部成功,要么全部失败。如果你的账户扣了钱,但是对方账户没收到,那可就闹大了!

这就是事务的意义:将一系列操作打包成一个逻辑单元,要么全部执行成功,要么全部不执行,保证数据的一致性和完整性。

事务具有四个基本特性,通常被称为ACID:

  • 原子性(Atomicity): 事务是一个不可分割的最小单位,要么全部执行,要么全部不执行。
  • 一致性(Consistency): 事务执行前后,数据库的状态必须保持一致。
  • 隔离性(Isolation): 多个事务并发执行时,每个事务都应该感觉不到其他事务的存在。
  • 持久性(Durability): 事务一旦提交,对数据库的修改就是永久性的。

二、编程式事务:亲力亲为,掌控一切

编程式事务,顾名思义,就是通过编写代码来控制事务的开始、提交和回滚。就像一个勤劳的管家,事无巨细,都要亲自打理。

1. 使用TransactionTemplate

Spring 提供了 TransactionTemplate 类来简化编程式事务的管理。它允许你将事务逻辑封装在一个回调方法中,Spring 会自动帮你处理事务的启动、提交和回滚。

@Autowired
private PlatformTransactionManager transactionManager;

public void transferMoney(String fromAccount, String toAccount, double amount) {
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    transactionTemplate.execute(status -> {
        try {
            // 1. 从fromAccount扣款
            accountDao.withdraw(fromAccount, amount);

            // 2. 向toAccount存款
            accountDao.deposit(toAccount, amount);

            return true; // 返回true表示事务成功
        } catch (Exception e) {
            status.setRollbackOnly(); // 设置回滚
            throw e; // 抛出异常,让Spring处理回滚
        }
    });
}

在这个例子中:

  • PlatformTransactionManager 是 Spring 提供的事务管理器接口,负责实际的事务操作。你需要根据你使用的数据库配置相应的实现类,例如 DataSourceTransactionManager
  • TransactionTemplate 接受一个 TransactionCallback 对象,你需要在 doInTransaction 方法中编写你的业务逻辑。
  • 如果业务逻辑成功执行,TransactionTemplate 会自动提交事务。
  • 如果发生异常,你需要调用 status.setRollbackOnly() 设置事务回滚,并抛出异常,让 Spring 处理回滚操作。

2. 直接使用TransactionManager

当然,你也可以直接使用 TransactionManager 来控制事务。

@Autowired
private PlatformTransactionManager transactionManager;

public void transferMoney(String fromAccount, String toAccount, double amount) {
    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
        // 1. 从fromAccount扣款
        accountDao.withdraw(fromAccount, amount);

        // 2. 向toAccount存款
        accountDao.deposit(toAccount, amount);

        transactionManager.commit(status); // 提交事务
    } catch (Exception e) {
        transactionManager.rollback(status); // 回滚事务
        throw e;
    }
}

在这个例子中:

  • transactionManager.getTransaction(new DefaultTransactionDefinition()) 开启一个新的事务。DefaultTransactionDefinition 可以用来设置事务的隔离级别、传播行为等属性。
  • transactionManager.commit(status) 提交事务。
  • transactionManager.rollback(status) 回滚事务。

编程式事务的优点:

  • 控制力强: 你可以完全控制事务的开始、提交和回滚时机。
  • 灵活性高: 可以在代码中根据不同的情况选择不同的事务处理方式。

编程式事务的缺点:

  • 代码侵入性强: 需要在业务逻辑代码中编写大量的事务管理代码,使得代码变得臃肿和难以维护。
  • 样板代码多: 每次使用都需要编写类似的事务管理代码,重复性高。

三、声明式事务:优雅懒散,自动挡

声明式事务,则是通过配置的方式来声明事务。你只需要告诉 Spring 哪些方法需要事务管理,Spring 会自动帮你处理事务的开始、提交和回滚。就像一个优雅的自动挡汽车,你只需要踩油门和刹车,其他的交给它就好。

1. 基于XML配置

最传统的声明式事务配置方式是基于 XML 文件。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="transfer*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
        <tx:method name="get*" read-only="true"/>
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/>
</aop:config>

在这个例子中:

  • transactionManager bean 定义了事务管理器,指定了数据源。
  • tx:advice 定义了事务通知,指定了事务的传播行为、隔离级别、回滚规则等。
  • aop:config 定义了 AOP 配置,将事务通知应用到指定的切入点。

重点解释:

  • propagation (传播行为): 定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。常见的传播行为有:

    • REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    • REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
    • SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
    • NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。
    • MANDATORY:必须在一个已存在的事务中执行,否则抛出异常。
    • NEVER:必须在一个没有事务的环境中执行,否则抛出异常。
    • NESTED:如果当前存在事务,则创建一个嵌套事务;如果当前没有事务,则创建一个新的事务。
  • rollback-for (回滚规则): 定义了哪些异常应该导致事务回滚。默认情况下,只有未检查异常(RuntimeException及其子类)会导致事务回滚。你可以通过 rollback-for 属性指定其他异常也应该导致事务回滚。

  • read-only (只读): 将事务设置为只读模式,可以提高查询性能。

2. 基于注解配置

更现代的方式是使用注解来配置声明式事务。

import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void transferMoney(String fromAccount, String toAccount, double amount) {
        // 1. 从fromAccount扣款
        accountDao.withdraw(fromAccount, amount);

        // 2. 向toAccount存款
        accountDao.deposit(toAccount, amount);
    }

    @Transactional(readOnly = true)
    public Account getAccount(String accountId) {
        return accountDao.getAccount(accountId);
    }
}

在这个例子中:

  • @Transactional 注解标记了 transferMoney 方法需要进行事务管理。
  • propagation 属性指定了事务的传播行为。
  • rollbackFor 属性指定了哪些异常应该导致事务回滚。
  • @Transactional(readOnly = true)getAccount 方法设置为只读事务。

需要在Spring配置中开启注解驱动的事务管理:

@Configuration
@EnableTransactionManagement
public class AppConfig {
    // ... 其他配置
}

或者在XML配置中:

<tx:annotation-driven transaction-manager="transactionManager"/>

声明式事务的优点:

  • 代码侵入性低: 不需要修改业务逻辑代码,只需要通过配置或注解来声明事务。
  • 易于维护: 事务配置集中管理,方便修改和维护。
  • 开发效率高: 减少了重复的事务管理代码,提高了开发效率。

声明式事务的缺点:

  • 控制力弱: 事务的开始、提交和回滚时机由 Spring 自动管理,无法精确控制。
  • 灵活性低: 无法根据不同的情况选择不同的事务处理方式。

四、选择的艺术:声明式 vs 编程式

既然声明式和编程式事务各有优缺点,那么在实际开发中,我们应该如何选择呢?

一般来说,如果你的业务逻辑比较简单,不需要对事务进行精细的控制,那么声明式事务是更好的选择。 它可以让你专注于业务逻辑的开发,而不需要关心事务管理的细节。

如果你的业务逻辑比较复杂,需要对事务进行精细的控制,例如需要根据不同的情况选择不同的事务处理方式,或者需要在事务中执行一些非标准的事务操作,那么编程式事务是更好的选择。

以下是一些选择的建议:

特性 声明式事务 编程式事务
代码侵入性
控制力
灵活性
易于维护
开发效率
适用场景 简单业务逻辑,无需精细控制 复杂业务逻辑,需要精细控制,特殊事务需求
学习曲线 容易 稍难

五、实战案例:一个复杂的转账场景

为了更好地理解声明式和编程式事务的应用,我们来看一个更复杂的转账场景。

假设我们需要实现一个转账功能,要求:

  1. 如果转账金额大于 1000 元,需要发送短信通知。
  2. 如果转账失败,需要记录日志。

使用声明式事务,我们可以这样实现:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private SmsService smsService;

    @Autowired
    private LogService logService;

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void transferMoney(String fromAccount, String toAccount, double amount) {
        try {
            // 1. 从fromAccount扣款
            accountDao.withdraw(fromAccount, amount);

            // 2. 向toAccount存款
            accountDao.deposit(toAccount, amount);

            // 3. 如果转账金额大于 1000 元,发送短信通知
            if (amount > 1000) {
                smsService.sendSms(toAccount, "您收到一笔转账,金额为:" + amount);
            }
        } catch (Exception e) {
            // 4. 记录日志
            logService.logError("转账失败", e);
            throw e; // 抛出异常,让Spring处理回滚
        }
    }
}

使用编程式事务,我们可以这样实现:

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private SmsService smsService;

    @Autowired
    private LogService logService;

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void transferMoney(String fromAccount, String toAccount, double amount) {
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.execute(status -> {
            try {
                // 1. 从fromAccount扣款
                accountDao.withdraw(fromAccount, amount);

                // 2. 向toAccount存款
                accountDao.deposit(toAccount, amount);

                // 3. 如果转账金额大于 1000 元,发送短信通知
                if (amount > 1000) {
                    smsService.sendSms(toAccount, "您收到一笔转账,金额为:" + amount);
                }

                return true;
            } catch (Exception e) {
                // 4. 记录日志
                logService.logError("转账失败", e);
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}

在这个例子中,虽然两种方式都能实现相同的功能,但是声明式事务的代码更加简洁,易于阅读和维护。

六、总结:选择适合你的事务管理方式

总而言之,Spring 的事务管理提供了声明式和编程式两种方式。声明式事务优雅简洁,适用于简单的业务场景;编程式事务灵活可控,适用于复杂的业务场景。

选择哪种方式,取决于你的具体需求和偏好。如果你追求代码的简洁性和易维护性,那么声明式事务是更好的选择。如果你需要对事务进行精细的控制,那么编程式事务是更好的选择。

希望这篇文章能够帮助你更好地理解 Spring 的事务管理,并在实际开发中做出正确的选择。记住,没有最好的方式,只有最适合你的方式。

好了,今天的分享就到这里。下次有机会,我们再聊聊 Spring 的其他有趣的小伙伴。祝各位编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注