JPA缓存类型

JPA缓存类型与原理

JPA缓存简介

缓存在一个对象的持久化过程中可以缓存对象或者它的数据。缓存同样影响着对象的一致性,如果你查找了一个对象,之后你查找了一个相同的对象,就会返回与刚才一致的对象(其指向的内存单元相同)
JPA 1.0 没有定义共享对象的缓存,其共享对象的缓存主要是有供应商自行提供。在JPA中,缓存主要存在于transaction或者扩展的持久化上下文来保护对象的一致性,但是JPA不必一定要在transaction或者持久化上下文中开启缓存。
JPA 2.0 定义了一个共享缓存。通过注解的形式 @Cacheable或者cacheable XML 属性的形式来开启或者停止缓存对象。
例如:

Example JAP 2.0 :

1
2
3
4
5
@Entity
@Cacheable
public class Employee{
...
}

同样的,你可以在jpa的persistence.xml文件中以添加属性的方式来配置全部持久化单元的缓存模式,
例如:

Example JAP 2.0 SharedCacheMode XML

1
2
3
<prsistence-unit name="GG">
<shared-cache-mode>NONE</shared-cache-mode>
</prsistence-unit>

对象一致性

对象一直性在java中的含义是:如果两个值x,y他们指向同一个逻辑单元,那么x和y相等。及指正指向相同的内存单元。
在JPA中,对象的一致维持在一个transaction,相同的EntityManager中。在JEE中,对象一致性只在同一个transaction中维持。

所以如下实例返回值为true

1
2
3
Employee employee1 = entityManager.find(Employee.class,123);
Employee employee2 = entityManager.find(Employee.class,123);
assert (employee1 == employee2 );

该值一直为true无论该值是如何获取的

1
2
3
Employee employee1 = entityManager.find(Employee.class, 123);
Employee employee2 = employee1.getManagedEmployees().get(0).getManager();
assert (employee1 == employee2)

在jpa中,对象一致性不会再不同的EntityManager中维持,每个EntityManager维持自己的持久化上下文和自己对象的transactional状态。
所以

如下的代码返回值为true

1
2
3
4
5
EntityManager entityManager1 = factory.createEntityManager();
EntityManager entityManager2 = factory.createEntityManager();
Employee employee1 = entityManager1.find(Employee.class, 123);
Employee employee2 = entityManager2.find(Enployee.class, 123);
assert(employee1 != employee2);

对象一致性是jpa的有点,他可以避免在应用程序中管理多个对象副本,并且可以避免当一个对象改变后其他副本不变。由于不同的EntityManager或者transactions(在JEE中)不能维持对象的唯一性,所以,每个transaction在其他用户的系统中必须相互隔离。无论如何,我们必须知道应用程序是拷贝、分离或者是合并对象。
一些JPA产品可能含有只读对象的概念,对象的一致性通过共享对象缓存中的EntityManager维系。

对象缓存

一个对象缓存就是缓存JAVA对象。对象缓存的优点是将对象按照其在java中的格式进行存储。所有属性都存放在对象层,当缓存命中后,无需做任何的转换。在EntityManager从cache拷贝或者塞入cache的时候,必须要保证他的tarnsaction的独立性。对象不需要被重新创建,他们之间的关联关系已经是有价值的了。
暂时数据可以通过对象缓存存储。他可以自动进行也可以指定。如果暂时数据是不需要的,你也必须在获取该对象的缓存时将其清除。
一些JPA提供商运行只读的查询来直接获取对象的缓存。一些提供商值运行对象缓存只读信息。如果只是查找,使用对象缓存可以大大的提高查找效率。
你也可以通过自制的缓存系统来缓存你的只读数据。
TopLink/EclipseLink:支持对象缓存。该对象缓存是默认的,但是可以设置全局或者有选择的开启或停止。在persisitenc unit属性中的eclipselink.cache.shared.default可以设置为false来停止缓存。只读查询语句可以通过在查询语句中使用提示eclipselink.read-only来标注。Entity中可以通过标注@ReadOnly来标注只读。

