Spring Data JPA 之 Session 的 open-in-view 对事务的影响

22 Session 的 open-in-view 对事务的影响

当我们使⽤ Spring Boot 加 JPA 的时候,会发现 Spring 帮我们新增了⼀个 spring.jpa.open-in-view 的配置,但是 Hibernate 本身却没有这个配置,不过其⼜是和 Hibernate 中的 Session 相关的,因此还是很重要的内容,所以这⼀讲我们来学习⼀下。

22.1 Session 是什么

我们通过⼀个类的关系图来回顾⼀下,看看 Session 在什么样的位置上。

在这里插入图片描述

其中,SessionImpl 是 Hibernate 实现 JPA 协议的 EntityManager 的⼀种实现⽅式,即实现类;⽽ Session 是 Hibernate 中的概念,完全符合 EntityManager 的接⼝协议,同时⼜完

成了 Hibernate 的特殊实现。

22.1.1 对 Session 的理解

在 Spring Data JPA 的框架中,我们可以狭隘地把 Session 理解为 EntityManager,因为其对于 JPA 的任何操作都是通过 EntityManager 的接⼝进⾏的,我们可以把 Session ⾥⾯的复杂逻辑当成⼀个⿊盒⼦。即使 SessionImpl 能够实现 Hibernate 的 Session 接⼝,但如果我们使⽤的是 Spring Data JPA,那么实现再多的接⼝也和我们没有任何关系。除⾮你不⽤ JPA 的接⼝,直接⽤ Hibernate 的 Navite 来实现,但是我不建议你这么做,因为过程太复杂了。那么 SessionImpl 对使⽤ JPA 体系的⼈来说,它主要解决了什么问题呢?

22.1.2 SessionImpl 解决了什么问题

  1. SessionImpl 是 EntityManager 的实现类,那么肯定实现了 JPA 协议规定的 EntityManager 的所有功能。⽐如我们上⼀课时讲解的 Persistence Context ⾥⾯ Entity 状态的所有操作,即管理了 Entity 的⽣命周期;EntityManager 暴露的 flushModel 的设置;EntityManager 对 Transaction 做了“是否开启新事务”“是否关闭当前事务”的逻辑。
  2. 实现 PersistenceContext 对象实例化的过程,使得 PersistenceContext ⽣命周期就是 Session 的⽣命周期。所以我们可以抽象地理解为,Sesession 是对⼀些数据库的操作,需要放在同⼀个上下⽂的集合中,就是我们常说的⼀级缓存。
  3. Session 有 open 的话,那么肯定有 close。open 的时候做了“是否开启事务”“是否获取连接”等逻辑;close 的时候做了“是否关闭事务”“释放连接”等动作;
  4. Session 的任何操作都离不开事务和连接,那么肯定⽤当前线程保存了这些资源。

当我们清楚了 SessionImpl、EntityManager 的这些基础概念之后,那么接着来看看 open-in-view 是什么,它都做了什么事情呢?

22.2 open-in-view 是做什么的

open-in-view 是 Spring Boot ⾃动加载 Spring Data JPA 提供的⼀个配置,全称为 spring.jpa.open-in-view=true,它只有 true 和 false 两个值,默认是 true。那么它到底有什么威⼒呢?

22.2.1 open-in-view 的作用

我们可以在 JpaBaseConfiguration 中找到关键源码,通过源码来看⼀下 open-in-view 都做了哪些事情,如下所示。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JpaProperties.class)
public abstract class JpaBaseConfiguration implements BeanFactoryAware {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass(WebMvcConfigurer.class)
    // 这个提供了⼀种⾃定义注册 OpenEntityManagerInViewInterceptor 或者 OpenEntityManagerInViewFilter 的可能
    // 同时我们可以看到在 Web 的 MVC 层打开 session 的两种⽅式,⼀种是 Interceptor,另外⼀种是 Filter;这两个类任选其⼀即可
    // 默认⽤的是 OpenEntityManagerInViewInterceptor.class;
    @ConditionalOnMissingBean({ OpenEntityManagerInViewInterceptor.class, OpenEntityManagerInViewFilter.class })
    @ConditionalOnMissingFilterBean(OpenEntityManagerInViewFilter.class)
    // 这⾥使⽤了 spring.jpa.open-in-view 的配置,只有为 true 的时候才会执⾏这个配置类,
    // 当什么都没配置的时候,默认就是 true,也就是默认此配置⽂件就会⾃动加载;我们可以设置成 false,关闭加载;
    @ConditionalOnProperty(prefix = "spring.jpa", name = "open-in-view", havingValue = "true", matchIfMissing = true)
    protected static class JpaWebConfiguration {
        private static final Log logger = LogFactory.getLog(JpaWebConfiguration.class);
        private final JpaProperties jpaProperties;
        protected JpaWebConfiguration(JpaProperties jpaProperties) {
            this.jpaProperties = jpaProperties;
        }

