[Java Performance] 数据库性能最佳实践 - JPA和读写优化

数据库性能最佳实践

当应用需要链接数据库时。那么应用的性能就可能收到数据库性能的影响。java

比方当数据库的I/O能力存在限制,或者因缺失了索引而致使运行的SQL语句需要对整张表进行遍历。对于这些问题。只相应用代码进行优化多是不够。还需要了解数据库的知识和特色。git

演示样例数据库

该数据库表示了128仅仅股票在1年内(261个工做日)的股价信息。github

当中有两张表:STOCKPRICE和STOCKOPTIONPRICE。sql

STOCKPRICE中使用股票代码做为主键。另外还有日期字段。它有33408条记录(128 * 261)。 STOCKOPTIONPRICE中存放了每仅仅股票在天天的5个Options。主键是股票代码,另外还有日期字段和表示Option号码的一个整型字段。它有167040条记录(128 * 261 * 5)。数据库

JPA

对JPA的性能影响最大的是它使用的JDBC Driver。除此以外,另外一些其它因素也会影响JPA的性能。缓存

JPA是经过对实体类型的字节码进行加强来提升JPA的性能的,这一点在Java EE环境中对用户是透明的。但是在Java SE环境中需要确保这些字节码操做的正确性。不然,会出现各类各样的问题影响JPA的性能。比方:安全

  • 需要懒载入(Lazy Load)的字段被立刻载入(Eager Load)了
  • 保存到数据库中的字段出现了没必要要的冗余
  • 应当保存到JPA缓存中的数据没有保存。致使本没必要要的重取(Refetch)操做

JPA对于字节码的加强通常做为编译阶段的一部分。在实体类型被编译成为字节码后,它们会被后置处理程序(它们是实现相关的,也就是EclipseLink和Hibernate使用的后置处理程序是不一样的)进行处理来加强这些字节码。获得通过优化了的字节码文件。性能优化

在有的JPA实现中,还提供了当类被载入到JVM中时,动态加强字节码的方法。需要为JVM指定一个agent,经过启动參数的形式提供。比方当但愿使用EclipseLink的这一功能时,可以传入:-javaagent:path_to/eclipselink.jarapp

事务处理(Transaction Handling)

JPA可以使用在Java SE和Java EE应用中。差异在于事务的处理方式。less

在Java EE中。JPA事务仅仅是应用server的Java事务API(JTA)实现的一部分。它提供了两种方式用来处理事务的边界:

  • 容器管理事务(Container-Managed Transaction,CMT)
  • 用户管理事务(User-Managed Transaction, UMT)

顾名思义,CMT会将事务的边界处理托付给容器,而UMT则需要用户在应用中指定边界的处理方式。在合理使用的状况下,CMT和UMT并无显著的差异。

但是。在使用不当的状况下,性能就会出现差别了,尤为是在使用UMT时,事务的范围可能会定义的过大或者太小。这样会对性能形成较大的影响。可以这样理解:CMT提供了一种通用的和折中的事务边界处理方式。使用它通常会更安全,而UMT则提供了一种更加灵活的处理方式,但是灵活是创建在用户必须十分了解它的基础上的。

@Stateless
public class Calculator {
    @PersistenceContext(unitName="Calc")
    EntityManager em;
    @TransactionAttribute(REQUIRED)
    public void calculate() {
        Parameters p = em.find(...);
        // ...perform expensive calculation...
        em.persist(...answer...);
    }
}

上述代码使用了CMT(使用了@TransactionAttribute注解),事务的做用域是整个方法。

当隔离等级是可反复读(Repeatable Read)时,意味着在进行计算(以上的Expensive Calculation凝视行)时,需要的数据会一直被锁定。从而对性能形成了影响。

在使用UMT时。会更灵活一点:

@Stateless
public class Calculator {
    @PersistenceContext(unitName="Calc")
    EntityManager em;
    public void calculate() {
        UserTransaction ut = ... lookup UT in application server...;
        ut.begin();
        Parameters p = em.find(...);
        ut.commit();

        // ...perform expensive calculation...

        ut.begin();
        em.persist(...answer...);
        ut.commit();
    }
}

上述代码的calculate方法没有使用@TransactionAttribute注解。

而是在方法中声明了两段Transaction,将昂贵的计算过程放在了事务外。固然,也可以使用CMT结合3个方法来完毕上面的逻辑。但是显然UMT更加方便和灵活。

在Java SE环境中。EntityManager被用来提供事务对象,但是事务的边界仍然需要在程序中进行设划分(Demarcating)。比方在如下的样例中:

在使用UMT时,会更灵活一点:

@Stateless
public class Calculator {
    @PersistenceContext(unitName="Calc")
    EntityManager em;
    public void calculate() {
        UserTransaction ut = ... lookup UT in application server...;
        ut.begin();
        Parameters p = em.find(...);
        ut.commit();

        // ...perform expensive calculation...

        ut.begin();
        em.persist(...answer...);
        ut.commit();
    }
}

上述代码的calculate方法没有使用@TransactionAttribute注解。