数据缓存

数据缓存缓存对象的数据而非对象。该数据为该对象在数据库中的一行数据。数据缓存操作简单,并且你无需担心对象关系,数据一致性或者缓存管理。 数据缓存的缺点是当应用程序使用该数据时,其并不存储,且并不存放关联。这意味着,在内存中命中了对象,也需要从数据库中查找数据和其关联。一些提供商提供了数据缓存、关系缓存和查询缓存来满足缓存数据间的关系。

缓存关联

一些提供商支持一些分离的缓存来存放关联关系,这些支持OneToManyManyToMany关联关系。由于OneToOneManyToOne关联关系会涉及到对象的Id,通常是不需要被缓存的,但是一个反向的OneToOne由于它涉及到的不是主键而是外键,所以需要将该关联关系缓存起来。
关联缓存的缓存结果通常只缓存关联对象的Id而非对象或者数据。关联缓存主要存放资源对象的Id和关系的名称。有时,如果数据存储存放的数据结果而非数据行时,关联关系被作为数据缓存的一部分。当关联缓存被命中了,相关的对象将会在数据缓存中一个个的被查找出来。但是,如果相关对象不在数据缓存中,那么它将必须在所选的数据库中查找,这将会严重影响数据库的效率,因为它需要一个一个的读取数据。

缓存类型

有很多不同的缓存类型。最常见的是LRU和MRU。
一些cache类型包括如下:

  • LRU :在内存中保留N个最近使用的对象
  • FULL:永远存放所有读到的内容。
  • Soft:当内存很低时,使用JAVA垃圾收集机制来提示缓存释放对象
  • Weak:与对象缓存相关,在缓存中存放所有当前使用的对象
  • L1 :涉及到事务缓存(事务缓存是EntityManager的一部分,并非共享缓存)
  • L2 : 为共享缓存,概念上的存放在EntityManagerFactory,它控制所有的EntityManager
  • 数据缓存 : 数据行存储
  • 对象缓存 : 直接缓存对象
  • 关联关系缓存:缓存对象的关联关系
  • 查询缓存: 缓存一个查询语句的结果集合
  • 只读 :缓存只存放只读对象
  • 读写 :缓存可以处理插入、更新和删除
  • 事务 :缓存可以处理插入、更新和删除,并且满足事务的ACID
  • 集群 :当一个对象更新或者删除后,使用JMS、JGroups或者其他的机制来向其他集群中的服务广播失效信息。
  • 副本 :当一个对象读入任何一个服务缓存中时,使用JMS、JGroups或者其他的机制来通知所有服务器
  • 分布式:通过集群,快速的传播缓存对象,并且可以在另外的服务器缓存中查到该对象。

TopLink/EclipseLink:支持L1、L2、LRU、Soft、Full、Weak、查询缓存。对象缓存支持读写的和事务。同时通过JMS和RMI支持集群缓存。

查询缓存

查询缓存缓存查询结果而非对象。对象缓存通过缓存对象的Id,所以对于查询不是用Id作为查询条件的查询操作并不有效。一些对象存储支持二级索引,但是即使如此对于一些返回大量对象的查询操作,这仍然不是很有效。你必须通过数据库来确保你获取了所有对象。这就是为什么查询缓存是非常有用的而不仅仅通过存放对象的Id,查询结果需要被缓存。缓存的key是基于查询名称和变量。所以如果你有NamedQuery一直执行,你可以缓存他的结果,而且你只要执行查询语句一次即可。
查询缓存的主要问题是,它会缓存旧数据。查询缓存通常会与对象缓存相互作用来确保对象在对象缓存中是否过时。查询缓存有一些无效选项,这个对象缓存很像。
TopLink / EclipseLink:通过在查询操作中暗示 eclipselink.query-results-cache来开启。

