數據庫性能最佳實踐
當應用須要連接數據庫時。那么應用的性能就可能收到數據庫性能的影響。
比方當數據庫的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中使用事務時一樣,在事務的范圍和事務的提交頻度上總會做出一些權衡,在下一節中會給出一些數據作為參考。
總結
- 在了解UMT的前提下,使用UMT進行事務的顯式管理會有更好的性能。
- 希望使用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 |
總結
- JPA應用和JDBC應用類似。限制對數據庫寫操作的次數可以提高性能。
- Statement緩存可以在JPA或者JDBC層實現,假設JDBC Driver提供了這個功能,優先在JDBC層實現。
- 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對象。
總結
- JPA有一些優化選項能夠限制(添加)單次數據庫訪問的讀取數據量。
- 對於BLOB和CLOB類型的字段,將它們的載入方式設置為懶載入。
- JPA實體的關聯實體能夠被設置為懶載入或者馬上載入。選擇取決於應用的詳細需求。
- 當須要馬上載入實體的關聯實體時,能夠結合命名查詢和JOIN語句。注意它對於JPA緩存的影響。
- 使用命名查詢比暫時查詢更快。