[Java Performance] 数据库性能最佳实践 – JPA缓存

[Java Performance] 数据库性能最佳实践 – JPA缓存

大家好,又见面了,我是全栈君。

JPA缓存(JPA Caching)

JPA有两种类型的缓存:

  • EntityManager自身就是一种缓存。事务中从数据库获取的和写入到数据库的数据会被缓存(什么样的数据会被缓存。在后面有介绍)。在一个程序中或许会有非常多个不同的EntityManager实例。每个实例执行着不同的事务,拥有着它们自己的缓存。

  • 当EntityManager提交一个事务后,它缓存的全部数据就会被合并到一个全局的缓存中。

    全部的EntityManager都可以訪问这个全局的缓存。

全局缓存被称为二级缓存(Level 2 Cache)。而EntityManager拥有的本地缓存被称为一级缓存(Level 1 Cache)。全部的JPA实现都拥有一级缓存,而且对它没有什么能够调优的。

而二级缓存就不同了:大多数JPA实现都提供了二级缓存,可是有些并没有把启用它作为默认选项,比方Hibernate。一旦启用了二级缓存。它的设置会对性能产生较大的影响。

仅仅有当使用实体的主键进行訪问时,JPA的缓存才会工作。这意味着。以下的两种获取方式会将获取的结果放入到JPA的缓存中:

  • 调用find()方法,由于它须要接受实体类的主键作为參数
  • 调用实体类型的getter方法来得到关联的实体类型。本质上。获取关联的实体对象也是通过关联对象的主键得到,由于在数据库的表结构中。存放的是该关联对象的外键信息。

那么当EntityManager须要通过主键或者关联关系获取一个实体对象时。它首先会去二级缓存中寻找。

假设找到了,那么它就不须要对数据库进行訪问了。

通过查询(JPQL)方式得到的实体对象是不会被放到二级缓存中的。

然而在一些JPA实现中也会将查询得到的结果放入到缓存中。可是仅仅有当同样的查询再次被运行时,这些缓存才会起作用。所以即使JPA的实现支持查询缓存,查询返回的实体也不会被存储在二级缓存中。因此也就不能被诸如find()等方法利用了。

通过以下的一段代码对二级缓存和查询进行性能測试:

EntityManager em = emf.createEntityManager();
Query q = em.createNamedQuery(queryName);
List<StockPrice> l = q.getResultList(); // SQL Call 1
for (StockPrice sp : l) {
    // ... process sp ...
    if (processOptions) {
        Collection<? extends StockOptionPrice> options = sp.getOptions(); // SQL Call 2
        for (StockOptionPrice sop : options) {
            // ... process sop ...
        }
    }
}
em.close();

以上代码通过一个命名查询来得到StockPrice实体对象。 布尔变量processOptions用来控制是否遍历关联的StockOptionPrice实体对象。

缓存和懒载入

@NamedQuery(name="findAll", query="SELECT s FROM StockPriceImpl s ORDER BY s.id.symbol")

@OneToMany(mappedBy="stock")
private Collection<StockOptionPrice> optionsPrices;

在默认情况下,对于StockPrice关联的StockOptionPrice,因为是一对多的关联方式,后者的载入类型是懒载入。执行

測试用例 首次运行 兴许运行
默认缓存策略 + 懒载入 61.9s (33,409 SQL调用) 3.2s (1 SQL 调用)
默认缓存策略 + 懒载入 + 不遍历关联对象 5.6s (1 SQL 调用) 2.8s (1 SQL 调用)

当须要遍历关联对象时。在首次运行时产生了大量SQL调用。这是由于对于每一个StockPrice实例。都须要遍历其StockOptionPrice集合,因此产生了:128 * 261 = 33408次SQL调用。

再加上获取StockPrice的一次命名查询,所以一共是33409次。可是在兴许运行时,仅仅会发生一次命名查询导致的SQL调用,这是由于StockOptionPrice此时所有都已经被存储到二级缓存中(由关联关系和find方法得到的实体对象会被保存到二级缓存中,而查询结果则不会被保存),不须要再对数据库进行訪问。

当不须要遍历关联对象时,每次运行都仅仅会产生一次SQL调用。