旧数据

!!缓存的主要问题是如何同步原始数据(The main issue with caching anything is the possibility of the cache version getting out of synch with the original. )!!。这涉及到作为旧数据或者不同步的数据。对于只读数据,这不存在问题,但是对于频繁进行数据改变的,这可能是一个主要问题。这里有很多方式来处理旧数据和未同步数据。

一级缓存

在事务过程中或者请求中缓存对象的状态并不是一个问题。这通常称为一级缓存,或者说是EntityManager缓存,并且通过正确的JPA事务语义完成。如果你读同一对象两次,你必须获取同一对象,和相同的内存改变。问题仅仅发生在查询和DML过程中。

通过数据库查询,查询不会引入未写入的对象的状态。例如,你要持久化一个新的对象,但是JP还没有将该对象存入数据库,因为它只会在事务提交的时候才会写入数据库。所以你的查询将无法返回一个新的对象,因为他查找的是数据库而非一级缓存中。对于这个问题,用户可以调用flush()操作或者flushMode自动触发刷新操作。在EntityManager或者QueryflushMode的默认情况是要触发一个刷新操作,但是当一个写操作在查询操作还没有释放时,该操作会无法触发刷新。一些JPA提供商支持当对象在内存中更改了后,JPA确认数据库查询结果。这可以直接获取数据而不是要触发刷新操作。这可以对简单的查询操作起作用,但是对于复杂的查询,这将会大大提高查询的复杂程度。由于应用程序查询数据都是在更改数据之前,或者不需要查询已有对象,所有这并不是一个问题。
如果你绕过JPA执行DML直接操作数据库,或者通过原生的SQL查询,JDBC或者JPQLUPDATE或者DELETE查询,那么数据库可能会与一级缓存不同步。如果你在执行DML之前已经获取了对象,他们会有一个旧的状态并且不会包括改变的信息。确保你做的操作是正确的,否则你可能必须重新刷新受影响的数据。

一级缓存,或者EntityManager缓存在JPA中可以跨越事务的界限。一个JTA管理EntityManager将只要存在于该JTA事务在JEE中的整个过程。JEE服务将会将应用的代理注入进一个EntityManager,当开启一个JTA事务时,一个新的EntityManager将会被创建,或者被清楚。一级缓存在一个EntityManager创建使用过程中一直存在,如果一直长时间的维持它,它可能会含有旧数据,甚至是内存漏洞和低效。在每个请求时创建一个新的EntityManager是一个很好的主意。一级缓存同样可以用EntityManager.clear()方法或者调用EntityManager.reflush()方法来实现清空的操作。

二级缓存

二级缓存包括了事务,EntityManager,他不是JPA必要的一部分。大多数JPA提供商支持二级缓存,但是实施和使用的语法上有一些不同。一些JPA提供商默认开启二级缓存。
如果系统只是一个提供访问数据库的应用程序或者服务,那么在使用二级缓存过程中问题不大。因为他应该经常会更新。但是主要的问题是在使用DML,如果应用直接通过原生SQL语句,JDBC,或者JPQL1的UPDATE或者DELETE语句。JPQL查询会自动使二级缓存数据无效,但是这可能需要基于JPA的提供商。如果你使用了原生的DML查询或者直接用JDBC,你需要通过刷新、清除或者其他方式使受影响的数据无效。
如果还有其他应用或者应用服务访问相同数据库,那么旧数据将会成为一个大问题。只读对象或者插入新对象不会发生问题。新对象一硬格从其他服务通过缓存数据来访问数据库而得到。这通常只使用find()操作和关联命中缓存。其他应用或者服务更新和删除对象会使数据成为旧数据。
由于更新对象,任何查询该对象的操作都可能返回旧的数据。在更新对象过程中或者由于一个用户在未使用锁的情况下,覆盖了另一个用户的更改信息这都会触发乐观锁。当然如果只有一个应用或者服务访问数据库,在不使用缓存的情况下也是可能发生的。这就是使用乐观锁的重要意义。当然,旧数据也肯能使用户获取到。