        // 关键逻辑在 OpenEntityManagerInViewInterceptor 类⾥⾯;加载 OpenEntityManagerInViewInterceptor ⽤来在 MVC 的拦截器⾥⾯打开 EntityManager
        // ⽽当我们没有配置 spring.jpa.open-in-view 的时候,看下⾯代码 spring 容器会打印 warn ⽇志警告我们,默认开启了 open-in-view
        @Bean
        public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
            if (this.jpaProperties.getOpenInView() == null) {
                logger.warn("spring.jpa.open-in-view is enabled by default. "
                            + "Therefore, database queries may be performed during view "
                            + "rendering. Explicitly configure spring.jpa.open-in-view to disable this warning");
            }
            return new OpenEntityManagerInViewInterceptor();
        }

        // 利⽤ WebMvcConfigurer 加载上⾯的 OpenEntityManagerInViewInterceptor 拦截器进⼊到 MVC ⾥⾯;
        @Bean
        public WebMvcConfigurer openEntityManagerInViewInterceptorConfigurer(
            OpenEntityManagerInViewInterceptor interceptor) {
            return new WebMvcConfigurer() {

                @Override
                public void addInterceptors(InterceptorRegistry registry) {
                    registry.addWebRequestInterceptor(interceptor);
                }
            };
        }
    }
    // .....其他不重要的代码省略
}

通过上⾯的源码我们可以看到,spring.jpa.open-in-view 的主要作⽤就是帮我们加载 OpenEntityManagerInViewInterceptor 这个类,那么我们再打开这个类的源码,看看它帮我们实现的主要功能是什么?

22.2.2 OpenEntityManagerInView-Interceptor 源码分析

通过源码我们可以发现,OpenEntityManagerInViewInterceptor 实现了 WebRequestInterceptor 的接⼝中的两个⽅法:

  1. public void preHandle(WebRequest request) ⽅法,⾥⾯实现了在每次的 Web MVC 求之前,通过 createEntityManager ⽅法创建 EntityManager 和 EntityManagerHolder 的逻辑;
  2. public void afterCompletion(WebRequest request, @Nullable Exception ex) ⽅法,⾥⾯实现了在每次 Web MVC 的请求结束之后,关闭 EntityManager 的逻辑。

我们如果继续看 createEntityManager ⽅法的实现,还会找到如下关键代码。

在这里插入图片描述

上图可以看到,我们通过 SessionFactoryImpl 中的 createEntityManager() ⽅法,创建了⼀个 EntityManager 的实现 Session;通过拦截器创建了 EntityManager 事务处理逻辑,默认是 Join 类型(即有事务存在会加⼊);⽽ builder.openSession() 逻辑就是 new SessionImpl(sessionFactory, this)。

所以这个时候可以知道,通过 open-in-view 配置的拦截器,会帮我们的每个请求都创建⼀个 SessionImpl 实例;⽽ SessionImpl ⾥⾯存储了整个 PersistenceContext 和各种事务连接状态,可以判断出来 Session 的实例对象⽐较⼤。并且,我们打开 spring.jap.open-in-view=true 会发现,如果⼀个请求处理的逻辑⽐较耗时,牵涉到的对象⽐较多,这个时候就⽐较考验我们对 jvm 的内存配置策略了,如果配置不好就会经常出现内存溢出的现象。因此当处理⽐较耗时的请求和批量处理请求的时候,需要考虑到这⼀点。

22.2.3 EntityManager 的开启时机及扩展场景

我们通过 IDEA 开发者⼯具,直接点击右键查 public Session createEntityManager() 此⽅法被使⽤到的地⽅即可,如下图所示。

在这里插入图片描述