而是在方法中声明了两段Transaction,将昂贵的计算过程放在了事务外。

固然,也可以使用CMT结合3个方法来完毕上面的逻辑。但是显然UMT更加方便和灵活。

在Java SE环境中。EntityManager被用来提供事务对象。但是事务的边界仍然需要在程序中进行设划分(Demarcating)。比方在如下的样例中:

public void run() {
    for (int i = startStock; i < numStocks; i++) {
        EntityManager em = emf.createEntityManager();
        EntityTransaction txn = em.getTransaction();
        txn.begin();
        while (!curDate.after(endDate)) {
            StockPrice sp = createRandomStock(curDate);
            if (sp != null) {
                em.persist(sp);
                for (int j = 0; j < 5; j++) {
                    StockOptionPriceImpl sop = createRandomOption(sp.getSymbol, sp.getDate());
                    em.persist(sop);
                }
            }
            curDate.setTime(curDate.getTime() + msPerDay);
        }
        txn.commit();
        em.close();
    }
}

上述代码中。整个while循环被包括在了事务中。和在JDBC中使用事务时同样,在事务的范围和事务的提交频度上总会作出一些权衡,在下一节中会给出一些数据做为參考。

总结

  1. 在了解UMT的前提下,使用UMT进行事务的显式管理会有更好的性能。
  2. 但愿使用CMT进行事务管理时,可以经过将方法划分为多个方法从而将事务的范围变小。

JPA写优化

在JDBC中。有两个关键的性能优化方法:

  • 重用PreparedStatement对象
  • 使用批量更新操做

JPA也能够完毕这两种优化,但是这些优化不是经过直接调用JPA的API来完毕的,在不一样的JPA实现中启用它们的方式也不尽一样。对于Java SE应用。想启用这些优化一般需要在persistence.xml文件里设置一些特定的属性。

比方,在JPA的參考实现(Reference Implementation)EclipseLink中,重用PreparedStatement需要向persistence.xml中加入一个属性:

<property name="eclipselink.jdbc.cache-statements" value="true" />

固然,假设JDBC Driver能够提供一个Statement Pool,那么启用该特性比启用JPA的以上特性更好。毕竟JPA也是创建在JDBC Driver之上的。

假设需要使用批量更新这一优化,可以向persistence.xml中加入属性:

<property name="eclipselink.jdbc.batch-writing" value="JDBC" />
<property name="eclipselink.jdbc.batch-writing.size" value="10000" />

批量更新的Size不只可以经过上面的eclipselink.jdbc.batch-writing.size进行设置,还可以经过调用EntityManager上的flush方法来让当前所有的Statements立刻被运行。

下表显示了在使用不一样的优化选项时。运行时间的不一样:

优化选项 时间
无批量更新, 无Statement缓存 240s
无批量更新, 有Statement缓存 200s
有批量更新, 无Statement缓存 23.37s
有批量更新, 有Statement缓存 21.08s

总结

  1. JPA应用和JDBC应用相似。限制对数据库写操做的次数能够提升性能。

  2. Statement缓存能够在JPA或者JDBC层实现,假设JDBC Driver提供了这个功能,优先在JDBC层实现。
  3. JPA更新操做有两种方式实现,一是经过声明式(即向persistence.xml加入属性),二是经过调用flush方法。

JPA读优化

因为JPA缓存的參与。使得JPA的读操做比想象中的要复杂一点。同一时候也因为JPA会将缓存的因素考虑进来,JPA生成的SQL也并不是最优的。

JPA的读操做会在三个场合下发生:

  • 调用EntityManager的find方法
  • 运行JPA查询语句
  • 需要使用某个实体对象关联的其余实体对象

对于前两种状况。能够控制的是读取实体对象相应表的部分列仍是整行,是否读取该实体对象关联的其余对象。

尽可能少地读取数据

可以将某个域设置为懒载入来避免在读该对象时就将此域同一时候读取。当读取一个实体对象时。被声明为懒载入的域将会从被生成的SQL语句中排除。此后仅仅要在调用该域的getter方法时,才会促使JPA进行一次读取操做。对于基本类型,很是少使用这个懒载入,因为它们的数据量较小。

但是对于BLOB或者CLOB类型的对象,就有必要了:

@Lob
@Column(name = "IMAGEDATA")
@Basic(fetch = FetchType.LAZY)
private byte[] imageData

以上的IMAGEDATA字段因为太大且不会经常被使用。因此被设置成懒载入。这样作的优势是:

  • 让SQL运行的更快
  • 节省了内存,减少了GC的压力

另外需要注意的是,懒载入的注解(fetch = FetchType.LAZY)对于JPA的实现仅仅是一个提示(Hint)。真正在运行读取操做的时候,JPA或许会忽略它。

与懒载入相反,还可以指定某些字段为立刻载入(Eager Load)字段。比方当一个实体被读取时,该实体的相关实体也会被读取,像如下这样:

@OneToMany(mappedBy="stock", fetch=FetchType.EAGER)
private Collection<StockOptionPriceImpl> optionsPrices;