刷新

刷新是最常用的更新旧数据的方法。大多数应用软件用户很数据缓存的概念,并且知道何时他们需要刷新数据,并且乐于点击刷新按钮。这个在浏览器非常常见,大多数浏览器有其访问网页的缓存,从而避免加载同一界面两次,除非用户点击刷新按钮。相同的理论也可以在构建JPA应用时使用。JPA提供商有很多刷新操作。
一些JPA提供商在二级缓存上支持刷新选项。一个选项是在任何查询数据操作后都刷新。这意味着find()操作将仍然通过缓存。但是如果查询数据库或者获取数据库数据,二级缓存将会刷新数据。这避免了查询旧数据。但是这也使缓存失去了意义。刷新的代价不光光是刷新对象还要刷新他们的关联关系。一些JPA提供商提供了该属性整合乐观锁。如果该数据在数据库行中的值比在缓存中的版本值新,那么他会刷新旧数据,否则缓存值将会被返回。这个属性提供了优化的缓存,从而避免查询到旧数据。然而通过find()或者通过关联获取的数据任然是旧值。一些JPA提供商也允许find()操作可配置成首先核对数据库,但是这违背了cache的目的。

JPA 2.0 内存 API

JPA 2.0提供了一组标准的查询提示来刷新或者忽略cache。这些查询提示在CacheRetrieveMode和CacheStoreMode定义。
Query hints:

  • javax.persistence.cache.retrieveMode : CacheRetrieveMode
    • BYPASS : 忽略缓存,直接从数据库中获取内容
    • USE : 允许使用cache,如果对象/数据已经在cache中,则使用在cache中的数据
  • javax.persistence.cache.storeMode : CacheStoreMode
    • BYPASS : 不缓存数据库的查询结果
    • REFRESH : 如果对象/数据已经存在于cache中,用数据库中查询结果刷新/代替这些数据。
    • USE :缓存查询的结果
缓存设置实例
1
2
3

Query query = em.createQuery("Select e from Employee e");
query.setHint("javax.persistence.cache.storeMode",CacheStoreMode.REFRESH);

JPA2.0也提供了Cache接口,用户可以利用EntityManagerFactorygetCache()方法得到Cache接口。该Cache接口可以用来人工的删除在cache中的实例。
无论是一个特殊的实例、一个完整的类或者一个完整的缓存都可以被删除。该接口同时也可以判断该实例是否存在。
一些JPA生产厂商或扩展getCache()来提供额外的API。
TopLink / EclipseLink : 提供一个额外的缓存接口JpaCache。提供额外的API进行失效、查询缓存、删除缓存操作。

缓存清除实例
1
2
Cache cache = factory.getCache();
cache.evict(Employee.class, id);

缓存无效

常用的使缓存失效的方式是设置其缓存为无效状态。设置一定的时间,一天中特定的次数之后,删除或者是设置其无效。 超时失效保证了应用系统将不会再数据超时之后再从缓存中获取。这个时间可以在应用中设置。用户可以设置每天的失效时间,这可以确保一天的数据是新的。如果一个批处理在晚上更新,这时间可以设置为该作业之后。数据也可以人工的设置失效类似于使用JPA 2.0中的evict()API。
大多数内存的实现提供了一些失效的方式。JPA没有定义任何失效的配置参数,所有这些配置参数都是基于JPA和cache的提供商。
TopLink / EclipseLink :通过使用@Cache标注和orm.xml中的<cache>元素提供对失效时间的设置。失效时间也可以通过API来设置,也可用于集群中设置。

集群中的缓存

