Spring是如何保证同一事务获取同一个Connection的?使用Spring的事务同步机制解决:数据库刚插入的记录却查询不到的问题【享学Spring】(下)

简介: Spring是如何保证同一事务获取同一个Connection的?使用Spring的事务同步机制解决:数据库刚插入的记录却查询不到的问题【享学Spring】(下)

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提供的小众功能,但正所谓小脾气、大能力描述它就很贴切~


我认为如果你真的想去了解一门技术的时候,还是不要放过每一个细节,把它融汇贯通,这样再学习一个新的技术就很容易举一反三了。(因为没有一句代码、注释都是无用的,否则它是废代码,就没有存在的必要)


最后可以分享一个找问题的小小小技巧:大胆猜测,小心求证

相关文章
|
7天前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
10天前
|
Java 测试技术 数据库
Spring事务传播机制(最全示例)
在使用Spring框架进行开发时,`service`层的方法通常带有事务。本文详细探讨了Spring事务在多个方法间的传播机制,主要包括7种传播类型:`REQUIRED`、`SUPPORTS`、`MANDATORY`、`REQUIRES_NEW`、`NOT_SUPPORTED`、`NEVER` 和 `NESTED`。通过示例代码和数据库插入测试,逐一展示了每种类型的运作方式。例如,`REQUIRED`表示如果当前存在事务则加入该事务,否则创建新事务;`SUPPORTS`表示如果当前存在事务则加入,否则以非事务方式执行;`MANDATORY`表示必须在现有事务中运行,否则抛出异常;
42 4
Spring事务传播机制(最全示例)
|
20天前
|
SQL 关系型数据库 MySQL
学成在线笔记+踩坑(3)——【内容模块】课程分类查询、课程增改删、课程计划增删改查,统一异常处理+JSR303校验
课程分类查询、课程新增、统一异常处理、统一封装结果类、JSR303校验、修改课程、查询课程计划、新增/修改课程计划
学成在线笔记+踩坑(3)——【内容模块】课程分类查询、课程增改删、课程计划增删改查,统一异常处理+JSR303校验
|
20天前
|
前端开发 应用服务中间件 API
|
5天前
|
Java Spring
Spring 事务传播机制是什么?
Spring 事务传播机制是什么?
14 4
|
6天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
45 5
|
28天前
|
存储 关系型数据库 MySQL
如何优化数据库查询?
如何优化数据库查询?
63 1
|
20天前
|
SQL 存储 数据库
MSSQL遍历数据库根据列值查询数据
【9月更文挑战第12天】在 SQL Server 中,可以通过游标或临时表遍历数据库并根据列值查询数据。示例展示了如何创建临时表存储数据库名,并通过循环遍历这些名称来执行特定查询。需替换 `YourTableName`、`YourColumnName` 和 `YourValue` 为实际值。此方法要求有足够权限访问各数据库。若无跨库权限,需分别执行查询。
|
2月前
|
SQL 数据库 Java
HQL vs SQL:谁将统治数据库查询的未来?揭秘Hibernate的神秘力量!
【8月更文挑战第31天】Hibernate查询语言(HQL)是一种面向对象的查询语言,它模仿了SQL的语法,但操作对象为持久化类及其属性,而非数据库表和列。HQL具有类型安全、易于维护等优点,支持面向对象的高级特性,内置大量函数,可灵活处理查询结果。下面通过示例对比HQL与SQL,展示HQL在实际应用中的优势。例如,HQL查询“从员工表中筛选年龄大于30岁的员工”只需简单地表示为 `FROM Employee e WHERE e.age &gt; 30`,而在SQL中则需明确指定表名和列名。此外,HQL在处理关联查询时也更为直观易懂。然而,对于某些复杂的数据库操作,SQL仍有其独特优势。
39 0
|
2月前
|
API Java 数据库连接
从平凡到卓越:Hibernate Criteria API 让你的数据库查询瞬间高大上,彻底告别复杂SQL!
【8月更文挑战第31天】构建复杂查询是数据库应用开发中的常见需求。Hibernate 的 Criteria API 以其强大和灵活的特点,允许开发者以面向对象的方式构建查询逻辑,同时具备 SQL 的表达力。本文将介绍 Criteria API 的基本用法并通过示例展示其实际应用。此 API 通过 API 构建查询条件而非直接编写查询语句,提高了代码的可读性和安全性。无论是简单的条件过滤还是复杂的分页和连接查询,Criteria API 均能胜任,有助于提升开发效率和应用的健壮性。
62 0
下一篇
无影云桌面