[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. 使用命名查詢比暫時查詢更快。
相關文章
相關標籤/搜索