>  기사  >  Java  >  Java Hibernate 프레임워크의 캐싱 및 지연 로딩 메커니즘에 대한 간략한 분석

Java Hibernate 프레임워크의 캐싱 및 지연 로딩 메커니즘에 대한 간략한 분석

高洛峰
高洛峰원래의
2017-01-23 13:05:211259검색

최대 절전 모드 1단계 캐시와 2단계 캐시의 차이점
캐시는 애플리케이션과 물리적 데이터 소스 사이에 있으며, 그 기능은 애플리케이션이 물리적 데이터 소스에 액세스하는 빈도를 줄여 성능을 향상시키는 것입니다. 애플리케이션의 작동. 캐시에 있는 데이터는 물리적 데이터 소스에 있는 데이터의 복사본입니다. 애플리케이션은 런타임에 캐시에서 데이터를 읽고 쓰며, 특정 순간이나 이벤트에 캐시에 있는 데이터와 물리적 데이터 소스를 동기화합니다.
캐시 매체는 대개 메모리이므로 읽기 및 쓰기 속도가 매우 빠릅니다. 그러나 캐시에 저장된 데이터의 양이 매우 클 경우 하드 디스크도 캐시 매체로 사용됩니다. 캐시 구현에서는 저장 매체뿐만 아니라 캐시에 대한 동시 액세스 관리와 캐시된 데이터의 수명 주기도 고려해야 합니다.
Hibernate의 캐시에는 Session 캐시와 SessionFactory 캐시가 포함되어 있으며 내장 캐시와 외부 캐시의 두 가지 범주로 나눌 수 있습니다. 세션 캐시는 내장되어 있어 언로드할 수 없습니다. Hibernate의 1차 캐시라고도 합니다. SessionFactory의 내장 캐시와 Session의 캐시는 구현상 유사합니다. 전자는 SessionFactory 객체의 일부 컬렉션 속성에 포함된 데이터를 참조하고, 후자는 Session의 일부 컬렉션 속성에 포함된 데이터를 참조합니다. SessionFactory의 내장 캐시는 매핑 메타데이터와 사전 정의된 SQL 문을 저장하며, 사전 정의된 SQL 문은 Hibernate 초기화 단계 동안 매핑 메타데이터에서 파생됩니다. SessionFactory의 캐시는 읽기 전용이므로 애플리케이션은 캐시에 있는 매핑 메타데이터와 미리 정의된 SQL 문을 수정할 수 없으므로 SessionFactory는 내장된 캐시를 매핑 파일과 동기화할 필요가 없습니다. SessionFactory의 외부 캐시는 구성 가능한 플러그인입니다. 기본적으로 SessionFactory는 이 플러그인을 활성화하지 않습니다. 외부 캐시의 데이터는 데이터베이스 데이터의 복사본이며 외부 캐시의 매체는 메모리 또는 하드 디스크일 수 있습니다. SessionFactory의 외부 캐시는 Hibernate의 2차 캐시라고도 합니다.
Hibernate의 두 가지 캐시 레벨은 모두 지속성 계층에 위치하며 데이터베이스 데이터의 복사본을 저장합니다. 둘 사이의 차이점을 이해하려면 지속성 계층 캐시의 두 가지 특성인 캐시 범위와 캐시의 동시 액세스 정책에 대한 깊은 이해가 필요합니다.
지속성 계층의 캐시 범위
캐시의 범위에 따라 캐시의 수명 주기와 이에 액세스할 수 있는 사람이 결정됩니다. 캐시의 범위는 세 가지 범주로 나뉩니다.
 1 트랜잭션 범위: 현재 트랜잭션에서만 캐시에 접근할 수 있습니다. 캐시의 수명주기는 트랜잭션의 수명주기에 따라 달라집니다. 트랜잭션이 끝나면 캐시도 수명주기를 종료합니다. 이 범위에서 캐시 매체는 메모리입니다. 트랜잭션은 데이터베이스 트랜잭션일 수도 있고 애플리케이션 트랜잭션일 수도 있습니다. 각 트랜잭션에는 자체 캐시가 있습니다. 캐시의 데이터는 일반적으로 상호 연관된 개체의 형태입니다.
 2 프로세스 범위: 프로세스 내의 모든 트랜잭션이 캐시를 공유합니다. 이러한 트랜잭션은 캐시에 동시에 액세스할 수 있으므로 캐시에 필요한 트랜잭션 격리 메커니즘을 채택해야 합니다. 캐시의 수명주기는 프로세스의 수명주기에 따라 달라집니다. 프로세스가 끝나면 캐시도 수명주기를 종료합니다. 프로세스 전체 캐시는 많은 양의 데이터를 저장할 수 있으므로 저장 매체는 메모리나 하드 디스크가 될 수 있습니다. 캐시에 있는 데이터는 관련 객체의 형태일 수도 있고 객체의 느슨한 데이터 형태일 수도 있습니다. 느슨한 객체 데이터 형식은 객체의 직렬화된 데이터 형식과 다소 유사하지만 객체를 느슨한 데이터로 분해하는 알고리즘은 객체 직렬화에 필요한 알고리즘보다 빠릅니다.
 3 클러스터 범위: 클러스터 환경에서는 하나의 머신 또는 여러 머신의 프로세스가 캐시를 공유합니다. 캐시에 있는 데이터는 클러스터 환경의 각 프로세스 노드에 복사되며, 캐시에 있는 데이터의 일관성을 보장하기 위해 프로세스 간 원격 통신이 사용됩니다.