同一时候注意到对于此測试用例,首次运行仍然比兴许运行要慢整整一倍,这是由于编译器的“热身”也会在首次运行期间进行(关于JIT编译器的性质。请查看相关章节)。

缓存和马上载入

当StockOptionPrice的载入方式切换成马上载入后,得到的測试数据例如以下:

測试用例 首次运行 兴许运行
默认缓存策略 + 马上载入 60.2s (33,409 SQL调用) 3.1s (1 SQL 调用)
默认缓存策略 + 马上载入 + 不遍历关联对象 60.2s (33,409 SQL 调用) 2.8s (1 SQL 调用)

此时,不管是否选择遍历关联对象。都会发生33409次SQL调用。

由于在运行命名查询得到每一个StockPrice对象后,就会顺便调用StockOptionPrice的getter方法来得到关联对象。此时得到的StockOptionPrice对象会被存储到二级缓存中。因此在兴许运行中不会再触发SQL调用。

JOIN FETCH和缓存

假设在命名查询中使用JOIN FETCH:

@NamedQuery(name="findAll", query="SELECT s FROM StockPriceEagerLazyImpl s " + "JOIN FETCH s.optionsPrices ORDER BY s.id.symbol")

測试用例 首次运行 兴许运行
默认配置 61.9s (33,409 SQL调用) 3.2s (1 SQL 调用)
JOIN FETCH 17.9s (1 SQL 调用) 11.4s (1 SQL 调用)
JOIN FETCH + 查询缓存 17.9s (1 SQL 调用) 1.1s (0 SQL 调用)

当使用了JOIN FETCH后,性能得到了很大的提升。尽管查询的数据量是相同的。可是发生的SQL调用剧减到了1,这也是性能得以大幅提升的首要原因。可是。由于缺少查询缓存。在兴许调用的时候仍然须要较长的时间(相同地,运行时间从17.9s -> 11.4s是由于首次运行期间JIT编译器须要“热身”)。

所以在最后一个測试用例,当开启了查询缓存后,兴许运行的时间大幅缩短到1.1s。同一时候没有发生SQL调用。这是一个使用查询缓存的典型样例。可是须要注意仅仅有当查询使用的參数全然同样时,查询缓存才会起作用。

避免查询

依据二级缓存的特点,假设不使用查询,那么得到的全部对象都会被保存到二级缓存中。那么当程序执行一段时间后。随着对象都被缓存,须要执行的SQL语句就越来越少。程序的执行速度也就越来越快了:

