共计 7280 个字符,预计需要花费 19 分钟才能阅读完成。
使用 Spring 操作 JDBC 虽然方便,但是我们在前面讨论 JDBC 的时候,讲到过 JDBC 事务,如果要在 Spring 中操作事务,没必要手写 JDBC 事务,可以使用 Spring 提供的高级接口来操作事务。
Spring 提供了一个 PlatformTransactionManager
来表示事务管理器,所有的事务都由它负责管理。而事务由 TransactionStatus
表示。如果手写事务代码,使用 try...catch
如下:
TransactionStatus tx = null;
try {// 开启事务:
tx = txManager.getTransaction(new DefaultTransactionDefinition());
// 相关 JDBC 操作:
jdbcTemplate.update("...");
jdbcTemplate.update("...");
// 提交事务:
txManager.commit(tx);
} catch (RuntimeException e) {// 回滚事务:
txManager.rollback(tx);
throw e;
}
Spring 为啥要抽象出 PlatformTransactionManager
和TransactionStatus
?原因是 JavaEE 除了提供 JDBC 事务外,它还支持分布式事务 JTA(Java Transaction API)。分布式事务是指多个数据源(比如多个数据库,多个消息系统)要在分布式环境下实现事务的时候,应该怎么实现。分布式事务实现起来非常复杂,简单地说就是通过一个分布式事务管理器实现两阶段提交,但本身数据库事务就不快,基于数据库事务实现的分布式事务就慢得难以忍受,所以使用率不高。
Spring 为了同时支持 JDBC 和 JTA 两种事务模型,就抽象出 PlatformTransactionManager
。因为我们的代码只需要 JDBC 事务,因此,在AppConfig
中,需要再定义一个 PlatformTransactionManager
对应的 Bean,它的实际类型是DataSourceTransactionManager
:
@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {
...
@Bean
PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {return new DataSourceTransactionManager(dataSource);
}
}
使用编程的方式使用 Spring 事务仍然比较繁琐,更好的方式是通过声明式事务来实现。使用声明式事务非常简单,除了在 AppConfig
中追加一个上述定义的 PlatformTransactionManager
外,再加一个 @EnableTransactionManagement
就可以启用声明式事务:
@Configuration
@ComponentScan
@EnableTransactionManagement // 启用声明式
@PropertySource("jdbc.properties")
public class AppConfig {...}
然后,对需要事务支持的方法,加一个 @Transactional
注解:
@Component
public class UserService {// 此 public 方法自动具有事务支持:
@Transactional
public User register(String email, String password, String name) {...}
}
或者更简单一点,直接在 Bean 的 class
处加上,表示所有 public
方法都具有事务支持:
@Component
@Transactional
public class UserService {...}
Spring 对一个声明式事务的方法,如何开启事务支持?原理仍然是 AOP 代理,即通过自动创建 Bean 的 Proxy 实现:
public class UserService$$EnhancerBySpringCGLIB extends UserService {UserService target = ...
PlatformTransactionManager txManager = ...
public User register(String email, String password, String name) {TransactionStatus tx = null;
try {tx = txManager.getTransaction(new DefaultTransactionDefinition());
target.register(email, password, name);
txManager.commit(tx);
} catch (RuntimeException e) {txManager.rollback(tx);
throw e;
}
}
...
}
注意:声明了 @EnableTransactionManagement
后,不必额外添加@EnableAspectJAutoProxy
。
回滚事务
默认情况下,如果发生了RuntimeException
,Spring 的声明式事务将自动回滚。在一个事务方法中,如果程序判断需要回滚事务,只需抛出RuntimeException
,例如:
@Transactional
public buyProducts(long productId, int num) {
...
if (store < num) {// 库存不够,购买失败:
throw new IllegalArgumentException("No enough products");
}
...
}
如果要针对 Checked Exception 回滚事务,需要在 @Transactional
注解中写出来:
@Transactional(rollbackFor = {RuntimeException.class, IOException.class})
public buyProducts(long productId, int num) throws IOException {...}
上述代码表示在抛出 RuntimeException
或IOException
时,事务将回滚。
为了简化代码,我们强烈建议业务异常体系从 RuntimeException
派生,这样就不必声明任何特殊异常即可让 Spring 的声明式事务正常工作:
public class BusinessException extends RuntimeException {...}
public class LoginException extends BusinessException {...}
public class PaymentException extends BusinessException {...}
事务边界
在使用事务的时候,明确事务边界非常重要。对于声明式事务,例如,下面的 register()
方法:
@Component
public class UserService {@Transactional
public User register(String email, String password, String name) {// 事务开始
...
} // 事务结束
}
它的事务边界就是 register()
方法开始和结束。
类似的,一个负责给用户增加积分的 addBonus()
方法:
@Component
public class BonusService {@Transactional
public void addBonus(long userId, int bonus) {// 事务开始
...
} // 事务结束
}
它的事务边界就是 addBonus()
方法开始和结束。
在现实世界中,问题总是要复杂一点点。用户注册后,能自动获得 100 积分,因此,实际代码如下:
@Component
public class UserService {@Autowired
BonusService bonusService;
@Transactional
public User register(String email, String password, String name) {// 插入用户记录:
User user = jdbcTemplate.insert("...");
// 增加 100 积分:
bonusService.addBonus(user.id, 100);
}
}
现在问题来了:调用方(比如 RegisterController
)调用UserService.register()
这个事务方法,它在内部又调用了 BonusService.addBonus()
这个事务方法,一共有几个事务?如果 addBonus()
抛出了异常需要回滚事务,register()
方法的事务是否也要回滚?
问题的复杂度是不是一下子提高了 10 倍?
事务传播
要解决上面的问题,我们首先要定义事务的传播模型。
假设用户注册的入口是 RegisterController
,它本身没有事务,仅仅是调用UserService.register()
这个事务方法:
@Controller
public class RegisterController {@Autowired
UserService userService;
@PostMapping("/register")
public ModelAndView doRegister(HttpServletRequest req) {String email = req.getParameter("email");
String password = req.getParameter("password");
String name = req.getParameter("name");
User user = userService.register(email, password, name);
return ...
}
}
因此,UserService.register()
这个事务方法的起始和结束,就是事务的范围。
我们需要关心的问题是,在 UserService.register()
这个事务方法内,调用BonusService.addBonus()
,我们期待的事务行为是什么:
@Transactional
public User register(String email, String password, String name) {// 事务已开启:
User user = jdbcTemplate.insert("...");
// ???:
bonusService.addBonus(user.id, 100);
} // 事务结束
对于大多数业务来说,我们期待 BonusService.addBonus()
的调用,和 UserService.register()
应当融合在一起,它的行为应该如下:
UserService.register()
已经开启了一个事务,那么在内部调用 BonusService.addBonus()
时,BonusService.addBonus()
方法就没必要再开启一个新事务,直接加入到 BonusService.register()
的事务里就好了。
其实就相当于:
UserService.register()
先执行了一条 INSERT 语句:INSERT INTO users ...
BonusService.addBonus()
再执行一条 INSERT 语句:INSERT INTO bonus ...
因此,Spring 的声明式事务为事务传播定义了几个级别,默认传播级别就是 REQUIRED,它的意思是,如果当前没有事务,就创建一个新事务,如果当前有事务,就加入到当前事务中执行。
我们观察 UserService.register()
方法,它在 RegisterController
中执行,因为 RegisterController
没有事务,因此,UserService.register()
方法会自动创建一个新事务。
在 UserService.register()
方法内部,调用 BonusService.addBonus()
方法时,因为 BonusService.addBonus()
检测到当前已经有事务了,因此,它会加入到当前事务中执行。
因此,整个业务流程的事务边界就清晰了:它只有一个事务,并且范围就是 UserService.register()
方法。
有的童鞋会问:把 BonusService.addBonus()
方法的 @Transactional
去掉,变成一个普通方法,那不就规避了复杂的传播模型吗?
去掉 BonusService.addBonus()
方法的 @Transactional
,会引来另一个问题,即其他地方如果调用BonusService.addBonus()
方法,那就没法保证事务了。例如,规定用户登录时积分 +5:
@Controller
public class LoginController {@Autowired
BonusService bonusService;
@PostMapping("/login")
public ModelAndView doLogin(HttpServletRequest req) {User user = ...
bonusService.addBonus(user.id, 5);
}
}
可见,BonusService.addBonus()
方法必须要有@Transactional
,否则,登录后积分就无法添加了。
默认的事务传播级别是REQUIRED
,它满足绝大部分的需求。还有一些其他的传播级别:
SUPPORTS
:表示如果有事务,就加入到当前事务,如果没有,那也不开启事务执行。这种传播级别可用于查询方法,因为 SELECT 语句既可以在事务内执行,也可以不需要事务;
MANDATORY
:表示必须要存在当前事务并加入执行,否则将抛出异常。这种传播级别可用于核心更新逻辑,比如用户余额变更,它总是被其他事务方法调用,不能直接由非事务方法调用;
REQUIRES_NEW
:表示不管当前有没有事务,都必须开启一个新的事务执行。如果当前已经有事务,那么当前事务会挂起,等新事务完成后,再恢复执行;
NOT_SUPPORTED
:表示不支持事务,如果当前有事务,那么当前事务会挂起,等这个方法执行完成后,再恢复执行;
NEVER
:和 NOT_SUPPORTED
相比,它不但不支持事务,而且在监测到当前有事务时,会抛出异常拒绝执行;
NESTED
:表示如果当前有事务,则开启一个嵌套级别事务,如果当前没有事务,则开启一个新事务。
上面这么多种事务的传播级别,其实默认的 REQUIRED
已经满足绝大部分需求,SUPPORTS
和 REQUIRES_NEW
在少数情况下会用到,其他基本不会用到,因为把事务搞得越复杂,不仅逻辑跟着复杂,而且速度也会越慢。
定义事务的传播级别也是写在 @Transactional
注解里的:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Product createProduct() {...}
现在只剩最后一个问题了:Spring 是如何传播事务的?
我们在 JDBC 中使用事务的时候,是这么个写法:
Connection conn = openConnection();
try {// 关闭自动提交:
conn.setAutoCommit(false);
// 执行多条 SQL 语句:
insert(); update(); delete();
// 提交事务:
conn.commit();} catch (SQLException e) {// 回滚事务:
conn.rollback();} finally {conn.setAutoCommit(true);
conn.close();}
Spring 使用声明式事务,最终也是通过执行 JDBC 事务来实现功能的,那么,一个事务方法,如何获知当前是否存在事务?
答案是使用 ThreadLocal。Spring 总是把 JDBC 相关的 Connection
和TransactionStatus
实例绑定到 ThreadLocal
。如果一个事务方法从ThreadLocal
未取到事务,那么它会打开一个新的 JDBC 连接,同时开启一个新的事务,否则,它就直接使用从 ThreadLocal
获取的 JDBC 连接以及TransactionStatus
。
因此,事务能正确传播的前提是,方法调用是在一个线程内才行。如果像下面这样写:
@Transactional
public User register(String email, String password, String name) {// BEGIN TX-A
User user = jdbcTemplate.insert("...");
new Thread(() -> {// BEGIN TX-B:
bonusService.addBonus(user.id, 100);
// END TX-B
}).start();} // END TX-A
在另一个线程中调用 BonusService.addBonus()
,它根本获取不到当前事务,因此,UserService.register()
和BonusService.addBonus()
两个方法,将分别开启两个完全独立的事务。
换句话说,事务只能在当前线程传播,无法跨线程传播。
那如果我们想实现跨线程传播事务呢?原理很简单,就是要想办法把当前线程绑定到 ThreadLocal
的Connection
和 TransactionStatus
实例传递给新线程,但实现起来非常复杂,根据异常回滚更加复杂,不推荐自己去实现。
练习
使用声明式事务。
下载练习
小结
Spring 提供的声明式事务极大地方便了在数据库中使用事务,正确使用声明式事务的关键在于确定好事务边界,理解事务传播级别。