대부분의 애플리케이션에서는 데이터베이스 데이터에 직접 액세스하는 것보다 액세스 속도가 반드시 훨씬 빠르지는 않기 때문에 클러스터 전체 캐시를 사용할지 여부를 신중하게 고려해야 합니다.
지속성 레이어는 여러 범위의 캐시를 제공할 수 있습니다. 해당 데이터가 트랜잭션 범위 캐시에 없으면 프로세스 범위 또는 클러스터 범위 캐시에서도 쿼리할 수 있습니다. 그래도 없으면 데이터베이스에서만 쿼리할 수 있습니다. 트랜잭션 수준 캐시는 지속성 계층의 첫 번째 수준 캐시이며 일반적으로 필수입니다. 프로세스 수준 또는 클러스터 수준 캐시는 지속성 계층의 두 번째 수준 캐시이며 일반적으로 선택 사항입니다.
지속성 계층 캐시에 대한 동시 액세스 전략
여러 동시 트랜잭션이 지속성 계층에 캐시된 동일한 데이터에 동시에 액세스하는 경우 동시성 문제가 발생하므로 필요한 트랜잭션 격리 조치를 취해야 합니다.
프로세스 전체 또는 클러스터 전체 캐시, 즉 2차 캐시에서 동시성 문제가 발생할 수 있습니다. 따라서 다음과 같은 네 가지 유형의 동시 액세스 전략을 설정할 수 있으며, 각 전략은 트랜잭션 격리 수준에 해당합니다.
트랜잭션: 관리되는 환경에만 적용 가능합니다. 반복 가능한 읽기 트랜잭션 격리 수준을 제공합니다. 자주 읽지만 거의 수정되지 않는 데이터의 경우 이 격리 유형을 사용하면 더티 읽기, 반복 불가능 읽기 등의 동시성 문제를 방지할 수 있습니다.
읽기-쓰기: 읽기 커밋된 트랜잭션 격리 수준을 제공합니다. 비클러스터형 환경에만 적용 가능합니다. 자주 읽히지만 거의 수정되지 않는 데이터의 경우 더티 읽기(dirty read)와 같은 동시성 문제를 방지할 수 있으므로 이 격리 유형을 사용할 수 있습니다.
비엄격한 읽기-쓰기: 캐시와 데이터베이스의 데이터 일관성이 보장되지 않습니다. 두 트랜잭션이 동시에 캐시에 있는 동일한 데이터에 액세스할 가능성이 있는 경우 더티 읽기를 방지하려면 해당 데이터에 대해 짧은 데이터 만료 시간을 구성해야 합니다. 이 동시 액세스 전략은 거의 수정되지 않고 가끔씩 더티 읽기를 허용하는 데이터에 사용할 수 있습니다. 읽기 전용: 이 동시 액세스 전략은 참조 데이터와 같이 수정되지 않는 데이터에 사용할 수 있습니다.
트랜잭션 동시 액세스 전략은 트랜잭션 격리 수준이 가장 높고 읽기 전용 격리 수준이 가장 낮습니다. 트랜잭션 격리 수준이 높을수록 동시성 성능은 낮아집니다.
2차 캐시에 저장하기에 적합한 데이터는 무엇인가요?
1. 거의 수정되지 않는 데이터
2. 그다지 중요하지 않은 데이터, 간헐적으로 동시 데이터 허용
3. 동시에 접근되지 않는 데이터
4. 참조 데이터
데이터 두 번째 수준 캐시에 저장하는 데 적합하지 않습니까?
1. 자주 수정되는 데이터
2. 금융 데이터, 동시성은 절대 허용되지 않습니다
3. 다른 애플리케이션과 공유되는 데이터.
Hibernate의 2단계 캐시
앞서 언급했듯이 Hibernate는 2단계 캐시를 제공하는데, 첫 번째 수준은 Session 캐시입니다. Session 개체의 수명 주기는 일반적으로 데이터베이스 트랜잭션이나 응용 프로그램 트랜잭션에 해당하므로 해당 캐시는 트랜잭션 범위 캐시입니다. 첫 번째 수준 캐싱이 필요하지만 허용되지 않으며 실제로 제거할 수 없습니다. 첫 번째 수준 캐시에서 영구 클래스의 각 인스턴스에는 고유한 OID가 있습니다.
두 번째 수준 캐시는 SessionFactory에서 관리하는 플러그형 캐시 플러그인입니다. SessionFactory 객체의 수명주기는 애플리케이션의 전체 프로세스에 해당하므로 두 번째 수준 캐시는 프로세스 전체 또는 클러스터 전체 캐시입니다. 이 캐시에 저장된 개체의 느슨한 데이터입니다. 두 번째 수준 개체에는 동시성 문제가 발생할 가능성이 있으며 캐시된 데이터에 대한 트랜잭션 격리 수준을 제공하는 적절한 동시 액세스 전략이 필요합니다. 캐시 어댑터는 특정 캐시 구현 소프트웨어를 Hibernate와 통합하는 데 사용됩니다. 두 번째 수준 캐싱은 선택 사항이며 클래스별 또는 컬렉션별 세분성으로 구성할 수 있습니다.
Hibernate의 2차 캐시 전략의 일반적인 과정은 다음과 같습니다.
1) 조건을 질의할 때 항상 select * from table_name where ....(모든 필드 선택)과 같은 SQL 문을 발행하여 질의합니다. 데이터베이스를 구축하고 동시에 데이터 개체를 가져옵니다.
 2) 획득한 모든 데이터 객체를 ID에 따라 2차 캐시에 넣습니다.
 3) Hibernate는 ID를 기준으로 데이터 객체에 접근할 때 먼저 Session 1차 캐시에서 검색하고, 2차 캐시가 구성되어 있으면 2차 캐시에서 확인한다. -레벨 캐시; 찾을 수 없으면 데이터베이스를 다시 쿼리하고 ID에 따라 결과를 캐시에 넣습니다.
 4) 데이터 삭제, 업데이트, 추가 시 캐시도 동시에 업데이트됩니다.
