ホームページ  >  記事  >  Java  >  Java の Hibernate フレームワークにおけるキャッシュと遅延ロードのメカニズムの簡単な分析

Java の Hibernate フレームワークにおけるキャッシュと遅延ロードのメカニズムの簡単な分析

高洛峰
高洛峰オリジナル
2017-01-23 13:05:211258ブラウズ

休止状態の第 1 レベル キャッシュと第 2 レベル キャッシュの違い
キャッシュはアプリケーションと物理データ ソースの間にあり、その機能はアプリケーションによる物理データ ソースへのアクセスの頻度を減らし、それによってアプリケーションの動作パフォーマンスを向上させることです。 。キャッシュ内のデータは、物理データ ソース内のデータのコピーであり、アプリケーションは実行時にキャッシュからデータを読み書きし、特定の瞬間またはイベントでキャッシュ内のデータと物理データ ソースを同期します。
キャッシュ媒体は通常メモリなので、読み取りと書き込みの速度が非常に高速です。ただし、キャッシュに保存されているデータの量が非常に多い場合は、ハード ディスクがキャッシュ メディアとしても使用されます。キャッシュの実装では、記憶媒体を考慮するだけでなく、キャッシュへの同時アクセスの管理やキャッシュされたデータのライフサイクルも考慮する必要があります。
Hibernate のキャッシュには、Session キャッシュと SessionFactory キャッシュが含まれており、SessionFactory キャッシュは、組み込みキャッシュと外部キャッシュの 2 つのカテゴリに分類できます。セッション キャッシュは組み込みであり、Hibernate の 1 次キャッシュとも呼ばれます。 SessionFactory の組み込みキャッシュと Session のキャッシュは実装において類似しています。前者は SessionFactory オブジェクトの一部のコレクション属性に含まれるデータを参照し、後者は Session の一部のコレクション属性に含まれるデータを参照します。 SessionFactory の組み込みキャッシュには、マッピング メタデータと事前定義された SQL ステートメントが格納されます。マッピング メタデータはマッピング ファイル内のデータのコピーであり、事前定義された SQL ステートメントは Hibernate の初期化フェーズ中にマッピング メタデータから派生します。 SessionFactory のキャッシュは読み取り専用であり、アプリケーションはキャッシュ内のマッピング メタデータと事前定義された SQL ステートメントを変更できないため、SessionFactory は組み込みキャッシュをマッピング ファイルと同期する必要はありません。 SessionFactory の外部キャッシュは構成可能なプラグインです。デフォルトでは、SessionFactory はこのプラグインを有効にしません。外部キャッシュ内のデータはデータベース データのコピーであり、外部キャッシュの媒体はメモリまたはハード ディスクです。 SessionFactory の外部キャッシュは、Hibernate の 2 次キャッシュとも呼ばれます。
Hibernate の 2 つのレベルのキャッシュはどちらも永続層にあり、データベース データのコピーを保存します。 2 つの違いを理解するには、永続層キャッシュの 2 つの特性、つまりキャッシュの範囲とキャッシュの同時アクセス ポリシーを深く理解する必要があります。
永続層におけるキャッシュのスコープ
キャッシュのスコープは、キャッシュのライフサイクルと誰がキャッシュにアクセスできるかを決定します。キャッシュの範囲は 3 つのカテゴリに分類されます。
1 トランザクションスコープ: キャッシュには現在のトランザクションのみがアクセスできます。キャッシュのライフ サイクルは、トランザクションのライフ サイクルによって異なります。トランザクションが終了すると、キャッシュのライフ サイクルも終了します。この範囲では、キャッシュ媒体はメモリです。トランザクションはデータベース トランザクションまたはアプリケーション トランザクションです。各トランザクションには独自のキャッシュがあり、通常、キャッシュ内のデータは相互に関連するオブジェクトの形式になります。
2 プロセススコープ: キャッシュはプロセス内のすべてのトランザクションによって共有されます。これらのトランザクションは同時にキャッシュにアクセスする可能性があるため、必要なトランザクション分離メカニズムをキャッシュに採用する必要があります。キャッシュのライフ サイクルは、プロセスのライフ サイクルによって異なります。プロセスが終了すると、キャッシュもライフ サイクルを終了します。プロセス全体のキャッシュには大量のデータが保存される場合があるため、記憶媒体にはメモリまたはハードディスクを使用できます。キャッシュ内のデータは、関連オブジェクトまたはオブジェクトのルース データの形式にすることができます。ルース オブジェクトのデータ形式は、オブジェクトのシリアル化されたデータ形式にある程度似ていますが、オブジェクトをルース データに分解するアルゴリズムは、オブジェクトのシリアル化に必要なアルゴリズムよりも高速です。
3 クラスターの範囲: クラスター環境では、キャッシュは 1 台または複数のマシン上のプロセスによって共有されます。キャッシュ内のデータはクラスター環境の各プロセス ノードにコピーされ、キャッシュ内のデータの一貫性を確保するためにプロセス間でリモート通信が使用されます。通常、キャッシュ内のデータはオブジェクトのルース データの形式になります。ほとんどのアプリケーションでは、アクセス速度がデータベース データに直接アクセスするよりも必ずしも高速であるとは限らないため、クラスター全体のキャッシュを使用するかどうかを慎重に検討する必要があります。
永続層は複数の範囲のキャッシュを提供できます。対応するデータがトランザクション スコープのキャッシュで見つからない場合は、プロセス スコープまたはクラスター スコープのキャッシュでクエリを実行することもできます。それでも見つからない場合は、データベースでのみクエリを実行できます。トランザクション全体のキャッシュは永続層の第 1 レベルのキャッシュであり、通常は必須です。プロセス全体またはクラスター全体のキャッシュは永続層の第 2 レベルのキャッシュであり、通常はオプションです。
永続層のキャッシュの同時アクセス戦略
複数の同時トランザクションが永続層にキャッシュされた同じデータに同時にアクセスすると、同時実行の問題が発生するため、必要なトランザクション分離対策を講じる必要があります。
同時実行性の問題は、プロセス全体またはクラスター全体のキャッシュ、つまり 2 次キャッシュで発生します。したがって、トランザクション分離レベルに応じて、以下の 4 種類の同時アクセス戦略を設定できます。
トランザクション: 管理された環境にのみ適用されます。これは、Repeatable Read トランザクション分離レベルを提供します。頻繁に読み取られるがほとんど変更されないデータの場合は、ダーティ リードや反復不可能な読み取りなどの同時実行性の問題を防ぐことができるため、この分離タイプを使用できます。
読み取り/書き込み: コミットされた読み取りトランザクション分離レベルを提供します。非クラスター環境にのみ適用されます。頻繁に読み取られるがほとんど変更されないデータの場合は、ダーティ リードなどの同時実行性の問題を防ぐことができるため、この分離タイプを使用できます。
非厳密な読み書き型: キャッシュとデータベース内のデータの一貫性は保証されません。 2 つのトランザクションがキャッシュ内の同じデータに同時にアクセスする可能性がある場合は、ダーティ リードを回避するためにデータの有効期限を短く設定する必要があります。この同時アクセス戦略は、めったに変更されず、時折ダーティ リードが許可されるデータに使用できます。 読み取り専用: この同時アクセス戦略は、参照データなど、決して変更されないデータに使用できます。
トランザクション同時アクセス戦略はトランザクション分離レベルが最も高く、読み取り専用は分離レベルが最も低くなります。トランザクション分離レベルが高くなるほど、同時実行パフォーマンスは低くなります。
2次キャッシュに保存するのに適しているのはどのようなデータですか?
1. めったに変更されないデータ
2. あまり重要ではないデータ、時々同時データは許可されます
3. 同時にアクセスされないデータ
4. 参照データ
第 2 レベルの保存に適さないデータキャッシュ?
1. 頻繁に変更されるデータ
2. 財務データ、同時実行は絶対に許可されません
3. 他のアプリケーションと共有されるデータ。
Hibernate の 2 レベル キャッシュ
前述したように、Hibernate は 2 レベルのキャッシュを提供し、最初のレベルはセッション キャッシュです。通常、Session オブジェクトのライフサイクルはデータベース トランザクションまたはアプリケーション トランザクションに対応するため、そのキャッシュはトランザクション スコープのキャッシュです。第 1 レベルのキャッシュは必須ですが、許可されておらず、実際には削除できません。第 1 レベルのキャッシュでは、永続クラスの各インスタンスは一意の OID を持ちます。
2次キャッシュはプラグイン可能なキャッシュプラグインで、SessionFactoryによって管理されます。 SessionFactory オブジェクトのライフサイクルはアプリケーションのプロセス全体に対応するため、2 次キャッシュはプロセス全体またはクラスター全体のキャッシュになります。このキャッシュに保存されているオブジェクトの緩いデータ。第 2 レベルのオブジェクトには同時実行性の問題が発生する可能性があり、キャッシュされたデータにトランザクション分離レベルを提供する適切な同時アクセス戦略が必要です。キャッシュ アダプターは、特定のキャッシュ実装ソフトウェアを Hibernate と統合するために使用されます。第 2 レベルのキャッシュはオプションであり、クラスごとまたはコレクションごとの粒度で構成できます。
Hibernate の 2 次キャッシュ戦略の一般的なプロセスは次のとおりです:
1) 条件付きクエリを実行するときは、常に select * from table_name where .... (すべてのフィールドを選択) などの SQL ステートメントを発行してデータベースにクエリを実行し、すべてのデータオブジェクトを一度に。
2) 取得したすべてのデータオブジェクトをIDに従って2次キャッシュに置きます。
3) Hibernate が ID に基づいてデータ オブジェクトにアクセスするとき、最初にセッションの 1 レベル キャッシュからチェックします。見つからない場合は、2 レベル キャッシュが設定されている場合は、2 レベル キャッシュからチェックします。キャッシュが見つからない場合は、データベースに再度クエリを実行し、その結果を ID に従ってキャッシュに格納します。
4) データの削除、更新、追加を行うと、同時にキャッシュも更新されます。
Hibernate の 2 次キャッシュ戦略は ID クエリに対するキャッシュ戦略ですが、条件付きクエリには影響しません。この目的のために、Hibernate は条件付きクエリのクエリ キャッシュを提供します。
Hibernate のクエリ キャッシュ戦略のプロセスは次のとおりです:
1) Hibernate はまず、この情報に基づいてクエリ キーを形成します。クエリ キーには、条件付きクエリによって要求される一般情報が含まれます: SQL、SQL に必要なパラメータ、レコード範囲 (開始)。位置 rowStart、最大レコード番号 maxRows) など。
2) Hibernate は、このクエリ キーに基づいて、クエリ キャッシュで対応する結果リストを検索します。存在する場合は結果リストを返し、存在しない場合はデータベースにクエリを実行して結果リストを取得し、クエリ キーに従って結果リスト全体をクエリ キャッシュに入れます。
3) クエリキーの SQL にはいくつかのテーブル名が含まれており、これらのテーブル内のデータが変更、削除、追加された場合、これらの関連するクエリキーはキャッシュからクリアされます。

Hibernate の遅延ロード メカニズム
遅延ロード:

遅延ロード メカニズムは、不必要なパフォーマンスのオーバーヘッドを回避するために提案されています。いわゆる遅延ロードとは、データが実際に必要なときにデータ ロード操作が実際に実行されることを意味します。 Hibernate はエンティティ オブジェクトの遅延ロードとコレクションの遅延ロードを提供します。さらに、Hibernate3 はプロパティの遅延ロードも提供します。以下では、これらのタイプの遅延読み込みの詳細をそれぞれ紹介します。

A. エンティティ オブジェクトの遅延ロード:

エンティティ オブジェクトに遅延ロードを使用する場合は、以下に示すように、エンティティのマッピング構成ファイルでそれを構成する必要があります。クラスの属性 true に設定すると、エンティティの遅延読み込み機能が有効になります。次のコードを実行すると:

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

(1)

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

(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 までご連絡ください。