让咱们先从事务提及,“什么是事务?咱们为何须要事务?”。事务是一组没法被分割的操做,要么全部操做所有成功,要么所有失败。咱们在开发中须要经过事务将一些操做组成一个单元,来保证程序逻辑上的正确性,例如所有插入成功,或者回滚,一条都不插入。做为程序员的咱们,对于事务管理,所须要作的即是进行事务的界定,即经过相似begin transaction和end transaction的操做来界定事务的开始和结束。html
下面是一个基本的JDBC事务管理代码:程序员
// 开启数据库链接 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进行事务管理的代码直观上来看,存在两个问题:spring
而若是咱们须要更换其余数据访问技术,例如Hibernate、MyBatis、JPA等,虽然事务管理的操做都相似,但API却不一样,则需使用相应的API来改写。这也会引来第三个问题:数据库
上文列出了三个待解决的问题,下面咱们看Spring事务是如何解决。编程
2.1 繁杂的事务管理APIapp
针对该问题,咱们很容易能够想到,在众多事务管理的API上抽象一层。经过定义接口屏蔽具体实现,再使用策略模式来决定具体的API。下面咱们看下Spring事务中定义的抽象接口。
在Spring事务中,核心接口是PlatformTransactionManager,也叫事务管理器,其定义以下:ide
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中会包含事务的元信息:线程
根据TransactionDefinition得到的TransactionStatus中会封装事务对象,并提供了操做事务和查看事务状态的方法,例如:debug
还支持嵌套事务的相关方法:代理
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"); } proce***ollback(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"); } proce***ollback(defStatus, true); return; } // 4.提交事务 processCommit(defStatus); }
在commit模板方法中定义了事务提交的基本逻辑,经过查看status的事务状态来决定抛出异常仍是回滚,或是提交。其中的proce***ollback和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改写后的事务管理代码:
// 得到事务管理器 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提供声明式事务将业务处理代码和事务管理代码分离。
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方法拆分到另外一个类中。
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