Hibernate의 2차 캐싱 전략은 ID 쿼리에 대한 캐싱 전략이며 조건 쿼리에는 영향을 미치지 않습니다. 이를 위해 Hibernate는 조건부 쿼리에 대한 쿼리 캐싱을 제공합니다.
Hibernate의 Query 캐싱 전략 과정은 다음과 같습니다.
1) Hibernate는 먼저 이 정보를 기반으로 Query Key를 형성합니다. Query Key에는 조건부 쿼리에서 요청하는 일반 정보인 SQL, SQL에서 필요한 매개변수, 레코드 범위(시작 위치) rowStart, 최대 레코드 수(maxRows) 등
 2) Hibernate는 이 Query Key를 기반으로 해당 결과 목록을 Query 캐시에서 검색합니다. 존재하는 경우 결과 목록을 반환하고 존재하지 않는 경우 데이터베이스를 쿼리하여 결과 목록을 얻은 다음 쿼리 키에 따라 전체 결과 목록을 쿼리 캐시에 넣습니다.
 3) 쿼리 키의 SQL에는 일부 테이블 이름이 포함되어 있습니다. 이러한 테이블의 데이터가 수정, 삭제, 추가되는 경우 이러한 관련 쿼리 키가 캐시에서 지워집니다.

Hibernate 지연 로딩 메커니즘
지연 로딩:

지연 로딩 메커니즘은 불필요한 성능 오버헤드를 피하기 위해 제안되었습니다. 소위 지연 로딩은 데이터가 실제로 필요할 때를 의미합니다. 그런 다음에만 데이터 로딩 작업이 실제로 수행됩니다. Hibernate는 엔터티 객체의 지연 로딩과 컬렉션의 지연 로딩도 제공합니다. 아래에서는 이러한 유형의 지연 로딩에 대해 각각 자세히 소개합니다.

A. 엔터티 개체의 지연 로딩:

엔터티 개체에 지연 로딩을 사용하려면 아래와 같이 엔터티의 매핑 구성 파일에 해당 구성을 만들어야 합니다.

<hibernate-mapping>
 
<class name=”com.neusoft.entity.User” table=”user” lazy=”true”>
 
  ……
 
</class>
 
</hibernate-mapping>

클래스의 지연 속성을 true로 설정하여 엔터티의 지연 로딩 기능을 활성화합니다. 다음 코드를 실행하면:

User user=(User)session.load(User.class,”1”);

(1)

System.out.println(user.getName());

(2)

当运行到(1)处时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到(2)处时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id='1';来查询数据,并构造目标对象,并且将它赋值到CGLIB$CALBACK_0.target属性中。

    这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。

B、        集合类型的延迟加载:

在Hibernate的延迟加载机制中,针对集合类型的应用,意义是最为重大的,因为这有可能使性能得到大幅度的提高,为此Hibernate进行了大量的努力,其中包括对JDK Collection的独立实现,我们在一对多关联中,定义的用来容纳关联对象的Set集合,并不是java.util.Set类型或其子类型,而是net.sf.hibernate.collection.Set类型,通过使用自定义集合类的实现,Hibernate实现了集合类型的延迟加载。为了对集合类型使用延迟加载,我们必须如下配置我们的实体类的关于关联的部分:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>

通过将ace372f96ca3ec664acb3aaa2421b04c元素的lazy属性设置为true来开启集合类型的延迟加载特性。我们看下面的代码:

User user=(User)session.load(User.class,”1”);
 
Collection addset=user.getAddresses();

  (1)

Iterator it=addset.iterator();

 (2)

while(it.hasNext()){
 
Address address=(Address)it.next();
 
System.out.println(address.getAddress());
 
}

当程序执行到(1)处时,这时并不会发起对关联数据的查询来加载关联数据,只有运行到(2)处时,真正的数据读取操作才会开始,这时Hibernate会根据缓存中符合条件的数据索引,来查找符合条件的实体对象。

这里我们引入了一个全新的概念——数据索引,下面我们首先将接一下什么是数据索引。在Hibernate中对集合类型进行缓存时,是分两部分进行缓存的,首先缓存集合中所有实体的id列表,然后缓存实体对象,这些实体对象的id列表,就是所谓的数据索引。当查找数据索引时,如果没有找到对应的数据索引,这时就会一条select SQL的执行,获得符合条件的数据,并构造实体对象集合和数据索引,然后返回实体对象的集合,并且将实体对象和数据索引纳入Hibernate的缓存之中。另一方面,如果找到对应的数据索引,则从数据索引中取出id列表,然后根据id在缓存中查找对应的实体,如果找到就从缓存中返回,如果没有找到,在发起select SQL查询。在这里我们看出了另外一个问题,这个问题可能会对性能产生影响,这就是集合类型的缓存策略。如果我们如下配置集合类型:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<cache usage=”read-only”/>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>

这里我们应用了6b8f159f2058e6fec96445d54a7ff0ae配置,如果采用这种策略来配置集合类型,Hibernate将只会对数据索引进行缓存,而不会对集合中的实体对象进行缓存。如上配置我们运行下面的代码:

User user=(User)session.load(User.class,”1”);
 
Collection addset=user.getAddresses();  
 
Iterator it=addset.iterator();       
 