对于@OneToOne和@ManyToOne类型的域。它们默认的载入方式就是立刻载入。因此在需要改变这一行为时,使用fetch = FetchType.LAZY。相同的,立刻载入对于JPA也是一个提示(Hint)。

当JPA读取对象的时候,假设该对象含有需要被立刻载入的关联对象。

在很是多JPA的实现中,并不会使用JOIN语句在一条SQL中完毕所有对象的读取。它们会运行一条SQL命令首先获取到主要对象,而后生成一条或者多条语句来完毕其余关联对象的读取。当使用find方法时,没法改变这一默认行为。而在使用JPQL时。是可使用JOIN语句的。

使用JPQL时,并不能指定需要选择一个对象的哪些域,比方如下的查询:

Query q = em.createQuery("SELECT s FROM StockPriceImpl s");

生成的SQL是这种:

SELECT <enumerated list of non-LAZY fields> FROM StockPriceTable

这也意味着当你不需要某些域时。仅仅能将它们声明为懒载入的域。

使用JPQL的JOIN语句能够经过一条SQL来获得一个对象和它的关联对象:

Query q = em.createQuery("SELECT s FROM StockOptionImpl s " + "JOIN FETCH s.optionsPrices");

以上的JPQL会生成例如如下的SQL:

SELECT t1.<fields>, t0.<fields> FROM StockOptionPrice t0, StockPrice t1 WHERE ((t0.SYMBOL = t1.SYMBOL) AND (t0.PRICEDATE = t1.PRICEDATE))

JOIN FETCH和域是懒载入仍是立刻载入没有直接的关系。当JOIN FETCH了懒载入的域,那么这些域也会读取。而后在程序需要使用这些懒载入的域时,不会再去从数据库中读取。

当使用JOIN FETCH获得的所有数据都会被程序所使用时,它就能帮助提升程序的性能。

因为它下降了SQL的运行次数和数据库的訪问次数,这通常是一个使用了数据库的应用的瓶颈所在。

但是JOIN FETCH和JPA缓存的关系会有些微妙,在后面介绍JPA缓存时会讲述。

JOIN FETCH的其余实现方式

除了直接在JPQL中使用JOIN FETCH,还可以经过设置提示来实现。这样的方式在很是多JPA实现中被支持。比方:

Query q = em.createQuery("SELECT s FROM StockOptionImpl s");
q.setQueryHint("eclipselink.join-fetch", "s.optionsPrices");

在有些JPA实现中。还提供了一个@JoinFetch注解来提供JOIN FETCH的功能。


获取组(Fetch Group)

当一个实体对象有多个懒载入的域,那么在它们同一时候被需要时,JPA通常会为每个别需要的域生成并运行一条SQL语句。

显而易见的是,在这样的场景下,生成并运行一条SQL语句会更好。

然而。JPA标准中并未定义这样的行为。但是大多数JPA实现都定义了一个获取组来完毕这一行为。即将多个懒载入域定义成一个获取组,每次载入它们中的随意一个时,整个组都会被载入。

因此,当需要这样的行为时,可以參考详细JPA实现的文档。


批量处理和查询(Batching and Queries)

JPA也能像JDBC处理ResultSet那样处理查询的结果:

  • 一次性返回所有结果集中的所有记录
  • 每次获取结果集中的一条记录
  • 一次获取结果集中的N条记录(和JDBC的Fetch Size相似)

相同,这个Fetch Size也是和详细的JPA实现相关的,比方在EclipseLink和Hibernate中按例如如下的方式进行设置:

// EclipseLink
q.setHint("eclipselink.JDBC_FETCH_SIZE", "100000");

// Hibernate
@BatchSize
// Query here...

同一时候。可以对Query设置分页相关的设置:

Query q = em.createNamedQuery("selectAll");
query.setFirstResult(101);
query.setMaxResults(100);
List<? implements StockPrice> = q.getResultList();

这样就行只获取第101条到第200条这个区间的数据了。

同一时候。以上使用了命名查询(Named Query。createNamedQuery())而不是暂时查询(Ad-hoc Query。createQuery())。在很是多JPA实现中命名查询的速度要更快,因为一个命名查询会相应Statement Cache Pool中的一个PreparedStatement。剩下需要作的就仅仅是给该对象绑定參数。尽管对于暂时查询,也可使用相同的实现方式,仅仅只是此时的JPQL仅仅有在执行时才能够知晓。因此实现起来比較困难,在很是多JPA实现中会为暂时查询新建一个Statement对象。

总结

  1. JPA有一些优化选项可以限制(添加)单次数据库訪问的读取数据量。

  2. 对于BLOB和CLOB类型的字段,将它们的载入方式设置为懒载入。
  3. JPA实体的关联实体可以被设置为懒载入或者立刻载入。选择取决于应用的详细需求。

  4. 当需要立刻载入实体的关联实体时,可以结合命名查询和JOIN语句。注意它对于JPA缓存的影响。
  5. 使用命名查询比暂时查询更快。
相关文章
相关标签/搜索