• [Java Performance] 数据库性能最佳实践


    数据库性能最佳实践

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

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

    演示样例数据库

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

    当中有两张表:STOCKPRICE和STOCKOPTIONPRICE。

    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.jar

    事务处理(Transaction Handling)

    JPA能够使用在Java SE和Java EE应用中。差别在于事务的处理方式。

    在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. 使用命名查询比暂时查询更快。

  • 相关阅读:
    Java 对文件的操作
    快速排序算法
    Java 时间和字符换的处理
    Redis 数据结构之Keys
    [转] Redis系统性介绍
    【转】JAVA 接口
    [转] Python 代码性能优化技巧
    几道关于面试的题目
    随手笔记2
    随手笔记
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5327738.html
Copyright © 2020-2023  润新知