前言
本文探討Jive(曾經(jīng)開(kāi)源的Java論壇)和Hibernate(Java開(kāi)源持久層)的數據庫對象的緩存策略,并闡述作者本人的Lightor(Java開(kāi)源持久層)采用的數據庫對象緩存策略。
本文的探討基于以前開(kāi)源的Jive代碼,Hibernate2.1.7源碼,和作者本人的Lightor代碼。
本文用ID (Identifier的縮寫(xiě))來(lái)代表數據記錄的關(guān)鍵字。
數據對象查詢(xún)一般分為兩種:條件查詢(xún),返回一個(gè)滿(mǎn)足條件的數據對象列表; ID查詢(xún),返回ID對應的數據對象。
本文主要探討“條件查詢(xún)”和“ID查詢(xún)”這兩種情況的緩存策略。
本文只探討一個(gè)JVM內的數據緩存策略,不涉及分布式緩存;本文只探討對應單表的數據對象的緩存,不涉及關(guān)聯(lián)表對象的情況。
一、Jive的緩存策略
1.Jive的緩存策略的過(guò)程描述:
(1)條件查詢(xún)的時(shí)候,Jive用 select id from table_name where …. (只選擇ID字段)這樣的SQL語(yǔ)句查詢(xún)數據庫,來(lái)獲得一個(gè)ID列表。
(2) Jive根據ID列表中的每個(gè)ID,首先查看緩存中是否存在對應ID的數據對象:如果存在,那么直接取出,加入到 結果列表中;如果不存在,那么通過(guò)一條select * from table_name where id = {ID value} 這樣的SQL查詢(xún)數據庫,取出對應的數據對象,放入到結果列表,并把這個(gè)數據對象按照ID放入到緩存中。
(3) ID查詢(xún)的時(shí)候,Jive執行類(lèi)似第(2)步的過(guò)程,先從緩存中查找該ID,查不到,再查詢(xún)數據庫,然后把結果放入到緩存。
(4) 刪除、更新、增加數據的時(shí)候,同時(shí)更新緩存。
2.Jive緩存策略的優(yōu)點(diǎn):
(1) ID查詢(xún)的時(shí)候,如果該ID已經(jīng)存在于緩存中,那么可以直接取出。節省了一條數據庫查詢(xún)。
(2) 當多次條件查詢(xún)的結果集相交的情況下,交集里面的數據對象不用重復從數據庫整個(gè)獲取,直接從緩存中獲取即可。
比如,第一次查詢(xún)的ID列表為{1, 2},然后根據ID列表的ID從數據庫中一個(gè)一個(gè)取出數據對象,結果集為{a(id = 1), b(id = 2)}。
下一次查詢(xún)的ID列表為{2, 3},由于ID = 2的數據對象已經(jīng)存在于緩存中,那么只要從數據庫中取出ID = 3的數據對象即可。
3.Jive緩存策略的缺點(diǎn):
(1) 在根據條件查找數據對象列表的過(guò)程中,DAO的第(1)步用來(lái)獲得ID列表的那一次數據庫查詢(xún),是必不可少的。
(2) 如果第(1)步返回的ID列表中有n個(gè)ID,在最壞的命中率(緩存中一個(gè)對應ID都沒(méi)有)情況下,Jive還要再查詢(xún)n次數據庫。最壞情況下,共需要n + 1數據庫查詢(xún)。
二、Hibernate的二級緩存策略
Hibernate用Session類(lèi)包裝了數據庫連接從打開(kāi)到關(guān)閉的過(guò)程。
Session內部維護一個(gè)數據對象集合,包括了本Session內選取的、操作的數據對象。這稱(chēng)為Session內部緩存,是Hibernate的第一級最快緩存,屬于Hibernate的既定行為,不需要進(jìn)行配置(也沒(méi)有辦法配置 :-)。
Session的生命期很短,存在于Session內部的第一級最快緩存的生命期當然也很短,命中率自然也很低。當然,這個(gè)Session內部緩存的主要作用是保持Session內部數據狀態(tài)同步。
如果需要跨Session的命中率較高的全局緩存,那么必須對Hibernate進(jìn)行二級緩存配置。一般來(lái)說(shuō),同樣數據類(lèi)型(Class)的數據對象,共用一個(gè)二級緩存(或其中的同一塊)。
1.Hibernate二級緩存策略的過(guò)程描述:
(1)條件查詢(xún)的時(shí)候,總是發(fā)出一條select * from table_name where …. (選擇所有字段)這樣的SQL語(yǔ)句查詢(xún)數據庫,一次獲得所有的數據對象。
(2) 把獲得的所有數據對象根據ID放入到第二級緩存中。
(3) 當Hibernate根據ID訪(fǎng)問(wèn)數據對象的時(shí)候,首先從Session一級緩存中查;查不到,如果配置了二級緩存,那么從二級緩存中查;查不到,再查詢(xún)數據庫,把結果按照ID放入到緩存。
(4) 刪除、更新、增加數據的時(shí)候,同時(shí)更新緩存。
2.Hibernate二級緩存策略的優(yōu)點(diǎn):
(1) 具有Jive緩存策略同樣的第(1)條優(yōu)點(diǎn):ID查詢(xún)的時(shí)候,如果該ID已經(jīng)存在于緩存中,那么可以直接取出。節省了一條數據庫查詢(xún)。
(2) 不具有Jive緩存策略的第(2)條缺點(diǎn),即hibernate不會(huì )有最壞情況下的 n + 1次數據庫查詢(xún)。
3.Hibernate二級緩存策略的缺點(diǎn):
(1) 同Jive緩存策略的第(1)條缺點(diǎn)一樣,條件查詢(xún)的時(shí)候,第(1)步的數據庫查詢(xún)語(yǔ)句是不可少的。而且Hibernate選擇所有的字段,比只選擇ID字段花費的時(shí)間和空間都多。
(2) 不具備Jive緩存策略的第(2)條優(yōu)點(diǎn)。條件查詢(xún)的時(shí)候,必須把數據庫對象從數據庫中整個(gè)取出,即使該數據庫的ID已經(jīng)存在于緩存中。
三、Hibernate的Query緩存策略
可以看到,Jive緩存和Hibernate的二級緩存策略,都只是針對于ID查詢(xún)的緩存策略,對于條件查詢(xún)則毫無(wú)作用。(盡管Jive緩存的第(2)個(gè)優(yōu)點(diǎn),能夠避免重復從數據庫獲取同一個(gè)ID對應的數據對象,但select id from …這條數據庫查詢(xún)是每次條件查詢(xún)都必不可少的)。
為此,Hibernate提供了針對條件查詢(xún)的Query緩存。
1.Hibernate的Query緩存策略的過(guò)程描述:
(1) 條件查詢(xún)的請求一般都包括如下信息:SQL, SQL需要的參數,記錄范圍(起始位置rowStart,最大記錄個(gè)數maxRows),等。
(2) Hibernate首先根據這些信息組成一個(gè)Query Key,根據這個(gè)Query Key到Query緩存中查找對應的結果列表。如果存在,那么返回這個(gè)結果列表;如果不存在,查詢(xún)數據庫,獲取結果列表,把整個(gè)結果列表根據Query Key放入到Query緩存中。
(3) Query Key中的SQL涉及到一些表名,如果這些表的任何數據發(fā)生修改、刪除、增加等操作,這些相關(guān)的Query Key都要從緩存中清空。
2.Hibernate的Query緩存策略的優(yōu)點(diǎn)
(1) 條件查詢(xún)的時(shí)候,如果Query Key已經(jīng)存在于緩存,那么不需要再查詢(xún)數據庫。命中的情況下,一次數據庫查詢(xún)也不需要。
3.Hibernate的Query緩存策略的缺點(diǎn)
(1) 條件查詢(xún)涉及到的表中,如果有任何一條記錄增加、刪除、或改變,那么緩存中所有和該表相關(guān)的Query Key都會(huì )失效。
比如,有這樣幾組Query Key,它們的SQL里面都包括table1。
SQL = select * from table1 where c1 = ? …., parameter = 1, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c1 = ? …., parameter = 1, rowStart = 21, maxRows = 20.
SQL = select * from table1 where c1 = ? ….., parameter = 2, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c1 = ? ….., parameter = 2, rowStart = 11, maxRows = 20.
SQL = select * from table1 where c2 = ? …., parameter = ‘abc’, rowStart = 11, maxRows = 20.
當table1的任何數據對象(任何字段)改變、增加、刪除的時(shí)候,這些Query Key對應的結果集都不能保證沒(méi)有發(fā)生變化。
很難做到根據數據對象的改動(dòng)精確判斷哪些Query Key對應的結果集受到影響。最簡(jiǎn)單的實(shí)現方法,就是清空所有SQL包含table1的Query Key。
(2) Query緩存中,Query Key對應的是數據對象列表,假如不同的Query Key對應的數據對象列表有交集,那么,交集部分的數據對象就是重復存儲的。
比如,Query Key 1對應的數據對象列表為{a(id = 1), b(id = 2)},Query Key 2對應的數據對象列表為{a(id = 1), c(id = 3)},這個(gè)a就在兩個(gè)List同時(shí)存在了兩份。
4.二級緩存和Query緩存同步的困惑
假如,Query緩存中,一個(gè)Query Key對應的結果列表為{a (id = 1) , b (id = 2), c (id = 3)}; 二級緩存里面有也id = 1對應的數據對象a。
這兩個(gè)數據對象a之間是什么關(guān)系?能夠保持狀態(tài)同步嗎?
我閱讀Hibernate的相關(guān)源碼,沒(méi)有發(fā)現兩個(gè)緩存之間的這種同步關(guān)系。
或者兩者之間毫無(wú)關(guān)系。就像我上面所說(shuō)的,只要表數據發(fā)生變化,相關(guān)的Query Key都要被清空。所以不用考慮同步問(wèn)題?
四、Lightor的緩存策略
Lightor是我做的Java開(kāi)源持久層框架。Lightor的意思是,Lightweight O/R。Hibernate,JDO,EJB CMP這些持久層框架,都是Layer。Lightor算不上Layer,而只是一個(gè)Helper。這里的O/R意思不是Object/Relational,而是Object/ResultSet的意思。:-)
Lightor的緩存策略,主要參照Hibernate的緩存思路,Lightor的緩存也分為 Query緩存和ID緩存。但其中有一點(diǎn)不同,兩者之間并不是毫無(wú)聯(lián)系的,而是相互關(guān)聯(lián)的。
1.Lightor的緩存策略的過(guò)程描述:
(1) 條件查詢(xún)的請求一般都包括如下信息:SQL, 對應SQL的參數,起始記錄位置(rowStart),最大記錄個(gè)數(maxRows),等。
(2) Lightor首先根據這些信息組成一個(gè)Query Key,根據這個(gè)Query Key到Query緩存中查找對應的結果ID列表。注意,這里獲取的是ID列表。
如果結果ID列表存在于Query緩存,那么根據這個(gè)ID列表的每個(gè)ID,到ID緩存中取對應的數據對象。如果所有ID對應的數據對象都找到,那個(gè)返回這個(gè)數據對象結果列表。注意,這里獲取的是整個(gè)數據對象(所有字段)的列表。
如果結果ID列表不存在于Query緩存,或者結果ID列表中的某一個(gè)ID不存在于ID緩存,那么,就查詢(xún)數據庫,獲取結果列表。然后,把獲取的每個(gè)數據對象按照ID放入到ID緩存;并組裝成一個(gè)ID列表,按照Query Key存放到Query緩存中。注意,這里是把ID列表,而不是整個(gè)對象列表,放入到Query緩存中。
(3) ID查詢(xún)的時(shí)候,Lightor先從ID緩存中查找該ID,如果不存在,那么查詢(xún)數據庫,把結果放入ID緩存。
(4) Query Key中的SQL涉及到一些表名,如果這些表的任何數據發(fā)生修改、刪除、增加等操作,這些相關(guān)的Query Key都要從緩存中清空。
2.Lightor的緩存策略的優(yōu)點(diǎn)
(1) Lightor的ID緩存具有Jive緩存,和Hibernate二級ID緩存的優(yōu)點(diǎn)。ID查詢(xún)的時(shí)候,如果該ID已經(jīng)存在于緩存中,那么可以直接取出。節省了一條數據庫查詢(xún)。
(2) Lightor的Query緩存具有Hibernate的Query緩存的優(yōu)點(diǎn)。條件查詢(xún)的時(shí)候,如果Query Key已經(jīng)存在于緩存,那么不需要再查詢(xún)數據庫。命中的情況下,一次數據庫查詢(xún)也不需要。
(3) Lightor的Query緩存中,Query Key對應的是ID列表,而不是數據對象列表,真正的數據對象只存在于ID緩存中。所以,不同的Query Key對應的ID列表如果有交集,ID對應的數據對象也不會(huì )在ID緩存中重復存儲。
(4) Lightor的緩存也沒(méi)有Jive緩存的最壞情況n + 1次數據庫查詢(xún)缺點(diǎn)。
3.Lightor的緩存策略的缺點(diǎn)
(1) Lightor的Query緩存具有Hibernate的Query緩存的缺點(diǎn)。條件查詢(xún)涉及到的表中,如果有任何一條記錄增加、刪除、或改變,那么緩存中所有和該表相關(guān)的Query Key都會(huì )失效。
(2) Lightor的ID緩存也具有hibernate的二級ID緩存具有的缺點(diǎn)。條件查詢(xún)的時(shí)候,即使ID已經(jīng)存在于緩存中,也需要重新把數據對象整個(gè)從數據庫取出,放入到緩存中。
五、Query Key的效率
Query緩存的Query Key的空間和時(shí)間開(kāi)銷(xiāo)比較大。
Query Key里面存放的東西不少,SQL, 參數,范圍(起始,個(gè)數)。
這里面最大的東西就是SQL。又占地方,又花時(shí)間(hashCode, equals)。
Query Key最關(guān)鍵的兩個(gè)方法是hashCode和equals,重點(diǎn)是SQL的hashCode和equals。
Lightor的做法是,由于Lightor直接使用SQL,不用HQL、OQL之類(lèi),所以推薦盡量使用static final String的SQL,能夠節省空間和時(shí)間,以至于Query Key的效率能夠相當于ID Key的效率。
至于Hibernate的QueryKey,有興趣的讀者可以去下載閱讀Hibernate的各個(gè)版本的源代碼,跟蹤一下QueryKey的實(shí)現優(yōu)化過(guò)程。
六、總結
這里列一個(gè)表,綜合表示Jive, Hibernate, Lightor的緩存策略的特征。
N + 1問(wèn)題
重復ID緩存問(wèn)題
Query緩存支持
Jive緩存
有
無(wú)
不支持
Hibernate緩存
無(wú)
有
支持
Lightor緩存
無(wú)
有
支持
注:
“重復ID緩存問(wèn)題”的含義是,每次條件查詢(xún),不是只取ID列表,而是取出完整對象(所有字段)的列表。這樣,同一個(gè)ID對應的數據對象,即使在緩存中已經(jīng)存在,也可能被重新放入緩存。參見(jiàn)相關(guān)緩存的缺點(diǎn)描述。
“重復ID緩存問(wèn)題”的負面效應到底有多大,就看你的select id from …(只選擇ID)比你的 select * from … (選擇所有字段)快多少。主要影響因素是,字段的個(gè)數,字段值的長(cháng)度,與數據庫服務(wù)器之間網(wǎng)絡(luò )傳輸速度。
不管怎么說(shuō),即使選擇所有字段,也只是一次數據庫查詢(xún)。而N + 1問(wèn)題帶來(lái)的可能最壞的負面效應(N + 1次數據查詢(xún))卻是非常大的。
選擇緩存策略的時(shí)候,應根據這些情況發(fā)生的概率和正負面效應進(jìn)行取舍。