在集群,各个机器将会直接更新数据库而不是更新其他机器的缓存,每个机器的cache很容易过期,所以在集群中缓存是很困难的。但是这并不意味这在集群中无法使用缓存,你必须认真的配置它的参数。
对于只读对象的缓存可以一直使用。对于查看大多数对象可以使用缓存,但是一些场景需要避免使用旧数据。如果旧数据只是在写操作过程中存在问题,可以使用乐观锁来避免发生旧数据的写入。当乐观锁的异常发生,一些JPA提供商会自动更新或者使当前数据在cache中失效。所有如果用户或者应用再次执行该事务时,写操作的时候将会成功。你的应用也可以捕获锁异常并且刷新或者使该对象失效,或者潜在的重试该事务(如果用户不需要关注锁错误)。缓存失效可以通过设置数据在缓存中的存活时间来减少旧数据的可能性。缓存的大小也影响旧数据。
虽然对于用户,返回旧数据是一个问题,但是当用户刚刚更新后,返回值为旧数据是一个更大的问题。这个可以通过设置session affinity来避免。但是必须确保用户在他们session过程中是和集群中的同一台机器做操作的。通常的操作是在页面上增加一个刷新按钮,这可以允许用户手动刷新。这些应用可以选择在更新重要数据之后进行刷新对象操作,类似的如在执行更新操作时使用只读的查询。
对于以写操作为主的对象,禁止那些对象的缓存是最好的解决方法。缓存对插入和更新是没有益处的。缓存操作将会增加很多额外的写操作,因为你必须要更新缓存,还要处理大量的缓存垃圾。所以如果cache没有提供帮助,你应该关闭它。如果对象含有一系列复杂的关系,只有很少一部分需要更新,那么缓存还是很有意义的。

缓存一致性

解决季军缓存的一个解决方法是通过消息框架在集群中各个机器之间协作。JMS或者JGroups可以与JPA或者应用时间结合,当一个更新发生时,利用广播通知其他机器。一些JPA提供商支持集群环境缓存的协作。
TopLink / EclipseLink : 利用JMS或者RMI 支持集群协作。通过使用@Cache注释或者在orm.xml中的<cache>并且使用持久化单元的属性eclipselink.cache.coordination.protocol完成集群缓存设置。

分布式缓存

一个分布式缓存是跨机器的缓存。每个对象将会存放在一个或者一批机器上。这可以避免旧数据,因为该缓存是从同一个地方获取更新数据的,所以它的数据始终是最新的。这个解决方法的重点是缓存需要网络通信。该解决方案当所有机器在集群中都相连并且有相同高速的网速,并且数据库服务器也有很好的链接和加载的情况下,该方法是很好的。分布式缓存环节了数据库访问,它可以使应用不受数据库的瓶颈扩展到一个大集群。一些分布式缓存提供商提供了本地缓存,提供缓存间的协调。
TopLink : 支持与Oracle Coherence的分布式缓存集成。

缓存事务的独立性

当使用缓存后,缓存的一致性和独立性就与数据库的事务独立性一样重要。对于基本缓存独立性,缓存的必须在数据库事务已经提交后更新,否者未提交的数据不能被其他用户访问。
缓存也可以是透明或者非透明的。在一个透明缓存中,这些从一个事务提交到缓存的变化作为一个简单的原子单元。这就意味这对象/数据必须先在缓存中加锁。当更新缓存后解锁。理性情况下,在事务执行之前获取锁,来确保与数据库中的一致性。非透明缓存在非加锁情况下,一步一步更新对象/数据。这意味着会有短暂一段时间,数据库中的内容和缓存中是数据不一致。这些都需视情况而定。
乐观锁定在高速缓存中的隔离的另一个重要的考虑因素。如果使用乐观锁,该缓存应该避免用旧数据替换新数据。当在读取的时候,系统正在更新数据库这种情况下,乐观锁非常重要。
一些JPA提供少运行配置他们缓存隔离。或者不同的缓存制定不同的隔离级别。