source:
http://www.ibm.com/developerworks/cn/data/library/techarticles/dm-0908renzg/index.html
關於 DB2 數據庫併發性的探討
DB2 數據庫本身所提供的機制對併發性的影響
文檔選項
打印本頁
將此頁作為電子郵件發送
級別: 初級
任之國 (renzhig@cn.ibm.com), 技術支持工程師, IBM
2009 年 8 月 06 日
OLTP 數據庫通常是高併發的使用模式,具有高的併發性對 OLTP 系統來説至關重要,併發事務實際上取決於資源的使用狀況,原則上應儘量減少對資源的鎖定時間,減少對資源的鎖定範圍,從而能夠儘量增加併發事務的數量,那麼影響併發的因素有哪些呢?這篇系列文章分 2 部分就這個內容進行一些探討,同時會盡量使用不同的工具(數據庫快照、事件監視器、控制中心等)來演示如何監控資源的使用狀況,讓大家在獲得知識的同時,也可以熟悉各種工具的使用。
概述
OLTP 數據庫通常是高併發的使用模式,具有高的併發性對 OLTP 系統來説至關重要,併發事務實際上取決於資源的使用狀況,原則上應儘量減少對資源的鎖定時間,減少對資源的鎖定範圍,從而能夠儘量增加併發事務的數量,那麼影響併發的因素有哪些呢?這篇系列文章之一將從 DB2 數據庫本身所提供的機制對併發性的影響進行一些探討,這些機制包括隔離級別、數據庫鎖參數、實例註冊表變量、樂觀鎖定。
同時本文討論及實驗所基於的環境如下,見圖 1:
圖 1. DB2 版本
設置 DB2 註冊表變量 DB2OPTIONS,將自動提交功能關閉,後面的實驗默認都基於這個設置。
db2set DB2OPTIONS=+c
db2 terminate
db2stop
db2start
回頁首
隔離級別與併發性
當多個用户訪問同一數據庫時會發生的現象介紹如下:
在單用户環境中,每個事務都是順序執行的,而不會遇到與其他事務的衝突。但是,在多用户環境下,多個事務可以(而且常常)同時執行。因此每個事務都有可能與其他正在運行的事務發生衝突。有可能與其他事務發生衝突的事務稱為交錯的 或並行的 事務,而相互隔離的事務稱為串行化 事務,這意味着同時運行它們的結果與一個接一個連續地運行它們的結果沒有區別。在多用户環境下,在使用並行事務時,會發生四種現象:
丟失更新:這種情況發生在兩個事務讀取並嘗試更新同一數據時,其中一個更新會丟失。例如:事務 1 和事務 2 讀取同一行數據,並都根據所讀取的數據計算出該行的新值。如果事務 1 用它的新值更新該行以後,事務 2 又更新了同一行,則事務 1 所執行的更新操作就丟失了。由於設計 DB2 的方法,DB2 不允許發生此類現象。
髒讀:當事務讀取尚未提交的數據時,就會發生這種情況。例如:事務 1 更改了一行數據,而事務 2 在事務 1 提交更改之前讀取了已更改的行。如果事務 1 回滾該更改,則事務 2 就會讀取被認為是不曾存在的數據。
不可重複的讀:當一個事務兩次讀取同一行數據,但每次獲得不同的數據值時,就會發生這種情況。例如:事務 1 讀取了一行數據,而事務 2 在更改或刪除該行後提交了更改。當事務 1 嘗試再次讀取該行時,它會檢索到不同的數據值(如果該行已經被更新的話),或發現該行不復存在了(如果該行被刪除的話)。
幻像:當最初沒有看到某個與搜索條件匹配的數據行,而在稍後的讀操作中又看到該行時,就會發生這種情況。例如:事務 1 讀取滿足某個搜索條件的一組數據行,而事務 2 插入了與事務 1 的搜索條件匹配的新行。如果事務 1 再次執行產生原先行集的查詢,就會檢索到不同的行集。
維護數據庫的一致性和數據完整性,同時又允許多個應用程序同時訪問同一數據,這樣的特性稱為併發性。 DB2 數據庫用來嘗試強制實施併發性的方法之一是通過使用隔離級別——通過‘事務、隔離級別、鎖’機制,它決定在第一個事務訪問數據時,如何對其他事務鎖定或隔離該事務所使用的數據。 DB2 使用下列隔離級別來強制實施併發性:
可重複的讀(Repeatable Read)
讀穩定性(Read Stability)
遊標穩定性(Cursor Stability)
未提交的讀(Uncommitted Read)
可重複的讀隔離級別可以防止所有現象,但是會大大降低併發性的程度(可以同時訪問同一資源的事務數量)。未提交的讀隔離級別提供了最大的併發性,但是後三種現象都可能出現。
DB2 UDB 支持以下隔離級別:可重複讀(Repeatable read,RR)、讀穩定性(Read stability,RS)、遊標穩定性(Cursor stability,CS)、未提交讀(Uncommitted read,UR),下面分別講述:
可重複讀(Repeatable read,RR) 確保 工作單元(UOW)期間的任何錶行讀操作直到 UOW 完成,
不會被其他應用程序進程更改。類似地,由另一個應用程序進程更改的任何行直到由該應用程序進程提交,不會被讀取。
運行在 RR 級別的應用程序進程是完全與併發應用程序進程的效果相隔離的。
RR 隔離級別通常會直接對錶加 S 鎖,所以對併發的影響最大,但有一種情況例外:如果 WHERE 條件字段上建有主鍵或者 UNIQUE INDEX,並且通過主鍵或者 UNIQUE INDEX 進行查詢,那麼數據庫將只對表加 IS 鎖,結果行加 S 鎖——在鎖列表足夠用,沒有發生鎖升級的情況下才成立,也就是説,這個時候 RR 級別 =RS 級別,這時不允許其他事務對這些行進行更新或者刪除,因為對行的更新或者刪除會對相應的行加 X 鎖,這和行 S 鎖相排斥;其他情況下,會直接對錶加 S 鎖,這時將不允許其他事務對任何行進行更新或者刪除。
下面我們做個實驗,驗證使用RR隔離級別的查詢對錶的加鎖情況。
表 1. 實驗使用的數據模型
表名 表結構 主鍵/
UNIQUE INDEX 數據規模
TEST_HAVE_PRI A 列上建有主鍵 10 行
TEST_NO_PRI 無主鍵和
UNIQUE INDEX 同樣的 10 行數據
使用 RR 隔離級別對 TEST_HAVE_PRI 表進行查詢,見圖 2:
圖 2. 執行查詢的 session1 窗口
可以看到表 TEST_HAVE_PRI 上加了 IS 鎖,行上加了 S 鎖,這是通過主鍵查詢時鎖的使用情況,見圖 3:
圖 3. 快照監控窗口
通過主鍵字段查詢後,被 RR 隔離級別鎖定的行,不能夠被其他事務更新(UPDATE/DELETE),session2 中的 UPDATE 事務將被鎖住,見圖 4:
圖 4. 執行 UPDATE 事務的 session2 窗口
通過 DB2 控制中心工具可以查看鎖定鏈條——即誰鎖定了誰,打開‘應用程序’窗口,可以看到選中的應用程序狀態為‘正在等待鎖定’——就是 session2 中的事務的狀態,見圖 5:
圖 5. 控制中心應用程序界面
點擊‘顯示鎖定鏈’按鈕,進入下一窗口,下圖如何解釋呢?
圖 6. 應用程序鎖定鏈界面
請點擊‘圖注’按鈕,先了解一下各種圖形元素所代表的意思,見圖 7:
圖 7. 圖注界面
根據圖 7 所示,圖 6 所表示的意思為:下方框事務正在等待上方框事務釋放鎖定,也就是 session2 中的事務正在等待 session1 中的事務釋放鎖定。
更詳細的信息,可以通過右鍵點擊方框,選擇‘顯示鎖定詳細信息’菜單,見圖 8:
圖 8. 應用程序鎖定鏈界面
下圖為 session1 中的查詢事務獲得的鎖的詳細信息——獲得一個表 IS 鎖、一個行 S 鎖,見圖 9:
圖 9. 鎖定詳細信息界面
圖 10 為 session2 中的更新事務獲得的鎖的詳細信息,這個事務處於正在等待鎖定 狀態,獲得了一個表 IX 鎖,同時想要獲得行 X 鎖,但是不成功,於是發生了鎖等待,與句柄為 901 的代理程序(即運行查詢事務的 session1 應用)中的行 S 鎖衝突(見圖 9),所以只能等待,直到句柄為 901 的代理程序提交或者回滾工作單元,才能真正獲得行 X 鎖。
圖 10. 鎖定詳細信息界面
表中其他的行可以被其他事務更新,見圖 11:
圖 11. 執行 UPDATE 操作的 session3 窗口
使用 RR 隔離級別對 TEST_NO_PRI 表進行查詢,讓我們看看不通過主鍵字段查詢時鎖的使用情況,見圖 12:
圖 12. 執行查詢的 session1 窗口
可以看到表 TEST_NO_PRI 上直接加了 S 鎖,其他任何事務不能對此表做任何更新操作 (DELETE/UPDATE/INSERT),見圖 13:
圖 13. 快照監控窗口
讀穩定性(Read stability,RS)類似於 RR 。但是,運行在 RS 級別的應用程序進程不是完全與併發應用程序
進程的效果相隔離的。如果這樣的應用程序進程不止一次發出同樣的查詢,它就會看到更改了的數據或者由其他
應用程序進程添加的新的“幻影(phantom)”行。
RS 隔離級別會對錶加 IS 鎖,結果行加 NS 鎖,這時不允許其他事務對這些行進行更新(UPDATE/DELETE),但是允許插入任何行,因為對這些行的更新會對相應的行加 X 鎖,這和行 NS 鎖相排斥——上述説法基於鎖列表足夠用,沒有發生鎖升級的情況下才成立。
下面我們做個實驗,驗證使用RS隔離級別的查詢對錶的加鎖情況:
表 2. 實驗使用的數據模型
表名 表結構 索引
TEST A 列上建有索引
使用 RS 隔離級別對 TEST 表查詢,見圖 14:
圖 14. 執行查詢的 session1 窗口
可以看到表 TEST 上加了 IS 鎖,行上加了 NS 鎖(存在 32768 個行 NS 鎖,這裏不一一列舉),見圖 15:
圖 15. 快照監控窗口
預更新(UPDATE/DELETE)這些行的事務將被鎖住,見圖 16、圖 17:
圖 16. 執行 UPDATE 事務的 session2 窗口
圖 17. 執行 DELETE 事務的 session3 窗口
但是可以插入任何行,見圖 18:
圖 18. 執行 INSERT 事務的 session4 窗口
遊標穩定性(Cursor stability,CS)也確保由另一個應用程序進程更改的任何行直到被那個應用程序進程提交,
不會被讀取。 CS 隔離級別只確保每個可更新遊標的當前行不被其他應用程序進程更改;
在 UOW 期間讀過的行可以被其他應用程序進程更改。針對可滾動更新遊標,在提交之前,會對所有結果行一直加 U 鎖,
無論遊標滾動到什麼地方; CS 隔離級別針對不可更新遊標會對錶加 IS 鎖,如果未在 WHERE 條件字段上創建索引,
查詢首先會查找鎖列表,檢查鎖列表中是否存在與 IS 鎖相排斥的鎖,如果存在的話,
那麼將等待所有持有排斥鎖的事務提交,查詢才能執行下去;如果 WHERE 條件字段創建了索引,並且使用了索引,
那麼查詢將通過索引得到結果行,然後檢查鎖列表中是否存在與結果行相排斥的鎖,如果存在的話,
那麼將等待所有持有排斥鎖的事務提交,查詢才能執行下去 .
下面我們做個實驗,驗證使用CS隔離級別的查詢對錶的加鎖情況:
使用 CS 隔離級別的可更新遊標對 TEST_HAVE_PRI 表進行查詢,並 FETCH 第 1 行(見圖 19),使用的數據模型見表1:
圖 19. 執行查詢事務的 session1 窗口
可以看到表 TEST_HAVA_PRI 上加了 IX 鎖,被取的當前行 (a= ’ 1 ’ ) 加了 U 鎖,符合條件的其他行暫時不加鎖,見圖 20:
圖 20. 快照監控窗口
這時其他事務不可以對這行進行更新操作 (UPDATE/DELETE) ——因為行上的 U 鎖與 X 鎖排斥,但是可以對其他行進行更新(比如對 a= ’ 3 ’這行),見圖 21、圖 22:
圖 21. 執行 UPDATE 事務的 session2 窗口
圖 22. 執行 UPDATE 事務的 session3 窗口
轉到 session1 窗口,繼續 FETCH 下一行,這時將釋放前一行 (a= ’ 1 ’ ) 上的 U 鎖,在當前行上 (a= ’ 2 ’ ) 加 U 鎖,見圖 23:
圖 23. FETCH 下一行
轉到 session2 窗口,可以看到之前被鎖住的 UPDATE 語句已經執行成功,見圖 24:
圖 24. 執行 UPDATE 事務的 session2 窗口
在 session4 窗口對 a= ’ 2 ’這行進行 UPDATE 操作,可以預見將被鎖住,見圖 25:
圖 25. 執行 UPDATE 事務的 session4 窗口
轉到 session1 窗口,繼續 FETCH 下一行,將釋放 a= ’ 2 ’這行上的 U 鎖(session4 中對 a= ’ 2 ’這行的 UPDATE 事務將成功執行,為什麼呢?),將要在 FETCH 的下一行上加 U 鎖,發現 session1 中的遊標 FETCH 操作被鎖住,這又為什麼呢?
因為 session1 中的事務釋放了 a= ’ 2 ’上的 U 鎖,所以 session4 中被鎖住的針對 a= ’ 2 ’這條記錄進行的 UPDATE 事務能夠執行成功,見圖 26:
圖 26. 執行 UPDATE 事務的 session4 窗口
為什麼 session1 中的事務(FETCH 下一條記錄)被鎖住呢?是因為這次恰好要 FETCH a= ’ 3 ’這條記錄,需要在行上加 U 鎖,這與 session3 中的 update 事務對這行所加的 X 鎖相排斥,所以 session1 窗口的 FETCH 下一行操作發生了鎖等待,直到 session3 中的事務提交或者回滾為止,見圖 27:
圖 27. 執行 FETCH 操作的 session1 窗口
未提交讀(Uncommitted read,UR)對於某些操作,允許在 UOW 期間讀過的任何行可以被其他應用程序進程
更改,並允許讀任何被另一個應用程序進程更改過的行,即使該更改還沒有提交。對於其他操作,UR 類似於 CS 。
下面我們做個實驗,驗證使用UR隔離級別的查詢是不會獲得任何級別鎖的:
表 3. 實驗使用的數據模型
表名 表結構 索引
TEST_NO_IND 沒有索引
採用 UR 隔離級別查詢 TEST_NO_IND 表,同時運行快照,可見,使用 UR 隔離級別查詢時,對錶 TEST_NO_IND 加了 IN 鎖,IN 就是 intent none 意思,就是不加任何鎖,見圖 28、圖 29:
圖 28. 執行查詢事務的 session1 窗口
圖 29. 快照監控窗口
綜上所述,離級別對併發性具有最顯著的影響,不同隔離級別獲得的資源的鎖定範圍也不同,如果所有事務都能做到不過分貪婪的佔有鎖資源——鎖的範圍大、佔用時間長,那麼事務之間發生鎖衝突的可能性將大大降低,事務的併發性也將會很好。那麼如何選擇正確的隔離級別呢?
使用的隔離級別不僅影響數據庫對併發性的支持如何,而且影響併發應用程序的性能。通常,使用的隔離級別越嚴格,併發性就越小,某些應用程序的性能可能會越低,因為它們要等待資源上的鎖被釋放。那麼,如何決定要使用哪種隔離級別呢?最好的方法是確定哪些現象是不可接受的,然後選擇能夠防止這些現象發生的隔離級別:
如果正在執行大型查詢,而且不希望併發事務所做的修改導致查詢的多次運行返回不同的結果,則使用可重複的讀隔離級別。
如果希望在應用程序之間獲得一定的併發性,還希望限定的行在事務執行期間保持穩定,則使用讀穩定性隔離級別。
如果希望獲得最大的併發性,同時不希望查詢看到未提交的數據,則使用遊標穩定性隔離級別。
如果正在只讀的表 / 視圖 / 數據庫上執行查詢,或者並不介意查詢是否返回未提交的數據,則使用未提交的讀隔離級別。 對於統計類報表,不需要得到十分精確的數據,那麼最好使用 UR 隔離級別,既可以節省昂貴的鎖列表資源,也不會因為鎖衝突影響其他事務的執行,同時也不會受到其他事務的影響,順利的得到統計結果。未提交的讀隔離級別通常用於那些訪問只讀表和視圖的事務,以及某些執行 SELECT 語句的事務(只要其他事務的未提交數據對這些語句沒有負面效果)。 顧名思義,其他事務對行所做的更改在已經提交之前對於使用未提交的讀隔離級別的事務是可見的。但是,此類事務不能看見或訪問其他事務所創建的表、視圖或索引,直到那些事務被提交為止。類似地,如果其他事務刪除了現有的表、視圖或索引,那麼僅當進行刪除操作的事務終止時,使用未提交的讀隔離級別的事務才能知道這些對象不再存在了。(一定要注意一點:當運行在未提交的讀隔離級別下的事務使用可更新遊標時,該事務的行為和在遊標穩定性隔離級別下運行一樣,並應用遊標穩定性隔離級別的約束。)
回頁首
數據庫鎖參數與併發性
當應用程序掛起的鎖定總數達到可供應用程序使用的最大鎖定列表空間量時,鎖定將會升級,將影響到應用程序併發性。可用鎖定列表空間量由 locklist 和 maxlocks 配置參數確定。
當應用程序達到允許的最大鎖定數並且沒有其他要升級的鎖定時,它將使用鎖定列表中為其他應用程序分配的空間。當整個鎖定列表已滿時,將發生錯誤。
以下幾種原因可能會導致產生過量鎖定升級:
鎖定列表大小(locklist)對於並行應用程序數目而言可能太小
可供每個應用程序使用的鎖定列表百分比(maxlocks)可能太小
一個或多個應用程序使用的鎖定數可能過量。
要解決這些問題,可以:
增加 locklist 配置參數值。
增加 maxlocks 配置參數值。
標識具有大量鎖定(請參閲locks_held_top監視器元素)的應用程序,或藉助以下公式並將該值與maxlocks進行比較以標識在鎖定列表中掛起過量鎖定的應用程序:
(((locks held * 36) ⁄ (locklist * 4096)) * 100)
這些應用程序還可能因為在鎖定列表中使用過量資源而導致其他應用程序中發生鎖定升級。這些應用程序可能需要求助於使用表鎖定(而不是行鎖定),儘管表鎖定可能導致lock_waits和lock_wait_time增加。
鎖參數(LOCKLIST、MAXLOCKS 和 LOCKTIMEOUT)背景知識
這些與鎖相關的控制都是數據庫配置參數:
LOCKLIST表明分配給鎖列表的存儲容量。每個數據庫都有一個鎖列表,鎖列表包含了併發連接到該數據庫的所有應用程序所持有的鎖。鎖定是數據庫管理器用來控制多個應用程序併發訪問數據庫中數據的機制。行和表都可以被鎖定。根據對象是否還持有其它鎖,每把鎖需要的鎖列表字節數不一樣:
在 32 位平台上,每個鎖需要 48 或 96 字節的鎖定列表,這取決於是否對該對象掛起了其他鎖:
對於沒有掛起其他鎖的對象,掛起一個鎖需要96字節
對於已經掛起了鎖的對象,記錄一個鎖需要48字節
在64位平台(HP-UX/PA-RISC除外)上,每個鎖需要64或128字節的鎖定列表,這取決於在該對象上是否掛起了其他鎖定:
對於沒有掛起其他鎖定的對象,掛起一個鎖定需要128字節
對於存在一個掛起的鎖定的對象,記錄一個鎖定需要64字節。
MAXLOCKS定義了應用程序持有的鎖列表的百分比,在數據庫管理器執行鎖升級之前必須填充該鎖列表。當一個應用程序所使用的鎖列表百分比達到MAXLOCKS時,數據庫管理器會升級這些鎖,這意味着用表鎖代替行鎖,從而減少列表中鎖的數量。當任何一個應用程序所持有的鎖數量達到整個鎖列表大小的這個百分比時,對該應用程序所持有的鎖進行鎖升級。如果鎖列表用完了空間,那麼也會發生鎖升級。數據庫管理器通過查看應用程序的鎖列表並查找行鎖最多的表,來決定對哪些鎖進行升級。如果用一個表鎖替換這些行鎖,將不再會超出MAXLOCKS值,那麼鎖升級就會停止。否則,鎖升級就會一直進行,直到所持有的鎖列表百分比低於MAXLOCKS。MAXLOCKS參數乘以MAXAPPLS參數不能小於100。
雖然升級過程本身並不用花很多時間,但是鎖定整個表(相對於鎖定個別行)降低了併發性,而且數據庫的整體性能可能會由於對受鎖升級影響的表的後續訪問而降低。
下面是一些控制鎖列表大小的建議:
經常進行提交以釋放鎖。
當執行大量更新時,更新之前,在整個事務期間鎖定整個表(使用SQL LOCK TABLE語句)。這隻使用了一把鎖從而防止其它事務妨礙這些更新,但是對於其他用户它的確減少了數據併發性。
使用alter TABLE語句的LOCKSIZE參數控制如何在持久基礎上對某個特定表進行鎖定。
查看應用程序使用的隔離級別。使用可重複讀隔離級別在某些情況下可能會導致自動執行表鎖定。當有可能減少所持有共享鎖的數量時,可以使用遊標穩定性(Cursor Stability)隔離級別。如果沒有損害應用程序完整性需求,那麼可以使用未提交的讀隔離級別而不是遊標穩定性隔離級別,以進一步減少鎖的數量。
使用下列步驟確定數據庫鎖列表所需的頁數:
計算鎖列表大小的下限:(512 * 32 * MAXAPPLS) / 4096,其中512是每個應用程序平均所含鎖數量的估計值,32是對象(已有一把鎖)上每把鎖所需的字節數。
計算鎖列表大小的上限:(512 * 64 * MAXAPPLS) / 4096,其中64是某個對象上第一把鎖所需的字節數。
對於您的數據,估計可能具有的併發數,並根據您的預計為鎖列表選擇一個初始值,該值位於您計算出的上限和下限之間。
使用數據庫系統監視器調優MAXLOCKS值。
設置MAXLOCKS時,請考慮鎖列表的大小(LOCKLIST):
MAXLOCKS = 100 * (512鎖/應用程序* 32字節/鎖* 2) / (LOCKLIST * 4096字節)
該樣本公式允許任何應用程序持有的鎖是平均數的兩倍。如果只有幾個應用程序併發地運行,則可以增大MAXLOCKS,因為在這些條件下鎖列表空間中不會有太多爭用。
LOCKTIMEOUT指定了應用程序為獲取鎖所等待的秒數。這有助於應用程序避免全局死鎖。
如果將該參數設置成0,那麼應用程序將不等待獲取鎖。在這種情形中,如果請求時沒有可用的鎖,那麼應用程序立刻會接收到-911。
如果將該參數設置成-1,那麼將關閉鎖超時檢測。在這種情形中,應用程序將等待獲取鎖(如果請求時沒有可用的鎖),一直到被授予了鎖或出現死鎖為止。
要更改鎖參數,請運行以下命令:
db2 -v update db cfg for DB_NAME using LOCKLIST a_number
db2 -v update db cfg for DB_NAME using MAXLOCKS b_number
db2 -v update db cfg for DB_NAME using LOCKTIMEOUT c_number
db2 -v terminate
一旦鎖列表滿了,由於鎖升級生成更多的表鎖和更少的行鎖,因此減少了數據庫中共享對象的併發性,從而降低了性能。另外,應用程序間可能會發生更多死鎖(因為它們都等待數量有限的表鎖),這會導致事務被回滾。當數據庫的鎖請求達到最大值時,應用程序將接收到值為-912的SQLCODE。如果鎖升級造成併發性方面的問題,則可能需要增大LOCKLIST參數或MAXLOCKS參數的值。可以使用數據庫系統監視器來確定是否發生鎖升級,跟蹤應用程序(連接)遭遇鎖超時的次數,或者數據庫檢測到的所有已連接應用程序的超時情形。
首先,運行下面這個命令以打開針對鎖的DB2監視器:
db2 -v update monitor switches using lock on
db2 -v terminate
然後收集數據庫快照:
db2 -v get snapshot for database on DB_NAME
在快照輸出中,檢查下列各項:
Locks held currently = 0
Lock waits = 0
Time database waited on locks (ms) = 0
Lock list memory in use (Bytes) = 504
Deadlocks detected = 0
Lock escalations = 0
Exclusive lock escalations = 0
Agents currently waiting on locks = 0
Lock Timeouts = 0
如果“Lock list memory in use (Bytes) ”超過定義的LOCKLIST大小的50%,那麼就增加LOCKLIST數據庫配置參數中的4KB頁的數量。鎖升級、鎖超時和死鎖將表明系統或應用程序中存在某些潛在問題。鎖定問題通常表明應用程序中存在一些相當嚴重的併發性問題,在增大鎖列表參數的值之前應當解決這些問題。
避免死鎖
從事務雙方的鎖定關係上來講有死鎖和活鎖 2 種,所謂活鎖:舉例來説可能會存在事務 A 在等待事務 B 釋放所佔用的鎖資源,但是事務 B 並不等待事務 A 釋放所佔用的鎖資源,所謂死鎖:舉例來説可能會存在事務 A 在等待事務 B 釋放所佔用的鎖資源,同時事務 B 在等待事務 A 釋放所佔用的鎖資源,也就是説,事務之間互相等待,如果沒有合理設置檢查死鎖的時間間隔 (DLCHKTIME) 參數,可能會導致相關事務雙方永遠等待下去,DLCHKTIME 參數默認設置為 10000 毫秒——即 10 秒,也就是説每隔 10 秒鐘,死鎖檢測進程會自動檢查數據庫範圍內有無死鎖存在,如果發現了死鎖存在,那麼將隨機挑選一個事務並強制終止它,這個事務將被回滾,那麼這時另外一個事務將可以順利執行下去。
總的來説,死鎖可能是由下列情況導致的:
數據庫發生鎖定升級
在系統生成的行鎖定已足夠的情況下應用程序顯式鎖定了表
綁定時應用程序使用了不適當的隔離級別
目錄表已被鎖定以供可重複讀
應用程序正以不同的順序獲取相同的鎖定,從而導致死鎖。
回頁首
實例註冊表變量與併發性
從DB2 V8以後陸續引入了三個註冊表變量:DB2_EVALUNCOMMITTED、DB2_SKIPDELETED、DB2_SKIPINSERTED,為什麼要引入這三個變量呢?在DB2沒有這三個變量前,如果一個用户正在更改一行數據,那麼DB2會在這一行加上排他鎖,別的用户不能訪問,除非使用UR隔離級別。
DB2為了改善應用程序併發性,從DB2 V8以後就陸續引入了這三個變量。這三個變量並不會改變鎖的本質,只不過通過了解它們的工作方式和機制可以使我們根據我們的業務邏輯來合理的設置調整以提高應用程序併發性。這三個變量改變加鎖的時機 , 減少鎖衝突 ( 這樣其行上的 insert/update/delete 操作不會鎖住未命中的 select 操作 )
註解:
db2set DB2_EVALUNCOMMITTED=ON
db2set DB2_SKIPDELETED=ON
db2set DB2_SKIPINSERTED=ON
db2set DB2_EVALUNCOMMITTED=ON - 這個參數將在記錄鎖之前進行謂詞檢查,儘量減少鎖的時間
db2set DB2_SKIPINSERTED=ON - 這個參數將新 insert 且沒有提交的數據跳過;例如,SELECT/UPDATE 語句不會發現這條記錄
db2set DB2_SKIPDELETED=ON - 這個參數將新 delete 且沒有提交的數據跳過;例如,SELECT/UPDATE 語句不等待這條記錄的提交,並且認為他已經被刪除了
下面我們通過實驗來演示這三個變量是如何影響SQL語句的行為的:
表 4. 實驗使用的數據模型
表名 表結構 數據
TEST 包含如下 10 行數據:
'1', '1'、'2', '2'、'3', '3'、'4', '4'、'5','5'、'6','6'、'7','7'、'8', '8'、'9','9'、'10','10'
在 session1 中插入一條記錄 ('11','11') 到 TEST 表中,見圖 30:
圖 30. 執行 INSERT 事務的 session1 窗口
在 session2 中根據條件 a= ’ 1 ’ or a= ’ 11 ’查詢 TEST 表,session2 被鎖住,為什麼呢?這是因為 session1 中 INSERT 事務還沒有 COMMIT,所以這個時候 session 2 處於鎖定等待 狀態,見圖 31、圖 32:
圖 31. 執行查詢事務的 session2 窗口
圖 32. 快照監控窗口
正常情況下上述事務的加鎖機制就是這樣的,但是有時候用户希望這個時候能夠查詢到數據,那麼怎麼解決這個矛盾呢?下面我們來仔細講解這三個變量。
設置 DB2_EVALUNCOMMITTED
DB2 V8.1.4 版本中首次引入了 DB2_EVALUNCOMMITTED 這個 DB2 註冊表變量。當它被啓用(=TRUE | ON | YES | 1)時,它將修改 DB2 中只讀查詢的行為,使之允許在索引掃描(必須是 Type 2 索引)或表訪問時推遲鎖,直到限定語句的所有謂詞都是已知的。引入這個新的註冊表變量是為了可選地提高一些應用程序的併發性,其實質是允許讀掃描推遲或避免行鎖,直到適合特定查詢的一個數據記錄成為已知。
在 DB2 V8.1.4 之前(並且沒有設置這個註冊表變量),DB2 將執行保守式的鎖:在驗證行是否滿足查詢的排除謂詞之前,它將鎖定每個被訪問的行,不管數據行是否被提交落實,以及根據語句的謂詞它是否被排除,對於索引掃描和表訪問都執行這樣的鎖定操作。
下面我們舉一個簡單的例子來演示:
表 5. 實驗使用的數據模型
表名 表結構 數據 備註
TEST 包含如下 10 行數據:
'1', '1'、'2', '2'、'3', '3'、'4', '4'、'5','5'、'6','6'、'7','7'、'8', '8'、'9','9'、'10','10' 暫時未建索引
現在有兩個session發出了下面的SQL語句,見圖33、圖34:
圖 33. 執行 INSERT 事務的 session1 窗口
圖 34. 執行查詢事務的 session2 窗口
我們查看session 2的狀態,可以看見session2處於鎖定等待 狀態,見圖 35:
圖 35. 快照監控窗口
session1 執行 DB2 INSERT INTO TABLE TEST VALUES ('11', '11') 將阻塞所有其他的掃描器掃描它,因為它持有行上的 X 鎖,所以 session2 執行 DB2 SELECT * FROM TEST 將被阻塞,直到 session1 提交或回滾。但是我們假設 session2 執行的語句是 DB2 SELECT * FROM TEST WHERE id='10' ,在此情況下,即使 session2 中事務 與 session1 中事務未提交的任何值沒有關係,它也仍將被阻塞,處於鎖定等待 狀態。在 DB2 中,默認情況下將發生這一系列的事件,因為默認的隔離級別是 CS,這種隔離級別表明,一個查詢訪問的任何一行在遊標定位到該行時都必須被鎖定。在 session1 釋放它用於更新(INSERT)表 TEST 的鎖之前,session2 不能包含表 TEST 第一行上的鎖。如果 DB2 事先能夠知道值 A='11' 不是 session2 的數據請求的一部分(換句話説,它在鎖行之前計算了謂詞),就可以避免阻塞,這是合情合理的,現在 DB2 數據庫已經支持這種行為,通過啓用 DB2_EVALUNCOMMITTED 註冊變量實現,該實例變量設置後需要重啓實例,見圖 36:
圖 36. 設置實例註冊表變量 DB2_EVALUNCOMMITTED
DB2_EVALUNCOMMITTED 變量影響 DB2 在遊標穩定性(CS)和讀穩定性(RS)隔離級別下的行鎖機制。當你啓用該功能時,DB2 可以對於未提交的更新數據(INSERT/UPDATE)事先進行謂詞判斷,如果未提交數據不符合該條語句的謂詞判斷條件,DB2 將不對未提交數據加鎖,這樣就避免了因為要對未提交數據加鎖而引起的鎖等待狀態,提高了應用程序訪問的併發性,同時 DB2 在進行表掃描時,會無條件地忽略被刪除的行數據(不管是否滿足謂詞判斷條件,不管是否提交)。這樣一定程度上緩解了鎖的問題,不會因為 INSERT/UPDATE/DELETE 一條記錄而可能造成整個表被鎖住。
下面我們通過一個實驗來演示:
實驗使用的數據模型見表 5:
現在有 6 個 session 窗口按照下面的命令序列做實驗:
session1 窗口:db2 delete from test where a='1'
session2 窗口:db2 select * from test
session1 窗口:db2 rollback
session2 窗口:db2 rollback
session3 窗口:db2 insert into test values('11', '11')
session4 窗口:db2 select * from test where a='10'
session3 窗口:db2 rollback
session4 窗口:db2 rollback
session5 窗口:db2 update test set a='100' where a='1'
session6 窗口:db2 select * from test where a='10'
session5 窗口:db2 rollback
session6 窗口:db2 rollback
在未設置DB2_EVALUNCOMMITTED=ON時,session2/session4/session6肯定都將處於鎖等待 狀態的,現在我們設置了DB2_EVALUNCOMMITTED=ON後,我們來看看session2/session4/session6能否檢索到數據,通過這個實驗我們發現當啓用 DB2_EVALUNCOMMITTED=ON 時,對於delete/insert/update操作的處理,DB2 在進行表掃描時會無條件地忽略被刪除的記錄(不管是否滿足謂詞判斷條件,不管是否提交),見圖 37、圖 38,而對於未提交的更新數據(INSERT/UPDATE)會事先進行謂詞判斷,如果未提交的記錄不符合該條語句的謂詞判斷條件,DB2 將不對未提交記錄加鎖,這樣就避免了因為要對未提交記錄加鎖而引起的查詢事務鎖等待狀態,見圖 39、圖 40、圖 41、圖 42:
圖 37. 執行 DELETE 事務的 session1 窗口
圖 38. 執行查詢事務的 session2 窗口
在 session1/session2 窗口使用 db2 rollback 命令將實驗場景恢復原狀。
圖 39. 執行 INSERT 事務的 session3 窗口
圖 40. 執行查詢事務的 session4 窗口
在 session3/session4 窗口使用 db2 rollback 命令將實驗場景恢復原狀。
圖 41. 執行 UPDATE 事務的 session5 窗口
圖 42. 執行查詢事務的 session6 窗口
在 session5/session6 窗口使用 db2 rollback 命令將實驗場景恢復原狀。
現在在TEST表上創建一個type-2的索引(在字段A上創建索引),然後再來做剛才的那個實驗:
我們發現session2處於鎖等待狀態(見圖 44),為什麼呢?
當您的DB2環境中啓用了evaluate uncommitted行為時,您應該清楚,謂詞計算可能發生在未提交的數據上。我們知道在表掃描訪問中,被刪除行被無條件忽略,而對於使用type-2索引進行掃描,被刪除的鍵不會被忽略(除非您還設置了DB2_SKIPDELETED註冊表變量,DB2_SKIPDELETED變量我們稍後介紹),實驗見圖43、圖44、圖45;如果您要在環境中單獨設置DB2_SKIPDELETED註冊表變量,DB2將也允許在表掃描訪問時無條件地忽略被刪除行,並忽略通過type-2索引掃描訪問的偽刪除索引鍵。
圖 43. 執行 DELETE 事務的 session1 窗口
圖 44. 執行查詢事務的 session2 窗口
圖 45. 快照監控窗口
設置 DB2_SKIPDELETED
DB2_SKIPDELETED=ON 該變量被啓用時,將允許使用 CS 或 RS 隔離級別的語句在索引掃描期間無條件地跳過被刪除的鍵,而在表訪問期間則無條件地跳過被刪除的行。當 DB2_EVALUNCOMMITTED 被啓用時,被刪除的行會被自動跳過,但是除非同時啓用了 DB2_SKIPDELETED,否則 type-2 索引中未提交的偽刪除鍵不會被跳過。
在上面的實驗中,我們發現當我們僅僅設置了 DB2_EVALUNCOMMITTED 變量時,如果表上有 type-2 索引,那麼在我們通過索引讀取數據時,被刪除的索引鍵不會被忽略。這種情況下如果你希望跳過被刪除的鍵,可以通過設置 DB2_SKIPDELETED=ON 來實現。
下面我們做個實驗來演示一下:
首先打開實例註冊表變量 DB2_SKIPDELETED,見圖 46:
圖 46. 設置實例註冊表變量 DB2_SKIPDELETED=ON
重新做剛才的實驗,我們可以看到在設置DB2_SKIPDELETED=ON後,即使test表上有type-2的索引,那麼在掃描type-2索引的時候仍然忽略這個被刪除的行。見圖47、圖48:
圖 47. 執行 DELETE 事務的 session1 窗口
圖 48. 執行查詢事務的 session2 窗口
設置 DB2_SKIPINSERTED
雖然當 SELECT 語句由於一個未提交的 INSERT 操作而被鎖住的這種行為是正確的 —— 但是有些特殊情況下希望 DB2 忽略正在等待提交的被插入的行,就好像它沒有發生一樣,這可以通過設置 DB2_SKIPINSERTED 註冊表變量來達到這種目的。 DB2_SKIPINSERTED=OFF 是默認設置,這使得 DB2 的行為和預期的一樣:SELECT 事務一直等到 INSERT 事務提交或回滾,然後返回數據;如果設置 DB2_SKIPINSERTED=ON,那麼 SELECT 事務將忽略尚未提交的 INSERT 事務(只對於 CS 和 RS 隔離級別),不論表上是否存在索引。該特性增加了併發性,同時又不犧牲隔離語義。
下面我們來看設置DB2_SKIPINSERTED變量前後的例子:
表 6. 實驗使用的數據模型
表名 表結構 數據 備註
TEST 包含如下 10 行數據:
'1', '1'、'2', '2'、'3', '3'、'4', '4'、'5','5'、'6','6'、'7','7'、'8', '8'、'9','9'、'10','10' 未建索引
雖然之前已經打開了 DB2_EVALUNCOMMITTED 註冊表變量,但是在打開 DB2_SKIPINSERTED 註冊表變量前,下面的 session2 查詢事務將處於 鎖定等待 狀態,為什麼呢?因為 session1 中的 INSERT 事務插入了 1 條記錄 '11', '11',而 session2 查詢事務事先根據謂詞條件判斷 session1 中插入的記錄在條件範圍之內,所以 session2 被鎖住;然而 session3 查詢事務卻可以正常執行(也是通過全表掃描,因為未建索引),因為 session3 查詢事務事先根據謂詞條件判斷 session1 中插入的記錄不在條件範圍之內(因為已經打開了DB2_EVALUNCOMMITTED變量,所以可以提前判斷未提交的INSERT/UPDATE事務所涉及到的記錄是否在謂詞條件範圍之內,如果不在條件範圍之內的話,DB2將不對未提交的記錄加鎖,這樣就避免了因為要對未提交記錄加鎖而引起的查詢事務鎖等待狀態),所以 session3 查詢事務可以正常執行,見圖 49、圖 50、圖 51、圖 52:
圖 49. 執行 INSERT 事務的 session1 窗口
圖 50. 執行查詢事務的 session2 窗口
圖 51. 快照監控窗口
圖 52. 執行查詢事務的 session3 窗口
如果這種情況下 session2 希望能夠跳過未提交 的 insert 操作而得到數據,那麼可以打開 DB2_SKIPINSERTED 註冊表變量,見圖 53:
圖 53. 設置實例註冊表變量 DB2_SKIPINSERTED=ON
然後再重複剛才的實驗,我們發現這個時候,session2 已經可以查詢到數據了,見圖 54、圖 55:
圖 54. 執行 INSERT 事務的 session1 窗口
圖 55. 執行查詢事務的 session2 窗口
可見session1中插入的記錄被忽略掉了。
總結
總的來説這 3 個註冊表變量會影響到併發性。通過合理設置這些變量可以改善併發性,但是也會影響到應用程序的行為,所以建議綜合考慮業務的需求和結合自己的業務邏輯,來考量是否適合啓用相應的註冊表變量。
回頁首
樂觀鎖定、增強的樂觀鎖定與併發性
樂觀鎖定是與悲觀鎖定相對應的,所以要談樂觀鎖定,就不可避免的要談談什麼是悲觀鎖定,什麼是樂觀鎖定?
悲觀鎖定
所謂悲觀鎖定,就是查詢表之後和嘗試搜索的更新或刪除記錄操作之間的時間段掛起鎖定的一種鎖定策略。我們假設有這樣一個場景,見圖 56:
圖 56. 應用程序場景
悲觀鎖定策略是:用户使用 RR/RS 隔離級別執行 SELECT 操作或以排他 exclusive 模式鎖定表並執行查詢操作(這裏舉例説明),通過遊標得到一個結果集——對結果集記錄加鎖或者鎖表,然後遍歷這個遊標,對這個結果集的每條記錄做進一步的判斷,符合條件的做更新操作,不符合條件的跳過,一直到遊標遍歷結束後,執行 COMMIT 操作,釋放由 SELECT 操作獲得的鎖以及更新操作獲得的鎖,見圖 57:
圖 57. 悲觀鎖定策略
悲觀鎖定的優點就是能夠保證實現一致且安全的更改。但是這種鎖定策略的主要缺點就是在數據處理期間,一直佔據着資源——被處理的資源的鎖生命週期比較長,這樣的話,可能會降低數據庫的併發性,對於具有大併發用户的系統,需要等待資源釋放的概率則會增加。
樂觀鎖定
樂觀鎖定 是一項技術,它用於在選擇 (SELECT) 行與更新或刪除行之間未擁有行鎖定的數據庫應用程序。應用程序樂觀地假定在更新或刪除操作前未鎖定的行不可能更改。如果行更改,那麼更新或刪除操作將失敗,並且應用程序邏輯將通過重試查詢操作(此處是舉例説明)來處理這種故障。樂觀鎖定策略的主要優點是最小化給定資源對於其他事務的不可用時間,因此,它具有比悲觀鎖定更好的並行性,因為其他應用程序可以讀寫該行。它的一個缺點是應用程序中需要有更多的重試邏輯。
基於上述同樣的場景,樂觀鎖定的策略是用户使用 CS/RS 隔離級別執行 SELECT 操作(此處是舉例説明),得到一個結果集存放到客户端,然後執行 COMMIT 操作,釋放對資源的鎖定,然後在客户端遍歷這個結果集,對這個結果集中的每條數據做進一步的判斷,符合條件的做更新操作(如果確實這一行已經被修改了,那麼更新操作將失敗,應用程序邏輯將處理這些失敗,例如,重新嘗試查詢),不符合條件的跳過,一直到遍歷結束,執行 COMMIT 操作,釋放相關資源上的鎖,見圖 58:
圖 58. 樂觀鎖定策略
DB2 V9.5 之前版本的 DB2 應用程序只能通過構建搜索式 UPDATE 語句啓用按值樂觀鎖定,該語句查找具有與所選值完全相同的值的行。如果行的列值已更改,那麼搜索式 UPDATE 語句將失敗。但是,按值樂觀鎖定具有一些缺點:
標識主動錯誤信息,這可能會更新錯誤的行
構建 UPDATE 搜索條件對應用程序來説很複雜
DB2 服務器根據值來搜索目標行的效率不高
某些客户機類型與數據庫類型之間的數據類型不匹配,例如,時間戳記不允許在搜索式 UPDATE 中使用所有列。
增強的樂觀鎖定
在 DB2 V9.5 之前,樂觀鎖定是按值樂觀鎖定,完全由應用程序本身邏輯控制實現的,而從 DB2 V9.5 開始支持增強的樂觀鎖定,除了應用程序本身邏輯控制實現之外,DB2 本身還提供了一些增強的機制來提高執行的效率。
DB2 V9.5 增加了速度更快的樂觀鎖定的支持,這種樂觀鎖定不會產生主動錯誤信息(誤判),
DB2 V9.5 的樂觀鎖定特性最小化了給定資源對於其他事務的不可用時間,進一步改善了併發性。這一支持通過如下所示的新 SQL 函數、表達式和特性實現的:
行標識符(RID_BIT或RID)內置函數:該內置函數可用於 SELECT 結果列表或謂詞語句。例如,在謂詞 WHERE RID_BIT(tab)=? 中,RID_BIT 等於謂詞被實現為一種新的直接訪問方法(避免了表掃描),從而可以更有效地定位行。在以前,被稱為值樂觀鎖定的技術確定值的方式為:將所有選擇(SELECT)的列值添加到謂詞,然後應用某些惟一的列組合來篩選出單個行,這種通過表掃描方式訪問方法效率較低。
ROW CHANGE TOKEN表達式:這種新的表達式返回一個標記作為 BIGINT 。這個標記表示某一行的修改序列中的一個相對點。應用程序可以將某行的當前行更改標記值與上次取回行時保存的行更改標記值進行比較,以判斷行是否發生修改。表需要定義一個行修改時間戳列來保存時間戳值,是否提供行修改時間戳列將影響樂觀鎖定的行為,因為該列有助於將行更改標記的粒度從頁級別提高到行級別,這對樂觀鎖定應用程序非常有利。
使用這種編程模型的應用程序將從增強的樂觀鎖定特性中獲益。
在應用程序中使用增強的樂觀鎖定
在 DB2 V9.5 中,對普通的表即可使用針對樂觀鎖定的新 SQL 表達式和屬性,但是,如果對普通的表不進行 DDL 修改的情況下,樂觀鎖定應用程序可能會產生更多的漏判。因此,要避免發生漏判,執行樂觀鎖定的目標表應執行以下任意一種操作來增加 ROW CHANGE TIMESTAMP 列:
創建表時定義 ROW CHANGE TIMESTAMP 列
對錶進行修改以包含 ROW CHANGE TIMESTAMP 列
要在應用程序中啓用增強的樂觀鎖定支持,需要執行以下基本步驟:
在初始查詢中,對要進行處理的所有行的行標識符和行更改標記執行 SELECT(使用 RID_BIT() 和 RID() 內置函數),使它們被包含在查詢列表中。
釋放行鎖定,以便其他應用程序可以對錶執行選擇、插入、更新和刪除操作。
通過在搜索條件中使用行標識符和行更改標記對目標行執行搜索式 UPDATE 或 DELETE,並樂觀地假定在執行原始查詢語句後未鎖定的行尚未更改。
如果行已更改,那麼更新操作將失敗,並且應用程序邏輯必須處理該故障。例如,應用程序將重試查詢和更新操作。
注:行標識符、行更改標記的用途如下
—搜索行標識符以直接訪問目標行,而不是通過全表掃描或者索引掃描
—搜索行更改標記以確認行的狀態,即如果行更改標記沒變,表明相應記錄未被任何事務更改,
反之表明記錄已經被更改。
針對樂觀鎖定設計並已啓用樂觀鎖定的應用程序,將按照下列操作順序向數據庫發送請求:
清單 1. 樂觀鎖定方式執行查詢
SELECT SALARY, row change token FOR STAFF, RID_BIT(STAFF)
INTO :h_SALARY, :h_rct, :h_rid
FROM STAFF WHERE ID = 240
在此方案中,應用程序首先讀取所需的每行。我們準備在應用程序中使用樂觀鎖定策略,所以選擇列表包括保存在 :h_rid 主變量中的行標識符值和保存在 :h_rct 主變量中的行更改標記值,見清單1。
在啓用了樂觀鎖定的情況下,應用程序樂觀地假定更新或刪除操作的任何目標行都保持不變。為了提高數據庫並行性,應用程序使用下列其中一種方法除去行鎖定:
落實工作單元,在這種情況下行鎖定將被除去
使用 WITH RELEASE 子句關閉遊標,在這種情況下行鎖定將被除去
使用較低的隔離級別:
遊標穩定性(CS),在這種情況下,在遊標訪存到下一行或結果表末尾後行未鎖定。
未落實的讀(UR),在這種情況下,任何未落實的數據將具有新的(未落實)行更改標記值。如果回滾未落實的數據,那麼已落實的舊行更改標記將是另一個值。
注: 假定通常不回滾更新,使用 UR 將允許最大並行性。
斷開與數據庫的連接,因此釋放應用程序的所有 DB2 服務器資源。
在釋放查詢操作所帶來的行鎖定之後,在應用程序邏輯中可能對上述查詢獲得的結果集進行相關判斷處理,得到想要繼續向下處理的行的行標識符和行更改標記,然後應用程序樂觀地更新這些行:
清單 2. 樂觀鎖定方式執行更新
UPDATE STAFF SET SALARY = SALARY * 1.2 WHERE row change token
FOR STAFF = :h_rct AND RID_BIT(STAFF) = :h_rid
通過行標識符、行更改標記對目標行進行 UPDATE 是最快的訪問方案,因為避免了對錶進行掃描,而通過直接訪存的方式(通過 ROWID 直接定位到該行):
RID_BIT(STAFF) = :h_rid
如果 RID_BIT() 謂詞未找到行,那麼表示行已刪除並且更新操作由於未找到行而失敗。
假定 RID_BIT() 謂詞找到了行,那麼在行更改標記未更改時,行更改標記謂詞 FOR STAFF = :h_rct 將找到該行。如果在查詢操作後行更改標記已更改,那麼 UPDATE 語句將由於未找到行而失敗。
下表總結了應用程序在啓用了樂觀鎖定之後,可能出現的一些情況。
表 7. 列示了在啓用樂觀鎖定後可能出現的情況
情況標識 操作 結果
情況 1 表中定義了行更改時間戳記列,並且其他應用程序未更改行。 由於行更改標記謂詞成功找到 :h_rid 所標識的行,所以更新操作成功。
情況 2 表中定義了行更改時間戳記列。另一個應用程序在查詢操作後在更新操作前更新了行,從而更新了行更改時間戳記列。 在查詢操作時獲得的行更改標記與行中當前行更改標記不匹配,因此 UPDATE 語句找不到行。
情況 3 表中定義了行更改時間戳記列。另一個應用程序更新了行,因此行具有新的行更改標記。此應用程序使用隔離級別 UR 查詢行,並獲取未落實的新的行更改標記。 此應用程序運行 UPDATE 語句,這將鎖定等待,直到其他應用程序釋放其行鎖定為止。如果其他應用程序使用新標記落實更改,那麼行更改標記謂詞將成功,因此 UPDATE 語句成功。如果其他應用程序回滾到舊標記,那麼行更改標記謂詞將失敗,因此 UPDATE 語句找不到行。
情況 4 表中未定義任何行更改時間戳記列。在查詢操作後在更新操作前,在同一頁面上更新、刪除或插入了另一行。 該頁面上的所有行共享一個行更改標記,該頁面上任何行的更新,都會導致此頁面上所有行的行更改標記更新。由於該頁面上所有行的行更改標記值已更改,因此 UPDATE 語句匹配不到行。
如果本場景中添加了行更改時間戳記列,那麼 UPDATE 語句將成功更新目標行。
情況 5 改變 (ALTER) 了表以便包含行更改時間戳記列,並且在改變操作後查詢返回的行還未修改。另一個應用程序更新該行,從而在此過程中將行更改時間戳記列添加至該行(具有當前時間戳記)。 行更改標記謂詞將先前生成的標記值與根據行更改時間戳記列創建的標記值進行比較,因為找不到匹配行,因此 UPDATE 語句執行失敗。
情況 6 在查詢操作後在更新操作前重組了表。 :h_rid 所標識的行標識找不到行,或者它包含具有另一個標記的行(經過重組,已經不是原來的那行),因此更新操作失敗。這是無法避免的被動錯誤信息情況。 記錄本身未被重組操作更新,記錄中所有的列值並未變化,只是記錄的位置發生了變更,因此重組後謂詞的 RID_BIT 部分無法標識原始行(實際上 UPDATE 語句根據行標識符、行更改標識無法匹配到行),因此 UPDATE 操作失敗。
舉例説明
假設有這樣的一個場景:2 個數據庫管理員同時接收到某個職員的請求,職員要求將自己的工作崗位變更一下。於是兩名 DBA(DBA1/DBA2)通過人力資源系統更新 SAMPLE 數據庫的 STAFF 表中的員工記錄。此時存在一種可能,即兩個 DBA 可能同時對同一名職員的記錄進行更新。下面設計了幾個場景,針對表 7 中所有的情況進行演示。
第 1 種情況
STAFF 表包含一個 ROW CHANGE TIMESTAMP 列 TS(後來添加)並且只有 DBA1 訪問了該表。 DBA1 從 STAFF 表中查詢數據並在稍後嘗試將 ID 為 70 的員工的工作從 Sales 更新為 Mgr 。更新成功。
清單 3. 將時間戳記列添加到 STAFF 表
ALTER TABLE STAFF ADD COLUMN TS TIMESTAMP NOT NULL
GENERATED ALWAYS FOR EACH ROW ON UPDATE AS
ROW CHANGE TIMESTAMP
清單 4. DBA1 執行查詢語句
通過查詢得到結果集的行標識符、時間標識符 , 然後執行 COMMIT 操作。
SELECT RID_BIT(STAFF),ROW CHANGE TOKEN FOR STAFF,ID,NAME,DEPT,JOB,TS
FROM STAFF WHERE ID=70
COMMIT
表 8. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A000006000000000000000000FCF84F' 74904229642240 70 Rothman 15 Sales 0001-01-01-00.00.00.000000
清單 5. DBA1 執行 UPDATE 語句
使用 DBA1 之前查詢出來的行標識符、時間標識符來更新行。
UPDATE STAFF SET JOB = 'Mgr'
WHERE RID_BIT(STAFF)=x'0A000006000000000000000000FCF84F'
AND ROW CHANGE TOKEN FOR STAFF=74904229642240
表 9. 執行 UPDATE 語句後的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A000006000000000000000000FCF84F' 141401933370756520 70 Rothman 15 Mgr 2009-07-02-15.10.17.921000
第 2 種情況
STAFF 表包含一個 ROW CHANGE TIMESTAMP 列 TS,並且 DBA1 和 DBA2 同時訪問該表。 DBA1 從 STAFF 表中查詢數據並執行提交操作,稍後嘗試更新這些數據。然而,在 DBA1 查詢數據到執行更新操作期間,DBA2 對相同的記錄進行了更新。 DBA2 執行的更新成功,而隨後 DBA1 執行的更新失敗。
清單 6. 為 STAFF 表添加時間戳記列
ALTER TABLE STAFF ADD COLUMN TS TIMESTAMP NOT NULL GENERATED
ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP
表 10. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00400B00000000000000000465A5C1' 141401934713623382 70 Rothman 15 Mgr 2009-07-02-15.30.18.562006
清單 7. DBA2 執行 UPDATE 語句
UPDATE STAFF SET JOB = 'Engineer' WHERE ID=70
OR
UPDATE STAFF SET JOB = 'Engineer'
WHERE RID_BIT(STAFF)= x'0A00400B00000000000000000465A5C1'
AND ROW CHANGE TOKEN FOR STAFF=141401934713623382
表 11. DBA2 執行 UPDATE 語句後的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00400B00000000000000000465A5C1' 141401934875088080 70 Rothman 15 Engineer 2009-07-02-15.32.44.546000
清單 8. DBA1 執行 UPDATE 語句
使用 DBA1 之前查詢得到的行標識符、時間標識符來更新行。
UPDATE STAFF SET JOB = 'Engi'
WHERE RID_BIT(STAFF)= x'0A00400B00000000000000000465A5C1'
AND ROW CHANGE TOKEN FOR STAFF=141401934713623382
DBA1 更新失敗。由於 DBA2 執行了 UPDATE,ROW CHANGE TOKEN 發生了改變,因此當 DBA1 把執行查詢時取回的標記與 DBA2 執行更新後的標記當前值進行比較時,無法找到匹配行,因此 DBA1 更新失敗。
第 3 種情況
STAFF 表包含一個 ROW CHANGE TIMESTAMP 列 TS,並且 DBA1 和 DBA2 同時訪問該表,DBA1 對行進行了更新,但還未提交修改,DBA2 使用 UR 隔離級別從 STAFF 表中查詢數據,接着 DBA1 提交它做出的修改,然後 DBA2 嘗試對相同的數據進行更新,DBA2 執行更新能夠成功,因為 DBA2 之前執行髒讀查詢讀取的是 DBA1 未提交的更新,這時變化後的 ROW CHANGE TOKEN 被讀取到(當 DBA1 後來執行提交操作時,提交前產生變化的 ROW CHANGE TOKEN 不會再發生改變),所以後來 DBA2 執行更新操作能夠找到匹配行,執行更新成功。然而,如果 DBA1 回滾更新操作而不是提交更新操作,那麼 DBA2 的更新操作將失敗。
表 12. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00000600000000000000000465ABC4' 141401934875088080 70 Rothman 15 Mgr 2009-07-02-15.32.44.546000
清單 9. DBA1 執行 UPDATE 語句,但是未提交
UPDATE STAFF SET JOB = 'Chief'
WHERE RID_BIT(STAFF)= x'0A00000600000000000000000465ABC4'
AND ROW CHANGE TOKEN FOR STAFF=141401934875088080
表 13. DBA2 使用隔離級別 UR 查詢此行 (WHERE ID=70) 的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00000600000000000000000465ABC4' 141401936241179608 70 Rothman 15 Chief 2009-07-02-15.53.03.343000
然後 DBA1 提交之前執行的 UPDATE 語句
清單 10. DBA1 執行提交操作
commit
表 14. DBA1 查詢提交後的結果清單 11. DBA2 執行 UPDATE 語句
UPDATE STAFF SET JOB = 'GM' WHERE ID=70
OR
UPDATE STAFF SET JOB = 'GM'
WHERE RID_BIT(STAFF)= x'0A00000600000000000000000465ABC4'
AND ROW CHANGE TOKEN FOR STAFF=141401936241179608
表 15. DBA2 執行 UPDATE 語句後的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00000600000000000000000465ABC4' 141401937033810064 70 Rothman 15 GM 2009-07-02-16.00.55.250000
第 4 種情況
STAFF 表不包含 ROW CHANGE TIMESTAMP 列。 DBA1 查詢一行 (ID=70),獲得 ROW CHANGE TOKEN/RID_BIT(),然後 DBA2 更新了同一數據頁中的其他記錄 (ID=110, 這時 ROW CHANGE TOKEN 將發生改變,這個頁中所有記錄的 ROW CHANGE TOKEN 都是一樣的,這時 ROW CHANGE TOKEN 是基於頁的 ),然後,DBA1 嘗試更新之前查詢到的記錄(通過 ROW CHANGE TOKEN/RID_BIT() 來定位),更新將會失敗,因為 ROW CHANGE TOKEN 已經發生了改變,找不到匹配的記錄了。
清單 12. DBA1 執行查詢語句
通過查詢得到結果集的行標識符、時間標識符 , 然後執行 COMMIT 操作。
SELECT RID_BIT(STAFF),ROW CHANGE TOKEN FOR STAFF,ID,NAME,DEPT,JOB
FROM STAFF WHERE ID = 70
COMMIT
表 16. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB
x'0A00400B00000000000000000465EFF9' 7632868011317133312 70 Rothman 15 Mgr
表 17. 同一數據頁面上還存在的其他一些記錄清單 13. DBA2 執行 UPDATE 語句
更新同一數據頁面上的另外一條記錄 (ID=110)
UPDATE STAFF SET JOB = 'Mgr' WHERE ID=110
表 18. DBA2 執行 UPDATE 語句後的結果清單 14. DBA1 執行 UPDATE 語句
通過之前查詢得到的 ROW CHANGE TOKEN/RID_BIT() 定位。
UPDATE STAFF SET JOB = 'Clerk'
WHERE RID_BIT(STAFF)= x'0A00400B00000000000000000465EFF9'
AND ROW CHANGE TOKEN FOR STAFF=7632868011317133312
更新將失敗,已經找不到匹配的記錄了,因為整個頁面的時間標識符已經由於 DBA2 執行 UPDATE 語句而改變了。
第 5 種情況
STAFF 表包含 ROW CHANGE TIMESTAMP 列 TS,DBA1 和 DBA2 訪問該表。 DBA1 從中查詢一行,然後 DBA2 更新了同一數據頁中的另外一行,接着 DBA1 更新之前查詢到的那條記錄,更新可以成功。
清單 15. DBA1 執行查詢操作
通過查詢得到結果集的行標識符、時間標識符 , 然後執行 COMMIT 操作。
SELECT RID_BIT(STAFF),ROW CHANGE TOKEN FOR STAFF,ID,NAME,DEPT,JOB,TS
FROM STAFF WHERE ID=120
COMMIT
表 19. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0F0000060000000000000000055F4927' 141402635178433027 120 Naughton 38 Clerk 2009-07-07-18.36.01.875011
表 20. 同一數據頁面上還存在的其他一些記錄清單 16. DBA2 執行 UPDATE 操作
更新同一數據頁面上的另外一條記錄。
UPDATE STAFF SET JOB = 'VP'
WHERE RID_BIT(STAFF)= x'0D0000060000000000000000055F4927'
AND ROW CHANGE TOKEN FOR STAFF=141402635178433025
表 21. DBA2 執行 UPDATE 語句後的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0D0000060000000000000000055F4927' 141402635937539040 100 Plotz 42 VP 2009-07-07-18.47.21.812000
清單 17. DBA1 執行 UPDATE 語句
通過之前查詢得到的 ROW CHANGE TOKEN/RID_BIT() 定位,更新成功。
UPDATE STAFF SET JOB = 'Mgr'
WHERE RID_BIT(STAFF)= x'0F0000060000000000000000055F4927'
AND ROW CHANGE TOKEN FOR STAFF=141402635178433027
表 22. DBA1 執行 UPDATE 語句後的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0F0000060000000000000000055F4927' 141402643285428659 120 Naughton 38 Mgr 2009-07-07-20.28.53.281011
第 6 種情況
STAFF 表具有一個 ROW CHANGE TIMESTAMP 列 TS,並且只有 DBA1 訪問該表。 DBA1 先從中查詢一條記錄用於以後更新它。然後表被離線重組。最後 DBA1 嘗試更新數據,更新失敗。更新失敗是因為執行 REORG 後 RID_BIT 指向的已經不是原來的那一條記錄,而且所有記錄的 ROW CHANGE TIMESTAMP 列值也發生了變化,所以 DBA1 根據之前查詢得到的 RID_BIT/ROW CHANGE TOKEN 已經找不到匹配的行,所以更新失敗。
清單 18. DBA1 執行查詢操作
通過查詢得到結果集的行標識符、時間標識符 , 然後執行 COMMIT 操作。
SELECT RID_BIT(STAFF),ROW CHANGE TOKEN FOR STAFF,ID,NAME,DEPT,JOB,TS
FROM STAFF WHERE ID=150
COMMIT
表 23. DBA1 執行查詢的結果
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'1200400B00000000000000000465EFF9' 74904229642240 150 Williams 51 Sales 0001-01-01-00.00.00.000000
清單 19. 重組 STAFF 表
REORG TABLE STAFF
表 24. 重組表 STAFF 後記錄發生的變化(ID=150)
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'12000006000000000000000004A3C230' 141401962414435156 150 Williams 51 Sales 2009-07-02-21.59.04.093012
可以發現ID=150這條記錄已經被移動到別的頁面上去了,因為 RID_BIT() 已經發生了變化,而且 ROW CHANGE TOKEN 也發生了變化。
清單 20. DBA1 執行 UPDATE 語句
根據之前查詢得到的行標識符、行更改標記執行更新操作,很明顯,更新操作不能成功。
即使行標識符可能仍然指向有效的記錄(已經不是原來的那條記錄,因為這個位置可能已經被新的記錄所佔據),
但是行更改標記也已經發生了改變,所以使用之前查詢得到的行標識符、行更改標記進行匹配,
匹配不到記錄,所以更新操作不能成功。
UPDATE STAFF SET JOB = 'Mgr'
WHERE RID_BIT(STAFF)= x'1200400B00000000000000000465EFF9'
AND ROW CHANGE TOKEN FOR STAFF=74904229642240
為了避免在使用悲觀鎖定技術時可能引發的鎖等待而導致的併發性問題,樂觀鎖定技術最小化了給定資源對於其他事務的不可用時間。通過使用樂觀鎖定,數據庫管理器在完成讀操作之後可以立即釋放鎖。
DB2 V9.5 支持更高效的樂觀鎖定,而且避免了誤判的發生。這些支持通過行標識符(RID_BIT 或 RID)內置函數、行更改標識符 (ROW CHANGE TOKEN 表達式 ) 實現的。使用這種編程模型的應用程序可以從增強的樂觀鎖定特性受益,並且能夠進一步增強併發性。
參考資料
學習
通過訪問 DB2 9 技術資源中心 ,查看 DB2 9 相關技術文章和教程的資源。
通過訪問 DB2 Express-C 產品專題,瞭解 DB2 Express-C 相關的產品和文檔、教程等技術資源。
通過訪問 DB2 V9.5 信息中心 ,瞭解 DB2 V9.7 的新特性概述和調優基本方法。
通過訪問 DB2 9基礎(730考試)認證指南,第 6 部分:數據併發性,講述在併發任務中通過某種機制維護數據庫一致性的教程。
通過訪問 分析DB2 for Linux, UNIX, and Windows中的鎖等待情形,本文通過例子演示如何使用用於 DB2 for LUX 的 db2pd 和 db2pdcfg 實用程序完成該任務。
通過訪問 通過改善鎖提高應用程序併發性,瞭解通過合理設置 DB2_EVALUNCOMMITTED、DB2_SKIPDELETED 和 DB2_SKIPINSERTED 註冊變量,改善鎖的情況提高應用程序併發。
通過訪問 使用 DB2 V9.5 樂觀鎖定特性改善併發性,瞭解這種增強的樂觀鎖定特性,並瞭解使用這種編程模型的應用程序如何從中獲益並進一步改善併發性。
通過訪問 DB2併發機制表格分析法,本文介紹一種數據庫併發機制表格分析方法。藉助表格,能夠幫助理解、分析 DB2 的併發機制。
獲得產品和技術
現在可以免費使用 DB2 。下載 DB2 Express-C,這是為社區提供的 DB2 Express Edition 的免費版本,它提供了與 DB2 Express Edition 相同的核心數據特性,為構建和部署應用程序奠定了堅實的基礎。
下載 DB2 Enterprise V9.5 試用版 ,試用本文中描述的特性。
下載 信息管理軟件試用版 ,體驗它們強大的功能。
討論
參與 developerWorks blog 並加入 developerWorks 社區。
關於作者
任之國,IBM 技術支持工程師,專攻 DB2 for LUW 平台的數據庫管理、開發、設計等相關內容,對數據庫相關管理和開發技術有濃厚興趣,你可以通過 [renzhig@cn.ibm.com] 或者 [zgrrzg@sina.com] 與他聯繫。
結束語
下面幾條記錄都屬於同一數據頁面。
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB
x'0B0000060000000000000000055F4927' 141402635178433023 80 James 20 Clerk
x'0C0000060000000000000000055F4927' 141402635178433024 90 Koonitz 42 Sales
x'0D0000060000000000000000055F4927' 141402635178433025 100 Plotz 42 Mgr
x'0E0000060000000000000000055F4927' 141402635178433026 110 Ngan 15 Clerk
下表為同一數據頁面上的部分記錄,可以看到雖然只更新了ID=110這條記錄,但是此頁面上所有記錄的 ROW CHANGE TOKEN 都共享 ID=110 這條記錄的 ROW CHANGE TOKEN,也就是説表在沒有時間戳記列存在的情況下,ROW CHANGE TOKEN 是基於頁存在的。
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB
x'0A00400B00000000000000000465EFF9' -1300866274502377472 70 Rothman 15 Mgr
x'0B00400B00000000000000000465EFF9' -1300866274502377472 80 James 20 Clerk
x'0C00400B00000000000000000465EFF9' -1300866274502377472 90 Koonitz 42 Sales
x'0D00400B00000000000000000465EFF9' -1300866274502377472 100 Plotz 42 Mgr
x'0E00400B00000000000000000465EFF9' -1300866274502377472 110 Ngan 15 Clerk
下面幾條記錄都屬於同一數據頁面。
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB
x'0B00400B00000000000000000465EFF9' 7632868011317133312 80 James 20 Clerk
x'0C00400B00000000000000000465EFF9' 7632868011317133312 90 Koonitz 42 Sales
x'0D00400B00000000000000000465EFF9' 7632868011317133312 100 Plotz 42 Mgr
x'0E00400B00000000000000000465EFF9' 7632868011317133312 110 Ngan 15 Clerk
可以發現時間標識符與 DBA2 查詢到的一致,實際上反映的都是新的時間標識符(不管是否提交)。
樂觀鎖定表達式 STAFF 表
RID_BIT ROW CHANGE TOKEN ID NAME DEPT JOB ROW CHANGE TIMESTAMP
x'0A00000600000000000000000465ABC4' 141401936241179608 70 Rothman 15 Chief 2009-07-02-15.53.03.343000