Spring Data JPA想要学得好,缓存机制掌握好

Posted c.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Data JPA想要学得好,缓存机制掌握好相关的知识,希望对你有一定的参考价值。

文章目录

Spring Data JPA想要学得好,缓存机制掌握好

本文章主要对JPA进行简单的介绍,主要重点在于JPA的一级缓存机制,会带领大家浅读一下具体实现的Hibernate中的源码。

Hibernate、JPA与Spring Data JPA之间的关系

JPA是一套规范,内部是有接口和抽象类组成的。hibernate是一套成熟的ORM框架,而且Hibernate实现了JPA规范,所以也可以称hibernate为JPA的一种实现方式,我们使用JPA的API编程,意味着站在更高的角度上看待问题(面向接口编程)Spring Data JPA是Spring提供的一套对JPA操作更加高级的封装,是在JPA规范下的专门用来进行数据持久化的解决方案。以上就是对hibernate、JPA与Spring Data JPA三者之间的关系说明。
参考:详谈hibernate,jpa与spring data jpa三者之间的关系

所以虽然我们标题是《Spring Data JPA想要学得好,缓存机制掌握好》,但实际上这里我们在探讨的是具体实现——Hibernate的缓存

JPA的EntityManager接口与Hibernate的Session接口

首先EntityManagerSession都是接口。

然后Session是继承于EntityManager的。

所以可以理解为EntityManager是对JPA持久化上下文交互的抽象,而Session 接口是 Hibernate 向应用程序提供的操纵对数据库的最主要的接口, 它提供了基本的保存, 更新, 删除和加载Java 对象的方法。而Hibernate是JPA的具体实现,所以Session自然是继承于EntityManager

然后我们可以看EntityManager具体的实现类其实是Hibernate相关的类

参考: 比较JPA的EntityManager接口与Hibernate的Session接口

Hibernate的缓存

Hibernate缓存包括两大类:一级缓存和二级缓存。

一级缓存又称为"Session的缓存",它是内置的,不能被卸载(不能被卸载的意思就是这种缓存不具有可选性,必须有的功能,不可以取消session缓存)。由于Session对象的生命周期通常对应一个数据库事务或者一个应用事务,因此它的缓存是事务范围的缓存在第一级缓存中,持久化类的每个实例都具有唯一的OID。我们使用@Transactional 注解时,JpaTransactionManager会在开启事务前打开一个session,将事务绑定在这个session上,事务结束session关闭。

二级缓存又称为"SessionFactory的缓存",由于SessionFactory对象的生命周期和应用程序的整个过程对应,因此二级缓存是进程范围或者集群范围的缓存,有可能出现并发问题,因此需要采用适当的并发访问策略。第二级缓存是可选的,是一个可配置的插件,在默认情况下,SessionFactory不会启用这个插件,二级缓存应用场景局限性比较大,适用于数据要求的实时性和准确性不高、变动很少的情况。
原文链接:https://blog.csdn.net/qq_34485381/article/details/107117550

Hibernate的一级缓存(Session的缓存)

本篇博文主要探讨的只是Hibernate的一级缓存,因为他是内置且必须的,而二级缓存基本很少使用。

我们在使用Spring Data JPA的时候,其实都会使用到Hibernate的一级缓存,所以了解缓存机制就变得尤为重要,下面举一个例子来看看:

@Transactional
public User test() 
List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
    .address("test address").phoneNumber("1234").build()));
User user = User.builder()
    .name("kevin").contactInfos(contactInfos).build();
contactInfos.get(0).setUser(user);
userRepository.save(user);
userRepository.findById(1L);
userRepository.findById(1L);
return user;

上面这个事务方法中会执行几次select语句?是两次吗?不真正答案是0次。可能有人就很好奇,明明调用了两次findById方法,却没有执行select语句,这是为什么呢?其实就是因为使用到了我们的一级缓存。

当我们执行save方法的时候,其实是会把保存后的数据存入到缓存中的,如下图:

浅读缓存源码解密缓存过程

