本章节将介绍 JPA 中缓存,合理使用缓存可以提高程序的性能。
在 JPA2.0 中,缓存分为一级缓存和二级缓存(JPA1.0 只支持一级缓存)。
在 JPA 中,持久化上下文(EntityManager)就是 JPA 的一级缓存。在该缓存区中,会将查询到的对象缓存到该区域中。
如果在同一个 EntityManager 中,查询相同 OID 的数据,那么只需要发送一条 sql。在事务提交/关闭 EntityManager 之后,一级缓存会清空。所以在不同的 EntityManager 中使用不同的一级缓存。
JPA 中的一级缓存也可以使用下面的方法手动清除缓存数据:
detach:清除一级缓存中指定的对象
clear:清除一级缓存中的所有的缓存数据
JPA 中的一级缓存的缓存能力是非常有限的,因为我们不会经常在一个 EntityManager 中查询相同的数据。
JPA 中的一级缓存属于请求范围级别的缓存,如下图:
上图中,在持久化上下文1和持久化上下文2中的User对象是不同的对象,它们之间不共享。
JPA 中的二级缓存通常是用来提高应用程序性能的,它可以避免访问已经从数据库加载的数据,提高访问未被修改数据对象的速度。
JPA 二级缓存是跨越持久化上下文的,是真正意义上的全局应用缓存,如下图:
如果二级缓存激活,JPA 会先从一级缓存中查找实体,未找到再从二级缓存中查找。当二级缓存有效时,就不能依靠事务来保护并发的数据,而是依靠锁策略,如在确认修改后,需要手工处理乐观锁失败等。
注意:JPA 二级缓存只能缓存通过 EntityManager 的 find 或 getReference 查询到的实体,以及通过实体的 getter 方法获取到的关联实体;而不能缓存通过 JPQL 查询获得的数据。
JPA 二级缓存通常用来提高性能,同时,使用二级缓存可能会导致提取到“陈旧”数据,也会出现并发写的问题。所以二级缓存最好是用在经常读取的数据,比较少更新数据的情况,而不应该对重要数据使用二级缓存。
对于不同的 JPA 实现产品,开启二级缓存的方式会有所不同,下面以 OpenJPA 为例,开启二级缓存,步骤如下:
(1)修改 persistence.xml 配置文件
<persistence-unit name="openJPA" transaction-type="RESOURCE_LOCAL"> <!-- JPA提供者 --> <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider> <!-- 声明实体类 --> <class>com.hxstrive.openjpa.model.User</class> <!-- 配置JPA数据库属性 --> <properties> <property name="openjpa.ConnectionURL" value="jdbc:mysql://localhost:3306/openjpa_learn?useSSL=false& serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8"/> <property name="openjpa.ConnectionDriverName" value="com.mysql.jdbc.Driver"/> <property name="openjpa.ConnectionUserName" value="root"/> <property name="openjpa.ConnectionPassword" value="aaaaaa"/> <property name="openjpa.Log" value="SQL=TRACE"/> <!-- 开启二级缓存 --> <property name="openjpa.DataCache" value="true(CacheSize=3, SoftReferenceSize=3)" /> <property name="openjpa.RemoteCommitProvider" value="sjvm" /> <!-- 自动生成表 --> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/> <!-- 不使用加载时强化和编译时强化,使用运行时Unenhanced(不能发挥OpenJPA的最大效能,所以也不推荐) --> <property name="openjpa.ClassLoadEnhancement" value="false"/> <property name="openjpa.DynamicEnhancementAgent" value="false"/> <property name="openjpa.RuntimeUnenhancedClasses" value="supported"/> </properties> </persistence-unit>
上面配置中,openjpa.DataCache 用来缓存 EntityManagerFactory 级别的持久化实体的。当启用 openjpa.DataCache 后,OpenJPA 会先检查 openjpa.DataCache,如果实体不存在再从存储介质中读取。
同其他缓存技术需要管理的属性相似,可能在缓存管理的算法上有所区别,OpenJPA 中属性管理如下:
CacheSize:指的是缓存实体的最大数目,默认 CacheSize 是 1000,如果缓存实体超出这一数值时,OpenJPA 会随机逐出一些实体,直至实体数目小于设定的最大数目。
SoftReferenceSize:指 OpenJPA 的缓存如果达到最大存储数目,或根据一定的算法需要从缓存中移出数据时,会把它们放到一个 softReferenceMap 中,softReferenceMap 中的数据也会删除只是这些数据比直接删除会待的时间久一些,以防止在缓存删除他们以后程序马上又需要访问他们。这样下次访问到这些移出的数据时就会到 softReferenceMap 中去找,这样比直接删除和重新访问数据库查找的效率好一点(这点跟其他的缓存实现有点不一样,其它的缓存是直接从缓存中删除这些数据)。如果你不想使用sofeReferenceMap,就把它的值设成 0。
(2)测试代码
下面代码将分别获取 ID 为 1 的 User 对象,通过输出的 SQL 语句观察是否使用缓存。代码如下:
import com.hxstrive.openjpa.model.User; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; public class Demo { private static final String PERSISTENCE_NAME = "openJPA"; public static void main(String[] args) { Demo demo = new Demo(); EntityManagerFactory managerFactory = null; try { managerFactory = Persistence.createEntityManagerFactory( PERSISTENCE_NAME, System.getProperties()); System.out.println("第一次:"); demo.queryUser(managerFactory, 1); System.out.println("\n第二次:"); demo.queryUser(managerFactory, 1); } catch (Exception e) { e.printStackTrace(); } finally { if (null != managerFactory) { managerFactory.close(); } } } private void queryUser(EntityManagerFactory managerFactory, int userId){ EntityManager entityManager = null; try { entityManager = managerFactory.createEntityManager(); User user = entityManager.find(User.class, userId); System.out.println(user); } finally { if (null != entityManager) { entityManager.close(); } } } }
如果开启 DataCache 缓存,执行上面 Java 代码,输出结果如下:
第一次: 141 openJPA INFO [main] openjpa.Runtime - Starting OpenJPA 2.4.2 344 openJPA INFO [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.MySQLDictionary". 1845 openJPA INFO [main] openjpa.jdbc.JDBC - Connected to MySQL version 5.5 using JDBC driver MySQL Connector Java version mysql-connector-java-5.1.46 ( Revision: 9cc87a48e75c2d2e87c1a293b2862ce651cb256e ). 2439 openJPA INFO [main] openjpa.Enhance - Creating subclass and redefining methods for "[class com.hxstrive.openjpa.model.User]". This means that your application will be less efficient than it would if you ran the OpenJPA enhancer. 3017 openJPA TRACE [main] openjpa.jdbc.SQL - <t 565372776, conn 1462044018> executing prepstmnt 1778629809 SELECT t0.age, t0.birthday, t0.name, t0.salary FROM User t0 WHERE t0.id = ? [params=?] 3033 openJPA TRACE [main] openjpa.jdbc.SQL - <t 565372776, conn 1462044018> [16 ms] spent User{id=1, name='update-update-User-7ef16e-0', age=80, salary=2357.7673, birthday=Thu Sep 02 22:17:23 CST 2021} 第二次: User{id=1, name='update-update-User-7ef16e-0', age=80, salary=2357.7673, birthday=Thu Sep 02 22:17:23 CST 2021}
如果关闭 DataCache 缓存,执行上面 Java 代码,输出结果如下:
第一次: 172 openJPA INFO [main] openjpa.Runtime - Starting OpenJPA 2.4.2 281 openJPA INFO [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.MySQLDictionary". 1065 openJPA INFO [main] openjpa.jdbc.JDBC - Connected to MySQL version 5.5 using JDBC driver MySQL Connector Java version mysql-connector-java-5.1.46 ( Revision: 9cc87a48e75c2d2e87c1a293b2862ce651cb256e ). 1799 openJPA INFO [main] openjpa.Enhance - Creating subclass and redefining methods for "[class com.hxstrive.openjpa.model.User]". This means that your application will be less efficient than it would if you ran the OpenJPA enhancer. 2424 openJPA TRACE [main] openjpa.jdbc.SQL - <t 931675031, conn 111900554> executing prepstmnt 940584193 SELECT t0.age, t0.birthday, t0.name, t0.salary FROM User t0 WHERE t0.id = ? [params=?] 2440 openJPA TRACE [main] openjpa.jdbc.SQL - <t 931675031, conn 111900554> [16 ms] spent User{id=1, name='update-update-User-7ef16e-0', age=80, salary=2357.7673, birthday=Thu Sep 02 22:17:23 CST 2021} 第二次: 2471 openJPA TRACE [main] openjpa.jdbc.SQL - <t 931675031, conn 111900554> executing prepstmnt 555273695 SELECT t0.age, t0.birthday, t0.name, t0.salary FROM User t0 WHERE t0.id = ? [params=?] 2471 openJPA TRACE [main] openjpa.jdbc.SQL - <t 931675031, conn 111900554> [0 ms] spent User{id=1, name='update-update-User-7ef16e-0', age=80, salary=2357.7673, birthday=Thu Sep 02 22:17:23 CST 2021}