Spring是如何保证事务获取同一个Connection的
相信这个问题,有了上面的理论支撑,此处不用再大花篇幅了。~以JdbcTemplate为例一笔带过。
JdbcTemplate执行SQL的方法主要分为update和query方法,他俩底层最终都是依赖于execute方法去执行(包括存储函数、储存过程),所以只需要看看execute是怎么获取connection链接的?
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations { ... public <T> T execute(StatementCallback<T> action) throws DataAccessException { ... // dataSource就是此JdbcTemplate所关联的数据源,这个在config配置文件里早就配置好了 // 显然,这里获取的连接就是事务相关的,和当前想成绑定的connection Connection con = DataSourceUtils.getConnection(obtainDataSource()); ... finally { JdbcUtils.closeStatement(stmt); DataSourceUtils.releaseConnection(con, getDataSource()); } } ... }
TransactionSynchronizationManager内部用ThreadLocal<Map<Object, Object>>对象存储资源,key为DataSource、value为connection对应的ConnectionHolder对象。
以上,就是它保证统一的核心原因,其它持久化框架处理方法都类似~
TransactionSynchronization的实现类们
首先就是TransactionSynchronizationAdapter,从明白中就能看出它仅仅是个Adapter适配器而已,并不做实事。但是这个适配器它额外帮我们实现了Ordered接口,所以子类们不用再显示实现了,这样非常利于我们书写匿名内部类去实现它,这一点还是很暖心的~~
public abstract class TransactionSynchronizationAdapter implements TransactionSynchronization, Ordered { @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } ... // 省略空实现们~~~ }
其余实现均为内部类实现,比如DataSourceUtils.ConnectionSynchronization、SimpleTransactionScope.CleanupSynchronization。还有后面会碰到的一个相对重要的的内部类实现:ApplicationListenerMethodTransactionalAdapter.TransactionSynchronizationEventAdapter,它和事务监听机制有关~
问题场景二模拟
场景一借助TransactionSynchronizationManager解决了“先插入再异步异步线程查询不到”的问题,也就是著名的:Spring如何在数据库事务提交成功后进行异步操作问题~~
case1最多就是丢失部分信息记录,影响甚微(毕竟非常重要的步骤并不建议使用这种异步方式去实现和处理~)。
case2也就是本case最坏情况最终会导致Spring准备好的所有的connection都被close,从而以后再次请求的话拿到的都是已关闭的连接,最终可能导致整个服务的不可用,可谓非常严重。本case主要是为了模拟出上面Spring官方Note的说明,使用时需要注意的点~
其实如果你在afteCommit里面如果不直接直接使用connection链接,是不会出现链接被关闭问题的。因为现在的高级框架都很好的处理了这个问题
下面我模拟此场景的代码如下:
@Slf4j @Service public class HelloServiceImpl implements HelloService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional @Override public Object hello(Integer id) { // 向数据库插入一条记录 String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)"; jdbcTemplate.update(sql); TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { // 在事务提交之后执行的代码块(方法) 此处使用TransactionSynchronizationAdapter,其实在Spring5后直接使用接口也很方便了~ @Override public void afterCommit() { String sql = "insert into user (id,name,age) values (" + (id + 1) + ",'fsx',21)"; int update = jdbcTemplate.update(sql); log.info(update + ""); } }); return "service hello"; } }
预期结果:本以为第二个insert是插入不进去的(不是报错,而是持久化不了),但是最终结果是:两条记录都插入成功了。
what a fuck,有点打我脸,挺疼。,与我之前掌握的理论相悖了,与Spring的javadoc里讲述的也相悖了(其实与Spring的并没有相悖,毕竟人家说的是“可能”,可见话不能说太满的重要性,哈哈)。这勾起了我的深入探索,究竟咋回事呢???
下面我把我的研究结果直接描述如下:
afterCommit()内的connection也提交成功的原因分析
按照AbstractPlatformTransactionManager事务的源码执行处:
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable { ... private void processCommit(DefaultTransactionStatus status) throws TransactionException { ... try { prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); ... doCommit(status); // 事务正常提交后 当然triggerAfterCompletion方法上面回滚里有而有个执行 此处不贴出了 try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } } ... // 清楚、回收事务相关的资源~~~ 并且恢复底层事务(若需要~) private void cleanupAfterCompletion(DefaultTransactionStatus status) { status.setCompleted(); if (status.isNewSynchronization()) { TransactionSynchronizationManager.clear(); } if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); } if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction after completion of inner transaction"); } Object transaction = (status.hasTransaction() ? status.getTransaction() : null); resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); } } }
可以明确的看到执行到triggerAfterCommit/triggerAfterCompletion的时候doCommit是执行完成了的,也就是说这个时候事务肯定是已经提交成功了(此时去数据库里查看此记录也确定已经持久化)。
所以我猜测:后续该connection是不可能再执行connection.commit()方法了的,因为同一个事务只可能被提交一次。从上面理论知道:即使我们在afterCommit()里执行,Spring也保证了我拿到的链接还是当前线程所属事务的Connection
因此我继续猜测:connection的自动提交功能可能是在这期间被恢复了,从而导致了这条SQL语句它的自动提交成功。
关于Connection的自动提交机制,以及事务对它的“影响干预”,请参与上面的推荐博文了解,有详细的表述
来到finally里cleanupAfterCompletion方法里有这么一句:
// 这里最终都会被执行~~~ // doCleanupAfterCompletion方法在本抽象类是一个空的protected方法 // 子类可以根据自己的需要,自己去实现事务提交完成后的操作 if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); }
我们大都使用的是子类DataSourceTransactionManager
,本例也一样使用的是它。因此可以看看它对doCleanupAfterCompletion
此方法的实现:
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { ... @Override protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; // 释放资源~~ Remove the connection holder from the thread, if exposed. if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.unbindResource(obtainDataSource()); } // Reset connection. Connection con = txObject.getConnectionHolder().getConnection(); try { // 这里是关键,在事后会恢复链接的自动提交本能,也就是常用的恢复现场机制嘛~~ // 显然这个和isMustRestoreAutoCommit属性的值有关,true就会恢复~~~ if (txObject.isMustRestoreAutoCommit()) { con.setAutoCommit(true); } DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); } ... txObject.getConnectionHolder().clear(); } ... }
从上注释可知,现在问题的关键的就是DataSourceTransactionObject对象isMustRestoreAutoCommit的属性值了,若它是true,那就完全符合我的猜想。
DataSourceTransactionObject
关于DataSourceTransactionObject,它是一个DataSourceTransactionManager的一个私有内部静态类。
private static class DataSourceTransactionObject extends JdbcTransactionObjectSupport { // 来自父类 @Nullable private ConnectionHolder connectionHolder; @Nullable private Integer previousIsolationLevel; private boolean savepointAllowed = false; // 来自本类 private boolean newConnectionHolder; private boolean mustRestoreAutoCommit; // 决定是否要恢复自动提交 默认情况下是false的 ... public void setMustRestoreAutoCommit(boolean mustRestoreAutoCommit) { this.mustRestoreAutoCommit = mustRestoreAutoCommit; } public boolean isMustRestoreAutoCommit() { return this.mustRestoreAutoCommit; } }
这个内部类很简单,就是聚合了一些属性值,此处我们只关注mustRestoreAutoCommit这个属性值是否被设置为true了,若被设置过,就符合我的预期和猜想了。
通过代码跟踪,发现DataSourceTransactionManager在doBegin的时候调用了setMustRestoreAutoCommit方法如下:
@Override protected void doBegin(Object transaction, TransactionDefinition definition) { ... if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); // 此处设置值为true if (logger.isDebugEnabled()) { logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); } con.setAutoCommit(false); } }
此处代码也就是当开启事务(doBegin)的时候的关键代码,它对DataSourceTransactionObject打入标记,表示最终需要事务它返还给链接自动提交的能力。
综上所述:上述案例Demo最终成功插入了两条数据的结果是完全正确,且我的猜想都解释通了。
备注:case2我本想构造的是在afterCommit()里使用connection而最终被错误关闭的情况case,目前来看若使用的是DataSourceTransactionManager这个事务管理器的话,是不用担心这种情况发生的,最终你的SQL都会被成功提交,也不会出现被误close掉的问题~
总结
这一篇文章的主旨是讲解Spring事务的同步机制,虽然这以能力可能是Spring提供的小众功能,但正所谓小脾气、大能力描述它就很贴切~
我认为如果你真的想去了解一门技术的时候,还是不要放过每一个细节,把它融汇贯通,这样再学习一个新的技术就很容易举一反三了。(因为没有一句代码、注释都是无用的,否则它是废代码,就没有存在的必要)
最后可以分享一个找问题的小小小技巧:大胆猜测,小心求证