其中,EntityManagerFactoryAccessor 是 OpenEntityManagerInViewInterceptor 的⽗类,从图上我们可以看得出来,Session 的创建(也可以说是 EntityManager 的创建)对我们有⽤的时机,⽬前就有三种。

  • 第⼀种:Web View Interceptor,通过 spring.jpa.open-in-view 控制。
  • 第⼆种:Web Filter,这种⽅式是 Spring 给我们提供的另外⼀种应⽤场景,⽐如有些耗时的、批量处理的请求,我们不想在请求的时候开启 Session,⽽是想在处理简单逻辑后,需要⽤到延迟加载机制的请求时 Open Session。因为开启 Session 后,我们写框架代码的时候可以利⽤ lazy 机制。⽽这个时候我们就可以考虑使⽤ OpenEntityManagerInViewFilter,配置请求 filter 的过滤机制,实现不同的请求以及不同 Open Session 的逻辑了。
  • 第三种:JPA Transaction,这种⽅式就是利⽤ JpaTransactionManager,实现在事务开启的时候打开 Session,在事务结束的时候关闭 Session。所以默认情况下,Session 的开启时机有两个:每个请求之前、新的事务开启之前;⽽Session 的关闭时机也是两个:每个请求结束之后、事务关闭之后。

此外,EntityManager(Session) 打开之后,资源存储在当前线程⾥⾯ (ThreadLoacal),所以⼀个 Session 中即使开启了多个事务,也不会创建多个 EntityManager 或者 Session。⽽事务在关闭之前,也会检查⼀下此 EntityManager / Session 是不是我这个事务创建的,如果是就关闭,如果不是就不关闭,不过其不会关闭在事务范围之外创建的 EntityManager / Session。

这个机制其实还给我们⼀些额外思考:我们是不是可以⾃由选择开启 / 关闭 Session 呢?不⼀定是 view / filter / 事务,任何多事务组合的代码模块都可以。只要我们知道什么时间开启,保证⼀定能 close 就没有问题。下⾯我们通过⽇志来看⼀下两种打开、关闭 EntityManager 的时机。

22.2.4 验证 EntityManager 创建和释放的日志

第⼀步:我们新建⼀个 UserController 的⽅法,⽤来模拟请求两段事务的情况,代码如下所示。

@PutMapping("/user")
public User updateUser() {
    User u1 = userRepository.findById(1L).orElseThrow(RuntimeException::new);
    User u2 = userRepository.findById(2L).orElseThrow(RuntimeException::new);

    //更新 u1,新开启⼀个事务
    u1.setName(UUID.randomUUID().toString());
    userRepository.save(u1);

    //更新 u2,新开启⼀个事务
    u2.setName(UUID.randomUUID().toString());
    userRepository.save(u2);

    return u1;
}

可以看到,⾥⾯调⽤了两个 save 操作,没有指定事务。但是我之前讲过,因为 userInfoRepository 的实现类 SimpleJpaRepository 的 save ⽅法上⾯有 @Transactional 注解,所以每个 userInfoRepository.save() ⽅法就会开启新的事务。我们利⽤这个机制在上⾯的 Controller ⾥⾯模拟了两个事务。

第⼆步:打开 open-in-view,同时修改⼀些⽇志级别,⽅便我们观察,配置如下述代码所示。

## 打开open-in-view
spring.jpa.open-in-view=true
## 修改⽇志级别
logging.level.org.springframework.orm.jpa.JpaTransactionManager=trace
logging.level.org.hibernate.internal=trace
logging.level.org.hibernate.engine.transaction.internal=trace

第三步:启动项⽬,发送如下请求。

###
PUT http://127.0.0.1:8080/user

通过日志可以看到,我们请求了之后就开启了 Session,然后在 Controller ⽅法执⾏的过程中开启了两段事务,每个事务结束之后都没有关闭 Session,⽽是等两个事务都结束之后,并且 Controller ⽅法执⾏完毕之后,才 Closing Session 的。中间过程只创建了⼀次 Session。

第四步:其他都不变的前提下,我们把 open-in-view 改成 false,如下⾯这⾏代码所示。

spring.jpa.open-in-view=false

通过⽇志可以看到,其中开启了两次事务,每个事务创建之后都会创建⼀个 Session,即开启了两个 Session,每个 Session 的 ID 是不⼀样的;在每个事务结束之后关闭了 Session,关闭了 EntityManager。

22.3 hibernate.connection.handling_mode 详解

通过之前讲解的类 AvailableSettings,可以找到如下三个关键配置。

