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

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

前言


关于Spring的事务,它是Spring Framework中极其重要的一块。前面用了大量的篇幅从应用层面、原理层面进行了比较全方位的一个讲解。但是因为它过于重要,所以本文继续做补充内容:Spring事务的同步机制(后面还有Spring事务的监听机制)


Spring事务同步机制?我估摸很多小伙伴从来没听过还有这么一说法,毕竟它在平时开发中你可能很少遇到(如果你没怎么考虑过系统性能和吞吐量的话)。


让我记录本文的源动力是忆起两年前自己在开发、调试过程中遇到这样一个诡异异常:


java.sql.SQLException: Connection has already been closed


但是,它不是必现的,重点:它不是必现的。

而一旦出现,任何涉及需要使用数据库连接的接口都有可能报这个错(已经影响正常work了),重启也解决不了问题的根本。


关于非必现问题,我曾经表达了一个观点:程序中的“软病(非必现问题)”是相对很难解决的,因为定位难度高,毕竟只要问题一旦定位了,从来不差解决方案


这个异常的字面意思非常简单:数据库连接池连接被关闭了。

可能大多数人(我当然也不例外)看到此异常都会fuck一句:what?我的连接都是交给Spring去管理了,自己从来不会手动close,怎么回事?难道Spring有bug?

敢于质疑“权威”一直以来都是件好事,但是有句话这么说:你对人家还不了解的情况下不要轻易说人家程序有bug。


可能大多数人对于Spring的事务,只知道怎么使用,比如加个注解啥的,但是底层原理并不清楚,因此定位此问题就会变得非常的困难了~

由于我之前有研究过Spring事务的同步机制这块,所以忆起这件事之后就迅速定位了问题所在:这和Spring事务的同步机制有关,并不是Spring事务的bug。

Spring事务极简介绍


关于Spring事务,我推荐小伙伴看看上面的【相关阅读】,能让你对Spring事务管理有个整体的掌握。但是由于过了有段时间了,此处做个非常简单的介绍:


Spring有声明式事务和编程式事务:


声明式事务只需要提供@Transactional的注解,然后事务的开启和提交/回滚、资源的清理就都由spring来管控,我们只需要关注业务代码即可;

编程式事务则需要使用spring提供的模板,如TransactionTemplate,或者直接使用底层的PlatformTransactionManager手动控制提交、回滚。


声明式事务的最大优点就是对代码的侵入性小,只需要在方法上加@Transactional的注解就可以实现事务;

编程式事务的最大优点就是事务的管控粒度较细,可以实现代码块级别的事务。


前提介绍


Spring把JDBC 的 Connection或者Hibernate的Session等访问数据库的链接(会话)都统一称为资源,显然我们知道Connection这种是线程不安全的,同一时刻是不能被多个线程共享的。


简单的说:同一时刻我们每个线程持有的Connection应该是独立的,且都是互不干扰和互不相同的


但是Spring管理的Service、Dao等他们都是无状态的单例Bean,怎么破?,如何保证单例Bean里面使用的Connection都能够独立呢?

Spring引入了一个类:事务同步管理类org.springframework.transaction.support.TransactionSynchronizationManager来解决这个问题。它的做法是内部使用了很多的ThreadLocal为不同的事务线程提供了独立的资源副本,并同时维护这些事务的配置属性和运行状态信息 (比如强大的事务嵌套、传播属性和这个强相关)。


这个同步管理器TransactionSynchronizationManager是掌管这一切的大脑,它管理的TransactionSynchronization是开放给调用者一个非常重要的扩展点,下面会有详细介绍~


TransactionSynchronizationManager 将 Dao、Service 类中影响线程安全的所有 “ 状态 ” 都统一抽取到该类中,并用 ThreadLocal 进行封装,这样一来, Dao (基于模板类或资源获取工具类创建的 Dao )和 Service (采用 Spring 事务管理机制)就不用自己来保存一些事务状态了,从而就变成了线程安全的单例对象了,优秀~


DataSourceUtils

这里有必要提前介绍Spring提供给我们的这个工具类。


有些场景比如我们使用MyBatis的时候,某些场景下,可能无法使用 Spring 提供的模板类来达到效果,而是需要直接操作源生API Connection。


那如何拿到这个链接Connection呢???(主意此处打大前提:必须保证和当前MaBatis线程使用的是同一个链接,这样才接受本事务控制嘛,否则就脱缰了~)


这个时候DataSourceUtils这个工具类就闪亮登场了,它提供了这个能力:


public abstract class DataSourceUtils {
  ...
  public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { ... }
  ...
  // 把definition和connection进行一些准备工作~
  public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException { ...}
  // Reset the given Connection after a transaction,
  // con.setTransactionIsolation(previousIsolationLevel);和con.setReadOnly(false);等等
  public static void resetConnectionAfterTransaction(Connection con, @Nullable Integer previousIsolationLevel) { ... }
  // 该JDBC Connection 是否是当前事务内的链接~
  public static boolean isConnectionTransactional(Connection con, @Nullable DataSource dataSource) { ... }
  // Statement 给他设置超时时间  不传timeout表示不超时
  public static void applyTransactionTimeout(Statement stmt, @Nullable DataSource dataSource) throws SQLException { ... }
  public static void applyTimeout(Statement stmt, @Nullable DataSource dataSource, int timeout) throws SQLException { ... }
  // 此处可能是归还给连接池,也有可能是close~(和连接池参数有关)
  public static void releaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) { ... }
  public static void doReleaseConnection(@Nullable Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
  // 这个是真close
  public static void doCloseConnection(Connection con, @Nullable DataSource dataSource) throws SQLException { ... }
  // 如果链接是代理,会拿到最底层的connection
  public static Connection getTargetConnection(Connection con) { ... }
}


getConnection()这个方法就是从TransactionSynchronizationManager里拿到一个现成的Connection(若没有现成的会用DataSource创建一个链接然后放进去~~~),所以这个工具类还是蛮好用的。


其实Spring不仅为JDBC提供了这个工具类,还为Hibernate、JPA、JDO等都提供了类似的工具类。

org.springframework.orm.hibernate.SessionFactoryUtils.getSession()

org.springframework.orm.jpa.EntityManagerFactoryUtils.getTransactionalEntityManager()

org.springframework.orm.jdo.PersistenceManagerFactoryUtils.getPersistenceManager()


问题场景一模拟


为了更好解释和说明,此处我模拟出这样的一个场景。

// 此处生路而关于DataSource、PlatformTransactionManager事务管理器等的配置
@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);
        // 做其余的事情  可能抛出异常
        System.out.println(1 / 0);
        return "service hello";
    }
}


如上Demo,这样子的因为有事务,所以最终这个插入都是不会成功的。(这个应该不用解释了吧,初级工程师应该必备的“技能”~)


    @Transactional
    @Override
    public Object hello(Integer id) {
        // 向数据库插入一条记录
        String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
        jdbcTemplate.update(sql);
        // 根据id去查询获取 总数(若查询到了肯定是count=1)
        String query = "select count(1) from user where id = " + id;
        Integer count = jdbcTemplate.queryForObject(query, Integer.class);
        log.info(count.toString());
        return "service hello";
    }


稍微改造一下,按照上面这么写,我相信想都不用想。count永远是返回1的~~这应该也是我们面向过程编程时候的经典案例:前面insert一条记录,下面是可以立马去查询出来的

下面我把它改造如下:


    @Transactional
    @Override
    public Object hello(Integer id) {
        // 向数据库插入一条记录
        String sql = "insert into user (id,name,age) values (" + id + ",'fsx',21)";
        jdbcTemplate.update(sql);
        // 生产环境一般会把些操作交给线程池,此处我只是模拟一下效果而已~
        new Thread(() -> {
            String query = "select count(1) from user where id = " + id;
            Integer count = jdbcTemplate.queryForObject(query, Integer.class);
            log.info(count.toString());
        }).start();
        // 把问题放大
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "service hello";
    }


经过这番改造,这样我们的count的值永远是0(是不是不符合预期了?)


小技巧:此处为了演示我使用sleep方式把问题放大了,否则可能有时候好使、有时候不好使 把问题放大是debug调试的一个基本技巧~


这个现象就是一个非常严重的问题,它可能会出现:刚插入的数据竟然查不到的诡异现象,这个在我们现阶段平时工作中也会较为频繁的遇到,若对这块不了解,它会对的业务逻辑、对mysql binlog的顺序有依赖的相关逻辑全都将会受到影响


解决方案


在互联网环境编程中,我们经常为了提高吞吐量、程序性能,会使用到异步的方式进行优化、消峰等等。因此连接池、线程池被得到了大量的应用。我们知道异步的提供的好处不言而喻,能够尽最大可能的提升硬件的利用率和能力,但它带来的缺点只有一个:提升系统的复杂性,很多时候需要深入的了解它才能运用自如,毕竟任何方案都是一把双刃剑,没有完美的~


比如一个业务处理中,发短信、发微信通知、记录操作日志等等这些非主干需求,我们一般都希望交给线程池去处理而不要干扰主要业务流程,所以我觉得现在多线程方式处理任务的概率已经越来越高了~