while(it.hasNext()){
 
Address address=(Address)it.next();
 
System.out.println(address.getAddress());
 
}
 
System.out.println(“Second query……”);
 
User user2=(User)session.load(User.class,”1”);
 
Collection it2=user2.getAddresses();
 
while(it2.hasNext()){
 
Address address2=(Address)it2.next();
 
System.out.println(address2.getAddress());
 
}

   

运行这段代码,会得到类似下面的输出:

Select * from user where id=&#39;1&#39;;
 
Select * from address where user_id=&#39;1&#39;;
 
Tianjin
 
Dalian
 
Second query……
 
Select * from address where id=&#39;1&#39;;
 
Select * from address where id=&#39;2&#39;;
 
Tianjin
 
Dalian

我们看到,当第二次执行查询时,执行了两条对address表的查询操作,为什么会这样?这是因为当第一次加载实体后,根据集合类型缓存策略的配置,只对集合数据索引进行了缓存,而并没有对集合中的实体对象进行缓存,所以在第二次再次加载实体时,Hibernate找到了对应实体的数据索引,但是根据数据索引,却无法在缓存中找到对应的实体,所以Hibernate根据找到的数据索引发起了两条select SQL的查询操作,这里造成了对性能的浪费,怎样才能避免这种情况呢?我们必须对集合类型中的实体也指定缓存策略,所以我们要如下对集合类型进行配置:

<hibernate-mapping>
 
  <class name=”com.neusoft.entity.User” table=”user”>
 
…..
 
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
 
<cache usage=”read-write”/>
 
<key column=”user_id”/>
 
<one-to-many class=”com.neusoft.entity.Arrderss”/>
 
</set>
 
  </class>
 
</hibernate-mapping>

   

此时Hibernate会对集合类型中的实体也进行缓存,如果根据这个配置再次运行上面的代码,将会得到类似如下的输出:

Select * from user where id=&#39;1&#39;;
 
Select * from address where user_id=&#39;1&#39;;
 
Tianjin
 
Dalian
 
Second query……
 
Tianjin
 
Dalian

这时将不会再有根据数据索引进行查询的SQL语句,因为此时可以直接从缓存中获得集合类型中存放的实体对象。

C、       属性延迟加载:

   在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在前面我们讲大数据对象读取时,在User对象中有一个resume字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。在Hibernate2中,我们只有通过我们前面讲过的面性能的粒度细分,来分解User类,来解决这个问题(请参照那一节的论述),但是在Hibernate3中,我们可以通过属性延迟加载机制,来使我们获得只有当我们真正需要操作这个字段时,才去读取这个字段数据的能力,为此我们必须如下配置我们的实体类:

<hibernate-mapping>
 
<class name=”com.neusoft.entity.User” table=”user”>
 
……
 
<property name=”resume” type=”java.sql.Clob” column=”resume” lazy=”true”/>
 
  </class>
 
</hibernate-mapping>

通过对3fcb97bb666cd7884d4d3210fb47b5ef元素的lazy属性设置true来开启属性的延迟加载,在Hibernate3中为了实现属性的延迟加载,使用了类增强器来对实体类的Class文件进行强化处理,通过增强器的增强,将CGLIB的回调机制逻辑,加入实体类,这里我们可以看出属性的延迟加载,还是通过CGLIB来实现的。CGLIB是Apache的一个开源工程,这个类库可以操纵java类的字节码,根据字节码来动态构造符合要求的类对象。根据上面的配置我们运行下面的代码:

String sql=”from User user where user.name=&#39;zx&#39; ”;
 
Query query=session.createQuery(sql);

 (1)

List list=query.list();
 
for(int i=0;i<list.size();i++){
 
User user=(User)list.get(i);
 
System.out.println(user.getName());
 
System.out.println(user.getResume()); }

  (2)

当执行到(1)处时,会生成类似如下的SQL语句:

Select id,age,name from user where name=&#39;zx&#39;;

这时Hibernate会检索User实体中所有非延迟加载属性对应的字段数据,当执行到(2)处时,会生成类似如下的SQL语句:

Select resume from user where id=&#39;1&#39;;

这时会发起对resume字段数据真正的读取操作。

更多浅析Java的Hibernate框架中的缓存和延迟加载机制相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.