那可能有的小伙伴会好奇,那数据是缓存到了哪里呢?接下来我们就通过debug的方式,来看一下在保存还有后续的查询过程是怎样的。

我们先来说结论,缓存的数据是存在SessionImpl类中的StatefulPersistenceContext persistenceContext 属性上。

StatefulPersistenceContext类中,其中有一个HashMap<EntityKey, Object> entitiesByKey属性,这个entitiesByKey属性字段的作用就是用来缓存数据的,这个StatefulPersistenceContext类中还有其他一些 Java 集合, 这些 Java 集合构成了 Session 缓存。

这次debug我们主要关注这个entitiesByKey属性字段

我们来看当我们执行userRepository.save(user);这句的时候,最终会把存入的数据加入到缓存中,通过调用StatefulPersistenceContext类中的addEntity方法,把数据存入到entitiesByKey属性字段上。当然不只是user数据会存入,级联的contactInfo的数据也是会存入的,这里我们就不过多展示了。

然后当我们调用userRepository.findById(1L);方法的时候,会先从缓存中获取数据,缓存中没有才回去真正的执行数据库查询。这个时候会调用StatefulPersistenceContext类中的getEntity方法,根据key去获取我们真正缓存的对象。

第二次我们调用userRepository.findById(1L);方法的时候,跟第一次是一样的,也是会从缓存中获取数据。

JPA的源码中也是像我们开发时经常写日志的,使用logger.debug()什么的。所以我们可以将JPA的日志级别设置为DEBUG级别,这样我们就可以根据日志推测到JPA内部到底是怎么执行的了。

logging:
  level:
    org.hibernate.type.descriptor.sql.BasicBinder: trace
    org:
      springframework:
        orm:
          jpa: debug

然后我们可以看到控制台的执行结果如下:

2022-08-12 11:18:22.542 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.example.service.UserService.save2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-08-12 11:18:22.543 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(13193469<open>)] for JPA transaction
2022-08-12 11:18:22.555 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@2e243122]
2022-08-12 11:18:24.124 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(13193469<open>)] for JPA transaction
2022-08-12 11:18:24.124 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-08-12 11:18:24.164 TRACE 5228 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
2022-08-12 11:20:03.262  WARN 5228 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m7s864ms197µs100ns).
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-08-12 11:20:03.267 TRACE 5228 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-08-12 11:20:03.267 TRACE 5228 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-12 11:20:03.268 TRACE 5228 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
2022-08-12 11:21:00.872  WARN 5228 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=59s937ms26µs800ns).
2022-08-12 11:21:21.496 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(13193469<open>)] for JPA transaction
2022-08-12 11:21:21.496 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2022-08-12 11:23:34.497  WARN 5228 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=2m2s354ms216µs200ns).
2022-08-12 11:23:36.406 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(13193469<open>)] for JPA transaction
2022-08-12 11:23:36.406 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2022-08-12 11:25:11.600  WARN 5228 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=1m37s102ms703µs100ns).
2022-08-12 11:25:17.292 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-08-12 11:25:17.292 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(13193469<open>)]
2022-08-12 11:25:19.667 DEBUG 5228 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(13193469<open>)] after transaction

