1. 解决什么问题
让我们先从事务说起,“什么是事务?我们为什么需要事务?”。事务是一组无法被分割的操作,要么所有操作全部成功,要么全部失败。我们在开发中需要通过事务将一些操作组成一个单元,来保证程序逻辑上的正确性,例如全部插入成功,或者回滚,一条都不插入。作为程序员的我们,对于事务管理,所需要做的便是进行事务的界定,即通过类似begin transaction
和end transaction
的操作来界定事务的开始和结束。
下面是一个基本的JDBC事务管理代码:
代码语言:javascript复制// 开启数据库连接
Connection con = openConnection();
try {
// 关闭自动提交
con.setAutoCommit(false);
// 业务处理
// ...
// 提交事务
con.commit();
} catch (SQLException | MyException e) {
// 捕获异常,回滚事务
try {
con.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
// 关闭连接
try {
con.setAutoCommit(true);
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
直接使用JDBC进行事务管理的代码直观上来看,存在两个问题:
- 业务处理代码与事务管理代码混杂;
- 大量的异常处理代码(在catch中还要try-catch)。
而如果我们需要更换其他数据访问技术,例如Hibernate、MyBatis、JPA等,虽然事务管理的操作都类似,但API却不同,则需使用相应的API来改写。这也会引来第三个问题:
- 繁杂的事务管理API。
上文列出了三个待解决的问题,下面我们看Spring事务是如何解决。
2. 如何解决
2.1 繁杂的事务管理API
针对该问题,我们很容易可以想到,在众多事务管理的API上抽象一层。通过定义接口屏蔽具体实现,再使用策略模式来决定具体的API。下面我们看下Spring事务中定义的抽象接口。
在Spring事务中,核心接口是PlatformTransactionManager
,也叫事务管理器,其定义如下:
public interface PlatformTransactionManager extends TransactionManager {
// 获取事务(新的事务或者已经存在的事务)
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
getTransaction
通过入参TransactionDefinition
来获得TransactionStatus
,即通过定义的事务元信息来创建相应的事务对象。在TransactionDefinition
中会包含事务的元信息:
- PropagationBehavior:传播行为;
- IsolationLevel:隔离级别;
- Timeout:超时时间;
- ReadOnly:是否只读。
根据TransactionDefinition
获得的TransactionStatus
中会封装事务对象,并提供了操作事务和查看事务状态的方法,例如:
setRollbackOnly
:标记事务为Rollback-only,以使其回滚;isRollbackOnly
:查看是否被标记为Rollback-only;isCompleted
:查看事务是否已完成(提交或回滚完成)。
还支持嵌套事务的相关方法:
createSavepoint
:创建savepoint;rollbackToSavepoint
:回滚到指定savepoint;releaseSavePoint
:释放savepoint。
TransactionStatus
事务对象可被传入到commit
方法或rollback
方法中,完成事务的提交或回滚。
下面我们通过一个具体实现来理解TransactionStatus
的作用。以commit
方法为例,如何通过TransactionStatus
完成事务的提交。AbstractPlatformTransactionManager
是PlatformTransactionManager
接口的的实现,作为模板类,其commit
实现如下:
public final void commit(TransactionStatus status) throws TransactionException {
// 1.检查事务是否已完成
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}
// 2.检查事务是否需要回滚(局部事务回滚)
DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus, false);
return;
}
// 3.检查事务是否需要回滚(全局事务回滚)
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus, true);
return;
}
// 4.提交事务
processCommit(defStatus);
}
在commit
模板方法中定义了事务提交的基本逻辑,通过查看status
的事务状态来决定抛出异常还是回滚,或是提交。其中的processRollback
和processCommit
方法也是模板方法,进一步定义了回滚、提交的逻辑。以processCommit
方法为例,具体的提交操作将由抽象方法doCommit
完成。
protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;
doCommit
的实现取决于具体的数据访问技术。我们看下JDBC相应的具体实现类DataSourceTransactionManager
中的doCommit
实现。
protected void doCommit(DefaultTransactionStatus status) {
// 获取status中的事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
// 通过事务对象获得数据库连接对象
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" con "]");
}
try {
// 执行commit
con.commit();
}
catch (SQLException ex) {
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
在commit
和processCommit
方法中我们根据入参的TransactionStatus
提供的事务状态来决定事务行为,而在doCommit
中需要执行事务提交时将会通过TransactionStatus
中的事务对象来获得数据库连接对象,再执行最后的commit
操作。通过这个示例我们可以理解TransactionStatus
所提供的事务状态和事务对象的作用。
下面是用Spring事务API改写后的事务管理代码:
代码语言:javascript复制// 获得事务管理器
PlatformTransactionManager txManager = getPlatformTransactionManager();
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 指定事务元信息
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 获得事务
TransactionStatus status = txManager.getTransaction(def);
try {
// 业务处理
}
catch (MyException ex) {
// 捕获异常,回滚事务
txManager.rollback(status);
throw ex;
}
// 提交事务
txManager.commit(status);
无论是使用JDBC、Hibernate还是MyBatis,我们只需要传给txManager
相应的具体实现就可以在多种数据访问技术中切换。
小结:Spring事务通过
PlatformTransactionManager
、TransactionDefinition
和TransactionStatus
接口统一事务管理API,并结合策略模式和模板方法决定具体实现。
Spring事务API代码还有个特点有没有发现,SQLException
不见了。下面来看Spring事务是如何解决大量的异常处理代码。
2.2 大量的异常处理代码
为什么使用JDBC的代码中会需要写这么多的异常处理代码。这是因为Connection
的每个方法都会抛出SQLException
,而SQLException
又是检查异常,这就强制我们在使用其方法时必须进行异常处理。那Spring事务是如何解决该问题的。我们看下doCommit
方法:
protected void doCommit(DefaultTransactionStatus status) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
Connection con = txObject.getConnectionHolder().getConnection();
if (status.isDebug()) {
logger.debug("Committing JDBC transaction on Connection [" con "]");
}
try {
con.commit();
}
catch (SQLException ex) {
// 异常转换
throw new TransactionSystemException("Could not commit JDBC transaction", ex);
}
}
Connection
的commit
方法会抛出检查异常SQLException
,在catch代码块中SQLException
将被转换成TransactionSystemException
抛出,而TransactionSystemException
是一个非检查异常。通过将检查异常转换成非检查异常,让我们能够自行决定是否捕获异常,不强制进行异常处理。
Spring事务中几乎为数据库的所有错误都定义了相应的异常,统一了JDBC、Hibernate、MyBatis等不同异常API。这有助于我们在处理异常时使用统一的异常API接口,无需关心具体的数据访问技术。
小结:Spring事务通过异常转换避免强制异常处理。
2.3 业务处理代码与事务管理代码混杂
在2.1节中给出了使用Spring事务API的写法,即编程式事务管理,但仍未解决“业务处理代码与事务管理代码混杂”的问题。这时候就可以利用Spring AOP将事务管理代码这一横切关注点从代码中剥离出来,即声明式事务管理。以注解方式为例,通过为方法标注@Transaction
注解,将为该方法提供事务管理。其原理如下图所示:
声明式事务原理
Spring事务会为@Transaction
标注的方法的类生成AOP增强的动态代理类对象,并且在调用目标方法的拦截链中加入TransactionInterceptor
进行环绕增加,实现事务管理。
下面我们看下TransactionInterceptor
中的具体实现,其invoke
方法中将调用invokeWithinTransaction
方法进行事务管理,如下所示:
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
// 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 创建事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 通过回调执行目标方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 清理当前线程事务信息
cleanupTransactionInfo(txInfo);
}
// 目标方法执行成功,提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
} else {
// 带回调的事务执行处理,一般用于编程式事务
// ...
}
}
在调用目标方法前后加入了创建事务、处理异常、提交事务等操作。这让我们不必编写事务管理代码,只需通过@Transaction
的属性指定事务相关元信息。
小结:Spring事务通过AOP提供声明式事务将业务处理代码和事务管理代码分离。
3. 存在什么问题
Spring事务为了我们解决了第一节中列出的三个问题,但同时也会带来些新的问题。
3.1 非public方法失效
@Transactional
只有标注在public级别的方法上才能生效,对于非public方法将不会生效。这是由于Spring AOP不支持对private、protect方法进行拦截。从原理上来说,动态代理是通过接口实现,所以自然不能支持private和protect方法的。而CGLIB是通过继承实现,其实是可以支持protect方法的拦截的,但Spring AOP中并不支持这样使用,笔者猜测做此限制是出于代理方法应是public的考虑,以及为了保持CGLIB和动态代理的一致。如果需要对protect或private方法拦截则建议使用AspectJ。
3.2 自调用失效
当通过在Bean的内部方法直接调用带有@Transactional
的方法时,@Transactional
将失效,例如:
public void saveAB(A a, B b)
{
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a)
{
dao.saveA(a);
}
@Transactional
public void saveB(B b)
{
dao.saveB(b);
}
在saveAB中调用saveA和saveB方法,两者的@Transactional
都将失效。这是因为Spring事务的实现基于代理类,当在内部直接调用方法时,将不会经过代理对象,而是直接调用目标对象的方法,无法被TransactionInterceptor
拦截处理。解决办法:
(1)ApplicationContextAware
通过ApplicationContextAware
注入的上下文获得代理对象。
public void saveAB(A a, B b)
{
Test self = (Test) applicationContext.getBean("Test");
self.saveA(a);
self.saveB(b);
}
(2)AopContext
通过AopContext
获得代理对象。
public void saveAB(A a, B b)
{
Test self = (Test)AopContext.currentProxy();
self.saveA(a);
self.saveB(b);
}
(3)@Autowired
通过@Autowired
注解注入代理对象。
@Component
public class Test {
@Autowired
Test self;
public void saveAB(A a, B b)
{
self.saveA(a);
self.saveB(b);
}
// ...
}
(4)拆分
将saveA、saveB方法拆分到另一个类中。
代码语言:javascript复制public void saveAB(A a, B b)
{
txOperate.saveA(a);
txOperate.saveB(b);
}
上述两个问题都是由于Spring事务的实现方式的限制导致的问题。下面再看两个由于使用不当容易犯错的两个问题。
3.3 检查异常默认不回滚
在默认情况下,抛出非检查异常会触发回滚,而检查异常不会。
根据invokeWithinTransaction
方法,我们可以知道异常处理逻辑在completeTransactionAfterThrowing
方法中,其实现如下:
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" txInfo.getJoinpointIdentification()
"] after exception: " ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 异常类型为回滚异常,执行事务回滚
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
else {
try {
// 异常类型为非回滚异常,仍然执行事务提交
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}
根据rollbackOn
判断异常是否为回滚异常。只有RuntimeException
和Error
的实例,即非检查异常,或者在@Transaction
中通过rollbackFor
属性指定的回滚异常类型,才会回滚事务。否则将继续提交事务。所以如果需要对非检查异常进行回滚,需要记得指定rollbackFor
属性,不然将回滚失效。
3.4 catch异常无法回滚
在3.3节中我们说到只有抛出非检查异常或是rollbackFor
中指定的异常才能触发回滚。如果我们把异常catch住,而且没抛出,则会导致无法触发回滚,这也是开发中常犯的错误。例如:
@Transactional
public void insert(List<User> users) {
try {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
for (User user : users) {
String insertUserSql = "insert into User (id, name) values (?,?)";
jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),
user.getName() });
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里由于catch住了所有Exception
,并且没抛出。当插入发生异常时,将不会触发回滚。
但同时我们也可以利用这种机制,用try-catch包裹不用参与事务的数据操作,例如对于写入一些不重要的日志,我们可将其用try-catch包裹,避免抛出异常,则能避免写日志失败而影响事务的提交。
参考
- Spring Framework Documentation——Data Access: https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html
- 《Spring揭秘》
- 5-common-spring-transactional-pitfalls: https://codete.com/blog/5-common-spring-transactional-pitfalls/
- Spring事务原理一探: https://zhuanlan.zhihu.com/p/54067384