// 指定获得db连接的⽅式,hibernate5.2 之后已经不推荐使⽤,改⽤ hibernate.connection.handling_mode 配置形式
String ACQUIRE_CONNECTIONS = "hibernate.connection.acquisition_mode";
// 释放连接的模式有哪些?hibernate5.2 之后也不推荐使⽤,改⽤ hibernate.connection.handling_mode 配置形式
String RELEASE_CONNECTIONS = "hibernate.connection.release_mode";
// 指定获取连接和释放连接的模式,hibernate5.2 之后新增的配置项,代替上⾯两个旧的配置
String CONNECTION_HANDLING = "hibernate.connection.handling_mode";

那么 hibernate.connection.handling_mode 对应的配置有哪些呢?Hibernate 5 提供了五种模式,我们详细看⼀下。

22.3.1 PhysicalConnectionHandling-Mode 的五种模式

在 Hibernate 5.2 ⾥⾯,hibernate.connection.handling_mode 这个 Key 对应的值在 PhysicalConnectionHandlingMode 枚举类⾥⾯有定义,核⼼代码如下所示。

public enum PhysicalConnectionHandlingMode {
    IMMEDIATE_ACQUISITION_AND_HOLD( IMMEDIATELY, ON_CLOSE ),
    DELAYED_ACQUISITION_AND_HOLD( AS_NEEDED, ON_CLOSE ),
    DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT( AS_NEEDED, AFTER_STATEMENT ),
    DELAYED_ACQUISITION_AND_RELEASE_BEFORE_TRANSACTION_COMPLETION( AS_NEEDED, BEFORE_TRANSACTION_COMPLETION ),
    DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION( AS_NEEDED, AFTER_TRANSACTION )
        ;
    private final ConnectionAcquisitionMode acquisitionMode;
    private final ConnectionReleaseMode releaseMode;
    PhysicalConnectionHandlingMode(
        ConnectionAcquisitionMode acquisitionMode,
        ConnectionReleaseMode releaseMode) {
        this.acquisitionMode = acquisitionMode;
        this.releaseMode = releaseMode;
    }
}

我们可以看到⼀共有五组值,也就是把原来的 ConnectionAcquisitionMode 和 ConnectionReleaseMode 分开配置的模式进⾏了组合配置管理,我们分别了解⼀下。

IMMEDIATE_ACQUISITION_AND_HOLD: ⽴即获取,⼀直保持连接到 Session 关闭。其可以代表如下⼏层含义:

  • Session ⼀旦打开就会获取连接;
  • Session 关闭的时候释放连接;
  • 如果 open-in-view=true 的时候,也就是说即使我们的请求⾥⾯没有做任何操作,或者有⼀些耗时操作,会导致数据库的连接释放不及时,从⽽导致 DB 连接不够⽤,如果请求频繁的话,会产⽣不必要的 DB 连接的上下⽂切换,浪费 CPU 性能;容易产⽣ DB 连接获取时间过⻓的现象,从⽽导致请求响应时间变⻓。

DELAYED_ACQUISITION_AND_HOLD: 延迟获取,⼀直保持连接到 Session 关闭。 其可以代表如下⼏层含义:

  • 表示需要的时候再获取连接,需要的时候是指进⾏ DB 操作的时候,这⾥主要是指事务打开的时候,就需要获取连接了(因为开启事务的时候要执⾏“AUTOCOMMIT=0”的操作,所以这⾥的按需就是指开启事务;我们也可以关闭事务开启的时候改变AUTOCOMMIT 的⾏为,那么这个时候的按需就是指执⾏ DB 操作的时候,不⼀定开启事务就会获得 DB 的连接);
  • 关闭连接的时机是 Session Colse 的时候;
  • ⼀个 Session ⾥⾯只有⼀个连接,⽽⼀个连接⾥⾯可以有多段事务;⽐较适合⼀个请求有多段事务的场景;
  • 这个配置解决了,当没有 DB 操作的时候,即没有事务的时候不会获取数据库连接的问题;从⽽可以减少不必要的 DB 连接切换;
  • 但是⼀旦⼀个 Session 在进⾏了 DB 操作之后,⼜做了⼀些耗时的操作才关闭,那么也会导致 DB 连接释放不及时,从⽽导致 DB 连接的利⽤率低、⾼并发的时候请求性能下降

DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT: 延迟获取,Statement 执⾏完释放。 其可以代表如下⼏层含义:

  • 表示等需要的时候再获取连接,不是 session ⼀打开就会获取连接;
  • 在每个 Statement 的 SQL 执⾏完就释放连接,⼀旦有事务每个 SQL 执⾏完释放满⾜不了业务逻辑,我们常⽤的事务模式就不⽣效了;
  • 这种⽅式适合没有事务的情景,⼯作中不常⻅,可能分布式事务中有场景需要。

DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION: 延迟获取,事务执⾏之后释放。 其可以代表如下⼏层含义:

  • 表示等需要的时候再获取连接,不是 Session ⼀打开就会获取连接;
  • 在事务执⾏完之后释放连接,同⼀个事务共享⼀个连接;
  • 这种情况下 open-in-view 的模式对 DB 连接的持有和事务⼀样了,⽐较适合⼀个请求⾥⾯事务模块不多请求的情况;
  • 如果事务都控制在 Service 层,这个配置就⾮常好⽤,其对 Connection 的利⽤率⽐较⾼,基本上可以做到不浪费;
  • 这个配置不适合⼀个 Session ⽣命周期⾥⾯有很多ᇿ⽴事务的业务模块,因为这样就会使⼀个请求⾥⾯产⽣⼤量没必要的获取连接、释放连接的过程。

DELAYED_ACQUISITION_AND_RELEASE_BEFORE_TRANSACTION_COMPLETION: 延迟获取,事务执⾏之前释放。 其可以代表如下⼏层含义:

  • 表示等需要的时候再获取连接,不是 Session ⼀打开就会获取连接;
  • 在事务执⾏完之前释放连接,这种不保险,也⽐较少⽤。

现在你知道了 handling_mode 的五种模式,那么通常会默认⽤哪⼀种呢?

22.3.2 默认模式及其修改

我们打开源码 HibernateJpaVendorAdapter 类⾥⾯可以看到如下加载⽅式。

在这里插入图片描述

可以看到是通过这段代码设置的默认值

jpaProperties.put(AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD);

那么,如何修改默认值呢?直接在 application.properties ⽂件⾥⾯做如下修改即可。

## 我们可以修改成按需获取连接,事务执⾏完之后释放连接
spring.jpa.properties.hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION

22.4 Session、EntityManager、Connection和Transaction 之间的关系

22.4.1 Connection 和 Transaction

  1. 事务是建⽴在 Connection 之上的,没有连接就没有事务。
  2. 以 MySQL InnoDB 为例,新开⼀个连接默认开启事务,默认每个 SQL 执⾏完之后⾃动提交事务。
  3. ⼀个连接⾥⾯可以有多次串⾏的事务段;⼀个事务只能属于⼀个 Connection。
  4. 事务与事务之间是相互隔离的,那么⾃然不同连接的不同事务也是隔离的。

22.4.2 EntityManager、Connection 和 Transaction

  1. EntityManager ⾥⾯有 DataSource,当 EntityManager ⾥⾯开启事务的时候,先判断当前线程⾥⾯是否有数据库连接,如果有直接⽤。
  2. 开启事务之前先开启连接;关闭事务,不⼀定关闭连接。
  3. 开启 EntityManager,不⼀定⽴⻢获得连接;获得连接,不⼀定⽴⻢开启事务。
  4. 关闭 EntityManager,⼀定关闭事务,释放连接;反之不然。

22.4.3 Session、EntityManager、Connection 和 Transaction

  1. Session 是 EntityManager 的⼦类,SessionImpl 是 Session 和 EntityManager 的实现类。那么⾃然 EntityManager 和 Connection、Transaction 的关系同样适⽤ Session、EntityManager、Connection 和 Transaction 的关系。
  2. Session 的⽣命周期决定了 EntityManager 的⽣命周期。

22.4.4 Session 和 Transaction

  1. 在 Hibernate 的 JPA 实现⾥⾯,开启 Transaction 之前,必须要先开启 Session。
  2. 默认情况下,Session 的⽣命周期由 open-in-view 决定是请求之前开启,还是事务之前开启。
  3. 事务关闭了,Session 不⼀定关闭。
  4. Session 关闭了,事务⼀定关闭。

22.5 本章小结

以上就是这⼀讲的内容了。本讲中我们通过源码分析了 spring.jpa.open-in-view 是什么、⼲什么⽤的,以及它对事务、连接池、EntityManager 和 Session 的影响。

到这⼀讲你应该已经掌握了 Spring Data JPA 的核⼼的原理⾥⾯最重要的五个时机,即 Session(Entity Manager)的 Open 和 Close 时机、数据库连接的获取和释放时机、事务的开启和关闭时机以及我们上⼀讲介绍的 Persistence Context 的创建和销毁时机、Flush 的触发时机。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值