从执行日志可以看出整个流程如下:

  1. 首先是开启了事务: Creating new transaction with name [org.example.service.UserService.save2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
  2. 然后是创建了一个EntityManagerOpened new EntityManager [SessionImpl(13193469<open>)] for JPA transaction
  3. 在执行完一系列操作之后,在最后提交了事务: Committing JPA transaction on EntityManager [SessionImpl(13193469<open>)]
  4. 最后关闭了EntityManagerClosing JPA EntityManager [SessionImpl(13193469<open>)] after transaction

下面再举一个查询的例子,当我们在进行查询之后,其实也是会缓存到一级缓存里的。下面这个例子有两个事务,第一个事务是插入数据,而第二个事务是查询数据

@Transactional
public User save() 
List<ContactInfo> contactInfos = new ArrayList<>(List.of(ContactInfo.builder()
    .address("test address").phoneNumber("1234").build()));
User user = User.builder()
    .name("kevin").contactInfos(contactInfos).build();
return userRepository.save(user);

@Transactional(readOnly = true)
public void read()
userRepository.findById(1L);
User user = userRepository.findById(1L).get();

@Test
void testRead()
    userService.save();
    userService.read();

在第二个事务方法中,我们连续执行了两次findById操作。那最终控制台执行了几次select操作呢?是两次还是0次还是1次?

答案是1次。

一样的我们来看最终输出的执行结果,之后我们在debug看看

2022-08-12 14:02:54.122 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.example.service.UserService.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-08-12 14:02:54.122 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(449653268<open>)] for JPA transaction
2022-08-12 14:02:54.132 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7e305953]
2022-08-12 14:02:54.150 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(449653268<open>)] for JPA transaction
2022-08-12 14:02:54.150 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-08-12 14:02:54.194 TRACE 22904 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-08-12 14:02:54.502 TRACE 22904 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-08-12 14:02:54.502 TRACE 22904 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-12 14:02:54.503 TRACE 22904 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2022-08-12 14:02:54.512 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-08-12 14:02:54.512 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(449653268<open>)]
2022-08-12 14:02:54.557 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(449653268<open>)] after transaction
2022-08-12 14:02:54.558 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.example.service.UserService.read]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
2022-08-12 14:02:54.558 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1881845799<open>)] for JPA transaction
2022-08-12 14:02:54.564 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1dab2cf5]
2022-08-12 14:02:54.564 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1881845799<open>)] for JPA transaction
2022-08-12 14:02:54.564 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
Hibernate: 
    select
        user0_.id as id1_1_0_,
        user0_.name as name2_1_0_ 
    from
        user user0_ 
    where
        user0_.id=?
2022-08-12 14:02:54.577 TRACE 22904 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-08-12 14:02:54.593 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1881845799<open>)] for JPA transaction
2022-08-12 14:02:54.593 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2022-08-12 14:02:54.594 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-08-12 14:02:54.594 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1881845799<open>)]
2022-08-12 14:02:54.599 DEBUG 22904 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1881845799<open>)] after transaction

从日志来看确实就是查询了一次数据库。因为从日志上看我们创建的EntityManager,或者说我们创建的Session是在事务级别的,从Opened new EntityManager [SessionImpl(449653268<open>)] for JPA transaction 这一句可以看出来。所以我们的缓存当然也是在同一个事务下才是可见的。所以在第一个事务1中虽然save方法缓存了数据,但是在第二个事务中我们开启了一个新的session,所以我们并看不到事务1的缓存,所以去真实的查询了数据库,并缓存了下来,然后事务2的第二个findById就能从缓存中获取数据,并不需要去真实的查询数据库了。

让我们可以debug来看看,建议自己动手理解更加深刻。

在第一个findById执行过程中会把查询后的数据进行缓存,我们可以看到会走进StatefulPersistenceContext类中的addEntity方法

在第二个findById执行过程中发现缓存中有对应的数据就直接拿出来,并不需要再次查询数据库了。

Hibernate的一级缓存(Session的缓存)的作用

从上面的例子来看,我们可以知道一级缓存最大的作用其实就是缓存的作用:减少访问数据库的频率。因为我们不想在代码中每次调用find方法,都需要真实的去查询数据库,或者是我们在保存完数据之后,我们也不需要直接去查询数据库获取数据,而是通过缓存就能获取到数据,这样可以大大减少我们对数据库的访问次数。但这仅仅只是缓存的其中一个作用,Hibernate的一级缓存还有一个作用就是用来同步缓存对象和数据库中的记录。

Hibernate的一级缓存的作用有两个

  1. 减少访问数据库的频率。
  2. 保证缓存中的对象与数据库中的相关记录保持同步。

同步缓存中的对象

前面我们都是讲了Hibernate的一级缓存中的第一个作用,现在我们来研究下它的第二个作用:保证缓存中的对象与数据库中的相关记录保持同步。