EntityManager em = emf.createEntityManager();
ArrayList<String> allSymbols = ... all valid symbols ...;
ArrayList<Date> allDates = ... all valid dates...;
for (String symbol : allSymbols) {
    for (Date date = allDates) {
        StockPrice sp = em.find(StockPriceImpl.class, new StockPricePK(symbol, date);
        // ... process sp ...
        if (processOptions) {
            Collection<? extends StockOptionPrice> options = sp.getOptions();
            // ... process options ...
        }
    }
}

測试结果例如以下所看到的:

測试用例 首次运行 兴许运行
默认配置 61.9s (33,409 SQL调用) 3.2s (1 SQL 调用)
无查询 100.5s (66,816 SQL 调用) 1.19s (0 SQL 调用)

首次运行会产生66816次SQL调用。当中33408次是调用find方法时产生的。另外33408次时调用getOptions方法时产生的。在此之后。全部的对象都会被保存到二级缓存中,因此兴许运行时,没有SQL被运行。

所以,当使用无查询的策略是。首次运行的时间一般会比較长,这个过程能够被看成是一个“热身”的过程。在“热身”结束之后。程序的性能会提高一个档次。

另外须要注意的一个问题是,即使使用getOptions方法得到的是一个集合对象,这个集合对象的全部元素也会被存储到二级缓存中,不要将它和查询混淆。所以,当希望缓存一个实体对象关联的一组实体对象时,仅仅须要调用对应的getter方法就可以。甚至不须要对该集合进行遍历。

设置JPA缓存的空间

当JPA缓存占用的内存过多时,它会给GC加入不小的压力。

所以JPA缓存的空间须要被细致设置。可是,JPA规范并没有规定怎样设置JPA缓存。所以须要查看相应JPA实现的相关文档。

TODO:和堆相关

总结

  1. JPA的二级缓存会自己主动地为应用缓存对象。

  2. 二级缓存不会保存查询(JPQL)的返回对象。所以当须要缓存对象时,不要使用查询。

    (或者开启查询缓存)

  3. 慎重使用结合了JOIN FETCH的查询。除非使用的JPA实现支持查询缓存。由于默认情况下。查询会跳过二级缓存。

JPA仅仅读实体(JPA Read-Only Entities)

虽然JPA规范并没有介绍仅仅读实体。可是在非常多JPA实现中,都会这样的实体作出对应的优化。

对仅仅读实体的操作在性能上一般都会优于读写实体(Read-Write Entities)。由于对于仅仅读实体,不须要保存它的状态,不须要将它放在事务中。也不须要对它进行加锁。

在Java EE容器中。不管使用的什么JPA实现,仅仅读实体一般都会被支持。应用server会保证对这些实体的获取是通过一个特殊的非事务性的JDBC连接来完毕。

这样做通常都有更好的性能。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/116230.html原文链接:https://javaforall.net

(0)
上一篇 2022年1月24日 上午11:00
下一篇 2022年1月24日 上午11:00


相关推荐

  • Win10下WireShark没有找到接口

    Win10下WireShark没有找到接口电脑的 win10 更新后 打开 WireShark 突然发现 没有找到接口 如下图所示 尝试了网上介绍的方法 在 http www win10pcap org download 下载安装 Win10Pcap 重启后仍然无法解决该问题 最后在 WireShark 官网 https www baidu com link url 1GCxDCUrOqXN

    2026年3月19日
    2
  • 虚函数详解[通俗易懂]

    虚函数详解[通俗易懂]文章目录一、虚函数实例二、虚函数的实现(内存布局)1、无继承情况2、单继承情况(无虚函数覆盖)3、单继承情况(有虚函数覆盖)4、多重继承情况(无虚函数覆盖)5、多重继承情况(有虚函数覆盖)三、虚函数的相关问题1、构造函数为什么不能定义为虚函数2、析构函数为什么要定义为虚函数?3、如何去验证虚函数表的存在  面向对象的语言有三大特性:继承、封装、多态。虚函数作为多态的实现方式,重要性毋庸置疑。 …

    2022年7月26日
    13
  • 网络编程学习笔记一:Socket编程

    网络编程学习笔记一:Socket编程“一切皆Socket!”话虽些许夸张,但是事实也是,现在的网络编程几乎都是用的socket。——有感于实际编程和开源项目研究。我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有

    2022年7月13日
    18
  • 51单片机rc522程序_51单片机usb驱动

    51单片机rc522程序_51单片机usb驱动最近在某宝上买了一块RC522模块,试玩了下,读写卡正常。想学习使用新的东西时,有必要了解它的工作原理和工作过程,不清楚或者不知道的可以参考相关数据手册和参考文献,在这里为了节省自己的时间,我只对我的51程序做一个小小的笔记~~想要驱动RC522模块对IC卡(这里用的是M1卡型号是S50)进行读写操作,一定要有以下5个步骤:一、寻卡二、防冲突三、选择卡四、验证扇区密码(每个扇区都有密匙A…

    2026年1月21日
    5
  • 哪位大神了解LEACH算法的可以解释下,LEACH算法构成网络结构时是用在随机部署网络节点的还是确定性部署网络节点呢?

    哪位大神了解LEACH算法的可以解释下,LEACH算法构成网络结构时是用在随机部署网络节点的还是确定性部署网络节点呢?哪位大神了解 LEACH 算法的可以解释下 LEACH 算法构成网络结构时是用在随机部署网络节点的还是确定性部署网络节点呢 我现在在做交通监控 需要确定性部署网络节点 想通过 LEACH 算法来构成路由树 传输协议 我查的资料怎么感觉 LEACH 算法是针对随机部署节点的呢 有哪位大神懂这方面可以给解答的吗 万分感谢

    2025年10月20日
    5
  • 证明frobenius范数是个范数_1范数怎么求

    证明frobenius范数是个范数_1范数怎么求Frobenius范数,简称F-范数,是一种矩阵范数,记为||·||F。矩阵A的Frobenius范数定义为矩阵A各项元素的绝对值平方的总和,即

    2022年8月3日
    7

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号