既然如此,我觉得出现上面我模拟的这种现象的可能性还是蛮高的,所以希望小伙伴们能引起重视一些。

定位到问题的原因是解决问题的关键,这里我先给出直接的解决方案,再做理论分析。

我们的诉求是:我们的异步线程的执行时,必须确保记录已经持久化到数据库了才ok。因此可以这么来做,一招制敌:


@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() {
                new Thread(() -> {
                    String query = "select count(1) from user where id = " + id;
                    Integer count = jdbcTemplate.queryForObject(query, Integer.class);
                    log.info(count.toString());
                }).start();
            }
        });
        // 把问题放大
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "service hello";
    }
}


我们使用TransactionSynchronizationManager注册一个TransactionSynchronization然后在afterCommit里执行我们的后续代码,这样就能100%确保我们的后续逻辑是在当前事务被commit后才执行的,完美的问题解决


它还有个方法afterCompletion()有类似的效果,至于它和afterCommit()有什么区别,我觉得稍微有点技术敏感性的小伙伴都能知晓的~

相关文章
|
8天前
|
SQL Java 关系型数据库
【SpringFramework】Spring事务
本文简述Spring中数据库及事务相关衍伸知识点。
36 9
|
16天前
|
Java 开发者 Spring
理解和解决Spring框架中的事务自调用问题
事务自调用问题是由于 Spring AOP 代理机制引起的,当方法在同一个类内部自调用时,事务注解将失效。通过使用代理对象调用、将事务逻辑分离到不同类中或使用 AspectJ 模式,可以有效解决这一问题。理解和解决这一问题,对于保证 Spring 应用中的事务管理正确性至关重要。掌握这些技巧,可以提高开发效率和代码的健壮性。
45 13
|
27天前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
85 14
|
1月前
|
缓存 安全 Java
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
51 2
Spring高手之路26——全方位掌握事务监听器
|
1月前
|
Java 关系型数据库 数据库
京东面试:聊聊Spring事务?Spring事务的10种失效场景?加入型传播和嵌套型传播有什么区别?
45岁老架构师尼恩分享了Spring事务的核心知识点,包括事务的两种管理方式(编程式和声明式)、@Transactional注解的五大属性(transactionManager、propagation、isolation、timeout、readOnly、rollbackFor)、事务的七种传播行为、事务隔离级别及其与数据库隔离级别的关系,以及Spring事务的10种失效场景。尼恩还强调了面试中如何给出高质量答案,推荐阅读《尼恩Java面试宝典PDF》以提升面试表现。更多技术资料可在公众号【技术自由圈】获取。
|
1月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
84 4
|
19天前
|
SQL 存储 Java
数据库———事务及bug的解决
事务的一些概念,并发事务以及并发事务引起的bug,脏读,不可重复读,幻读,数据库中的隔离级别,事务的简单应用
|
2月前
|
JavaScript Java 关系型数据库
Spring事务失效的8种场景
本文总结了使用 @Transactional 注解时事务可能失效的几种情况,包括数据库引擎不支持事务、类未被 Spring 管理、方法非 public、自身调用、未配置事务管理器、设置为不支持事务、异常未抛出及异常类型不匹配等。针对这些情况,文章提供了相应的解决建议,帮助开发者排查和解决事务不生效的问题。
|
22天前
|
存储 Oracle 关系型数据库
数据库传奇:MySQL创世之父的两千金My、Maria
《数据库传奇:MySQL创世之父的两千金My、Maria》介绍了MySQL的发展历程及其分支MariaDB。MySQL由Michael Widenius等人于1994年创建,现归Oracle所有,广泛应用于阿里巴巴、腾讯等企业。2009年,Widenius因担心Oracle收购影响MySQL的开源性,创建了MariaDB,提供额外功能和改进。维基百科、Google等已逐步替换为MariaDB,以确保更好的性能和社区支持。掌握MariaDB作为备用方案,对未来发展至关重要。
48 3
|
22天前
|
安全 关系型数据库 MySQL
MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!
《MySQL崩溃保险箱:探秘Redo/Undo日志确保数据库安全无忧!》介绍了MySQL中的三种关键日志:二进制日志(Binary Log)、重做日志(Redo Log)和撤销日志(Undo Log)。这些日志确保了数据库的ACID特性,即原子性、一致性、隔离性和持久性。Redo Log记录数据页的物理修改,保证事务持久性;Undo Log记录事务的逆操作,支持回滚和多版本并发控制(MVCC)。文章还详细对比了InnoDB和MyISAM存储引擎在事务支持、锁定机制、并发性等方面的差异,强调了InnoDB在高并发和事务处理中的优势。通过这些机制,MySQL能够在事务执行、崩溃和恢复过程中保持
54 3