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


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


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

相关文章
|
10月前
|
人工智能 JSON 安全
Spring Boot实现无感刷新Token机制
本文深入解析在Spring Boot项目中实现JWT无感刷新Token的机制,涵盖双Token策略、Refresh Token安全性及具体示例代码,帮助开发者提升用户体验与系统安全性。
1087 4
|
7月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
1323 5
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
501 2
|
9月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
11月前
|
缓存 NoSQL 算法
Redis数据库的键值过期和删除机制
我们需要注意的是,虽然Redis提供了这么多高级的缓存机制,但在使用过程中,必须理解应用的特性,选择合适的缓存策略,才能最大化Redis的性能。因此,在设计和实施应用程序时,理解应用的数据访问模式,以及这些模式如何与Redis的缓存机制相互作用,尤为重要。
324 24
|
9月前
|
JSON 前端开发 Java
Spring MVC 核心组件与请求处理机制详解
本文解析了 Spring MVC 的核心组件及请求流程,核心组件包括 DispatcherServlet(中央调度)、HandlerMapping(URL 匹配处理器)、HandlerAdapter(执行处理器)、Handler(业务方法)、ViewResolver(视图解析),其中仅 Handler 需开发者实现。 详细描述了请求执行的 7 步流程:请求到达 DispatcherServlet 后,经映射器、适配器找到并执行处理器,再通过视图解析器渲染视图(前后端分离下视图解析可省略)。 介绍了拦截器的使用(实现 HandlerInterceptor 接口 + 配置类)及与过滤器的区别
904 0
|
9月前
|
SQL XML Java
配置Spring框架以连接SQL Server数据库
最后,需要集成Spring配置到应用中,这通常在 `main`方法或者Spring Boot的应用配置类中通过加载XML配置或使用注解来实现。
695 0
|
10月前
|
SQL Java 数据库
解决Java Spring Boot应用中MyBatis-Plus查询问题的策略。
保持技能更新是侦探的重要素质。定期回顾最佳实践和新技术。比如,定期查看MyBatis-Plus的更新和社区的最佳做法,这样才能不断提升查询效率和性能。
557 1
|
存储 缓存 Oracle
崖山数据库YashanDB的共享集群机制初探
YashanDB共享集群是崖山数据库系统的核心特性,支持单库多实例并发读写,确保强一致性与高可用性。基于Shared-Disk架构和Cohesive Memory技术,实现数据页协同访问及资源控制。其核心组件包括YCK、YCS和YFS,提供金融级RPO=0、RTO&lt;10秒的高可用能力。通过自研“七种武器”(如页内锁、去中心化事务管理等),优化性能并解决读写冲突。相比Oracle RAC,YashanDB在TPC-C测试中性能高出30%,适用于金融、电信等关键领域,推动国产化替代进程。
崖山数据库YashanDB的共享集群机制初探
|
XML Java Maven
Spring 手动实现Spring底层机制
Spring 第六节 手动实现Spring底层机制 万字详解!
561 32

热门文章

最新文章

下一篇
开通oss服务