Spring编程式事务全解析:封装流程、原理与常见疑问
Spring的事务管理是个让人又爱又恨的东西,声明式事务(@Transactional
)用着方便,但不够灵活;编程式事务则完全放开手脚,适合复杂场景。今天我们从最底层的实现聊起,逐步走进封装的编程式事务,剖析它与声明式的关系,再解答一个常见疑问:为什么查询不同Repository不能放在同一个@Transactional
里?通过代码、表格和分析,咱们把这些问题掰开揉碎聊清楚。
从最底层开始:朴素的DataSource事务
咱们先从最原始的方式入手,用DataSource
直接操作事务,假设要保存用户信息:
java
@Service
public class UserService {
@Autowired
private DataSource dataSource;
public void saveUser(String username) {
try (Connection conn = dataSource.getConnection()) {
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement("INSERT INTO users (username) VALUES (?)");
ps.setString(1, username);
ps.executeUpdate();
conn.commit();
} catch (SQLException e) {
try (Connection conn = dataSource.getConnection()) {
conn.rollback();
} catch (SQLException ex) {
throw new RuntimeException("Rollback failed", ex);
}
throw new RuntimeException("Save failed", e);
}
}
}
这代码很直白:拿Connection
,关自动提交,手动执行SQL,成功就提交,失败就回滚。但问题也明显:
- 代码量大:手动管理连接和异常,太繁琐。
- 资源管理麻烦:得自己释放连接,容易漏。
- 不灵活:没法跟Spring的其他机制无缝衔接。
第一步封装:PlatformTransactionManager
Spring提供了PlatformTransactionManager
,把事务逻辑抽象出来,咱们改写一下:
java
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private PlatformTransactionManager txManager;
public void saveUser(String username) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", username);
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
throw e;
}
}
}
- 封装意义 :
TxManager
接管了Connection
的获取、提交和回滚,底层还是JDBC,但开发者不用直接碰Connection
。 - 进步:事务配置(传播行为、隔离级别)可定制,资源管理交给Spring。
- 不足:还是得手动提交和回滚,代码不够简洁。
第二步封装:TransactionTemplate
再进一步,Spring提供了TransactionTemplate
,直接把提交回滚包起来:
java
@Service
public class UserService {
@Autowired
private TransactionTemplate txTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveUser(String username) {
txTemplate.execute(status -> {
jdbcTemplate.update("INSERT INTO users (username) VALUES (?)", username);
return null;
});
}
}
- 封装意义:事务的生命周期(开启、提交、回滚)全自动化,异常时自动回滚。
- 进步:代码简洁到飞起,专注业务逻辑就好。
- 联系声明式 :
@Transactional
其实就是用AOP把类似TransactionTemplate
的逻辑加到方法前后,Spring核心的事务机制一脉相承。
封装流程表格
层级 | 操作方式 | 封装内容 | 带来的好处 | 局限性 |
---|---|---|---|---|
DataSource | 直接用Connection 控制事务 |
无封装,纯手动操作 | 完全掌控,灵活性高 | 代码繁琐,资源管理复杂 |
TxManager | 用PlatformTransactionManager 管理 |
抽象事务定义和状态 | 配置灵活,与Spring集成 | 仍需手动提交回滚 |
TxTemplate | 用TransactionTemplate 执行 |
自动提交回滚,回调式API | 简洁高效,专注业务 | 粒度仍受限于单一事务 |
每一步封装都在减少重复劳动,提升开发效率,同时保留必要的灵活性。DataSource
是基础,TxManager
是桥梁,TxTemplate
是现代编程式事务的标配。
复杂场景:为什么查询不同Repository不行?
现在聊聊一个常见疑问:为什么查询不同的Repository不能放在同一个@Transactional
里?比如:
java
@Service
public class UserService {
@Autowired
private UserRepository userRepo; // 数据源1
@Autowired
private OrderRepository orderRepo; // 数据源2
@Transactional
public void process(String username) {
User user = userRepo.findByUsername(username); // 数据源1
Order order = orderRepo.findByOrderId(1L); // 数据源2
// 业务逻辑
}
}
运行后可能会发现,第二个查询压根没被事务管理,甚至报错。这是啥情况?
是数据源不同的问题吗?
大部分时候是的。Spring的事务默认绑定到一个TransactionManager
,而每个TransactionManager
通常对应一个DataSource
。上面例子中:
@Transactional
默认用userRepo
的数据源事务管理器。orderRepo
用的是另一个数据源,事务管不到它。
如果没配置多数据源支持(比如JTA
),第二个查询要么跑在独立连接上,要么压根没事务。这会导致:
- 一致性问题:异常时只有第一个数据源回滚。
- 性能隐患:多个连接没统一管理,可能锁冲突。
同数据源不同表呢?
如果userRepo
和orderRepo
用的是同一个DataSource
,那没问题,同一个事务完全可以管多个表:
java
@Transactional
public void process(String username) {
User user = userRepo.findByUsername(username); // 表users
Order order = orderRepo.findByOrderId(1L); // 表orders
// 正常事务管理
}
只要是同一DataSource
,事务没毛病。所以,关键不是"不同表",而是"不同数据源"。
优化方向:解决多数据源问题
面对多数据源,咱们得优化:
- 指定TransactionManager:
java
@Transactional(transactionManager = "userTxManager")
public void processUser(String username) {
userRepo.findByUsername(username);
}
@Transactional(transactionManager = "orderTxManager")
public void processOrder(Long orderId) {
orderRepo.findByOrderId(orderId);
}
分开事务,各自管理。
- 用编程式事务:
java
public void process(String username, Long orderId) {
TransactionTemplate userTx = new TransactionTemplate(userTxManager);
TransactionTemplate orderTx = new TransactionTemplate(orderTxManager);
userTx.execute(status -> {
userRepo.findByUsername(username);
return null;
});
orderTx.execute(status -> {
orderRepo.findByOrderId(orderId);
return null;
});
}
细粒度控制,每个数据源独立。
- 分布式事务(JTA) : 如果需要统一管理多数据源,用
Atomikos
或Bitronix
实现XA事务:
java
@Transactional // JTA事务管理器
public void process(String username, Long orderId) {
userRepo.findByUsername(username);
orderRepo.findByOrderId(orderId);
}
所有数据源一致提交或回滚。
总结
从DataSource
到TxManager
再到TxTemplate
,Spring编程式事务的封装让代码从繁琐走向优雅,每层都在解决资源管理、配置灵活性和开发效率的问题。它跟声明式事务共享核心逻辑,只是控制权交给了开发者。至于查询不同Repository的坑,主要源自数据源差异,同源不同表完全没问题。优化方向上,细粒度可以用TxTemplate
,多数据源可以用JTA,具体看场景需求。希望这篇聊下来,你对事务的"前世今生"更有感觉了!