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 编程式
既然声明式和编程式事务各有优缺点,那么在实际开发中,我们应该如何选择呢?
一般来说,如果你的业务逻辑比较简单,不需要对事务进行精细的控制,那么声明式事务是更好的选择。 它可以让你专注于业务逻辑的开发,而不需要关心事务管理的细节。
如果你的业务逻辑比较复杂,需要对事务进行精细的控制,例如需要根据不同的情况选择不同的事务处理方式,或者需要在事务中执行一些非标准的事务操作,那么编程式事务是更好的选择。
以下是一些选择的建议:
特性 | 声明式事务 | 编程式事务 |
---|---|---|
代码侵入性 | 低 | 高 |
控制力 | 弱 | 强 |
灵活性 | 低 | 高 |
易于维护 | 是 | 否 |
开发效率 | 高 | 低 |
适用场景 | 简单业务逻辑,无需精细控制 | 复杂业务逻辑,需要精细控制,特殊事务需求 |
学习曲线 | 容易 | 稍难 |
五、实战案例:一个复杂的转账场景
为了更好地理解声明式和编程式事务的应用,我们来看一个更复杂的转账场景。
假设我们需要实现一个转账功能,要求:
- 如果转账金额大于 1000 元,需要发送短信通知。
- 如果转账失败,需要记录日志。
使用声明式事务,我们可以这样实现:
@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 的其他有趣的小伙伴。祝各位编码愉快!