我们来举一个例子,对上面的代码做一点小小的改造

@Transactional(readOnly = true)
public void read()
    userRepository.findById(1L);
    User user = userRepository.findById(1L).get();
    user.setName("test1111");

你们觉得上面的代码,最终会执行update语句吗?答案是不会出现update语句。我们可以直接看执行日志就可以看出来,但在看之前我们需要再设置一下我们日志级别,因为我们想关注更多的信息。

logging:
  level:
    org.hibernate.type.descriptor.sql.BasicBinder: trace
    org:
      springframework:
        orm:
          jpa: debug
      hibernate:
        event:
          internal: trace

接下来我们来看执行的结果

2022-08-12 15:44:47.961 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.example.service.UserService.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-08-12 15:44:47.962 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(443006127<open>)] for JPA transaction
2022-08-12 15:44:47.972 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@171b0d3]
2022-08-12 15:44:47.990 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(443006127<open>)] for JPA transaction
2022-08-12 15:44:47.991 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction
2022-08-12 15:44:48.000 TRACE 25408 --- [           main] o.hibernate.event.internal.EntityState   : Transient instance of: org.example.entity.User
2022-08-12 15:44:48.001 TRACE 25408 --- [           main] o.h.e.i.DefaultPersistEventListener      : Saving transient instance
2022-08-12 15:44:48.005 TRACE 25408 --- [           main] o.h.e.i.AbstractSaveEventListener        : Saving [org.example.entity.User#<null>]
2022-08-12 15:44:48.024 TRACE 25408 --- [           main] o.hibernate.event.internal.WrapVisitor   : Wrapped collection in role: org.example.entity.User.contactInfos
Hibernate: 
    insert 
    into
        user
        (name) 
    values
        (?)
2022-08-12 15:44:48.042 TRACE 25408 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [kevin]
2022-08-12 15:44:48.064 TRACE 25408 --- [           main] o.hibernate.event.internal.EntityState   : Transient instance of: org.example.entity.ContactInfo
2022-08-12 15:44:48.065 TRACE 25408 --- [           main] o.h.e.i.DefaultPersistEventListener      : Saving transient instance
2022-08-12 15:44:48.065 TRACE 25408 --- [           main] o.h.e.i.AbstractSaveEventListener        : Saving [org.example.entity.ContactInfo#<null>]
Hibernate: 
    insert 
    into
        contact_info
        (address, phone_number, uid) 
    values
        (?, ?, ?)
2022-08-12 15:44:48.065 TRACE 25408 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [test address]
2022-08-12 15:44:48.065 TRACE 25408 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [1234]
2022-08-12 15:44:48.066 TRACE 25408 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [null]
2022-08-12 15:44:48.075 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit
2022-08-12 15:44:48.075 DEBUG 25408 --- [           main] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(443006127<open>)]
2022-08-12 15:44:48.076 TRACE 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushing session
2022-08-12 15:44:48.076 DEBUG 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
2022-08-12 15:44:48.077 TRACE 25408 --- [           main] o.hibernate.event.internal.EntityState   : Persistent instance of: org.example.entity.ContactInfo
2022-08-12 15:44:48.077 TRACE 25408 --- [           main] o.h.e.i.DefaultPersistEventListener      : Ignoring persistent instance
2022-08-12 15:44:48.077 DEBUG 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
2022-08-12 15:44:48.078 TRACE 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushing entities and processing referenced collections
2022-08-12 15:44:48.081 TRACE 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing unreferenced collections
2022-08-12 15:44:48.082 TRACE 25408 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Scheduling collection removes/(re)creates/updates
2022-08-12 15:Spring data jpa删除元素时必须先查询出来吗

乐观锁定的重试机制(spring data + JPA)

spring-data-jpa+hibernate 各种缓存的配置演示

Spring Data Jpa缓存介绍

Spring Data JPA整合Redis缓存的配置

如何在 Spring Data JPA CRUDRepository 中添加缓存功能