在數據庫管理系統(DBMS)的領域中,術語“並發性”用於表示不止一個應用程序基本上(從用戶的角度來看)同時訪問同一數據的能力。因為 DBMS 的主要優點之一就是可以在多個用戶和多個應用程序中共享數據,所以數據庫系統應該提供一種管理並發訪問數據的方法。DBMS 必須確保維護數據的一致狀態和數據的完整性。
取得該效果的一種方法就是實施只串行(serial-only)模式來處理數據庫請求。即每個事務都要等待另一事務(具有更高的優先權或者比它早啟動)完成其工作。然而,對於現在的在線系統和客戶異常來說,這種處理方式所產生的性能水平簡直令人無法接受。
而另一種方法就是,DBMS 可以通過 鎖的方式管理多個應用程序對數據的訪問。鎖是一種軟件機制,用於在維護數據完整性和一致性的同時,允許盡可能大的吞吐量(通過最大限度地並發訪問數據)。
並發性控制的重要性
如果沒有控制並發性的有效方法,就可能損害數據的完整性和一致性。DBMS 必須保護數據庫,防止發生下列狀況:
丟失更新—— 假設應用程序 A 和應用程序 B 同時讀取數據庫中的同一行,並且都為其中某一列計算新值。如果應用程序 A 先用其新值更新該行,隨後應用程序 B 又更新同一行,那麼第一次的更新(由應用程序 A 執行的)就會丟失。
不可重復讀—— 某些應用程序進程可能要求完成以下事件序列:程序 A 從表中讀取特定的一行,然後繼續進行其他的 SQL 請求。稍後,程序 A 再次讀取開始的那一行,並且必須在所有的列中找到與第一次讀取相同的值。如果缺乏合適的並發性控制,另一應用程序就可能在這兩次讀取操作之間修改該行數據。
訪問未提交的數據—— 應用程序 A 更新一行中的某些列的值,而在提交該修改之前,應用程序 B 讀入該行的新(更新)值。如果應用程序 A 接著又“撤銷”更新值(通過程序邏輯中的 SQL ROLLBACK 語句,或者因為發生錯誤由 DB2 UDB 自動進行回滾),那麼,應用程序 B 對該行的處理就是基於未提交的(因而可能是不正確的)數據進行的。
在維護數據完整性的同時,提供多個應用程序同時訪問數據的能力稱作 並發性控制。
鎖
鎖是一種由 DB2 UDB 用於完成並發性控制的軟件機制。鎖實質上就是一個控制塊,將 DB2 UDB 對象或資源與應用程序關聯起來,並控制其他應用程序如何訪問同一對象或資源。與 DB2 UDB 資源有關聯的應用程序被稱為“持有”或“擁有”該鎖。
通過使用鎖,DB2 UDB(管理該數據庫)可以防止發生上述幾類問題。DB2 UDB 與另一 MVS 地址空間 IRLM 配合管理這些鎖。IRLM 將跟蹤這些鎖及其所有者,以確定應用程序請求的 DB2 UDB 資源是否可用於該類工作。資源可以是 鎖定的或 共享的,這取決於當前資源上的鎖的“持有者”所進行的處理類型,以及請求應用程序所預期的處理類型。
鎖模式
最常用的兩種鎖模式是 共享的和 排他的。共享鎖與只讀操作有關聯,這意味著持有該鎖的應用程序可以讀取數據,而其他應用程序也可以讀取該數據。排他鎖與寫操作有關聯,這意味著持有該鎖的應用程序有資格更新數據,但在鎖所有者完成更新(將修改提交給數據庫)並釋放該鎖之前,其他應用程序無法使用該數據。
DB2 UDB 和 IRLM 使用其他類型和子類型的鎖模式來實現鎖定和並發性控制。您可以在 DB2 UDB Administration 手冊中找到關於這點的更多詳細描述。
鎖定粒度
除了使用各種鎖模式,DB2 UDB 還提供了不同的鎖定級別,用以控制被鎖定數據的范圍。各種級別表示了 DB2 UDB 使用的鎖定粒度,其范圍可以從單個行到整個表空間。DB2 UDB 根據鎖定粒度來使用不同的鎖模式。
具有多種鎖定級別的理由十分簡單。某些應用程序可能要求有權讀取或更新大范圍的數據,而其他應用程序則可能只要求訪問窄得多的范圍。如果只能使用一種鎖定級別,則會降低整個系統性能。例如,一下子鎖定太多數據會強制其他應用程序進行不必要的等待。否則,DB2 UDB 可能使用過多的系統資源,嘗試服務對附加數據資源進行鎖定的附加請求。能夠實現多種級別的鎖粒度可以極大地提高並發性水平。
暫掛
若一個應用程序進程請求一個鎖,而該鎖已被另一應用程序進程所擁有,且不能共享,此時,就稱該進程為 暫掛的。請求應用程序會被掛起,即它將暫時停止運行。鎖請求的優先次序如下:將新來的鎖請求按照接收次序進行排隊。已經持有鎖的應用程序的請求以及進行鎖提升的請求要比新應用程序的請求先得到服務。而在那些分組中,請求次序則為“先進先出(first in,first out,FIFO)”。
超時
當應用程序處於暫掛狀態(見上面)超過了預設的一段時間間隔,那麼就要終止該應用程序。該應用程序被稱為已經超時。在終止該應用程序之前,會在 SQLCA 中收到一條合適的錯誤消息。SQLCA(SQL 通信域)是 SQL 應用程序預留的一塊大小固定的存儲區域,用於從 DB2 UDB 向程序傳遞條件代碼和其他信息。
某些操作,如 COMMIT 和 ROLLBACK,就不能超時。在下面的子標題 RESOURCE TIMEOUT 中,將對決定應用程序進程可以等待資源多長時間的預設時間間隔進行討論。
死鎖
當兩個或更多應用程序每個都持有另一應用程序所需資源上的鎖,沒有這些資源,那些應用程序都無法繼續完成其工作時,這時就會出現死鎖的狀況。
以下是一個簡單的死鎖場景:
應用程序 A 訪問表 T,並請求頁面 X 上的排他(非可共享的)鎖。
應用程序 B 訪問表 T,並請求頁面 Y 上的排他鎖。
然後,應用程序 A 請求頁面 Y 上的鎖,同時仍然持有頁面 X 上的排他鎖。應用程序 A 將被掛起,因為應用程序 B 具有頁面 Y 上的排他鎖。
然後,應用程序 B 請求頁面 X 上的鎖,同時仍然持有頁面 Y 上的排他鎖。應用程序 B 將被掛起,因為應用程序 A 具有頁面 X 上的排他鎖。
這是一種僵持情況。應用程序 A 和 B 都無法繼續工作。
在一段預設的時間間隔之後(請參閱標題 DEADLOCK TIME 下面的討論),DB2 UDB 將終止當前工作單元,因為某個應用程序陷入死鎖狀態(通常為所做工作最少的應用程序)。這將釋放終止程序所持有的鎖,並允許剩余的應用程序繼續下去。 DB2 UDB 將向被終止的應用程序的 SQLCA 發送描述性的錯誤消息和信息。
實用程序和命令的並發機制
當 SQL 應用程序使用事務鎖來控制對 DB2 UDB 對象的並發訪問時,DB2 UDB 實用程序和命令可以通過其他方法訪問 DB2 UDB 對象,即 聲明(claim)、放棄(drain)和 兼容性規則。
聲明是指通知 DB2 UDB 當前正在訪問某個特定對象。提交之後,該聲明通常不會繼續存在。為了在下一工作單元裡訪問 DB2 UDB 對象,應用程序需要進行新的聲明。聲明通知 DB2 UDB 當前正關注某個 DB2 UDB 對象,或是該對象上存在活動。只要 DB2 UDB 對象上存在聲明,在釋放那些聲明之前,就不能采取任何放棄(drain)。
放棄(drain)是指通過下列方式來訪問 DB2 UDB 對象的動作:
阻止對對象進行任何新的聲明。
等待釋放對象上現有的所有聲明。
DB2 UDB 對象上的放棄導致 DB2 UDB 停止(quIEsce)所有當前正聲明該資源的應用程序,其方式為允許它們到達提交點,但阻止它們(或任何其他的應用程序進程)進行新的聲明。Drain 鎖還阻止沖突進程同時放棄(drain)同一對象。
DB2 UDB 一般通過一組兼容性規則來控制實用程序的並發操作。如果兩個實用程序不需要同時以不可兼容的模式訪問相同的 DB2 UDB 對象,就將它們視為是“兼容的”。當一個實用程序作業開始時,DB2 UDB 就檢查系統,查看當前是否有其他任何實用程序正處理同一 DB2 UDB 對象。如果該對象當前未被另一實用程序訪問,或者如果另一執行實用程序是可兼容的,該實用程序才可以繼續。
數據庫設計考慮
取得高度並發性應該是指導 DB2 UDB 數據庫初始設計的目標之一。在創建各個表的同時,可能要考慮許多影響並發性的功能。您可以在實現創建之後再添加某些功能(例如通過更改 DB2 UDB 對象),但另一些功能則無法添加,或者至少需要大量重復操作和/或打亂當前實現。因此,最好是一開始就考慮這些問題。
分區
我極力建議您將較大的表創建為分區表。一個分區表空間只包含單個分區表。要基於分區索引的鍵范圍將該表分成多個分區。每個分區都是作為單獨的數據集(dataset)來創建的,並且可以作為單獨的實體由 DB2 UDB 加以處理。
對於大量的批處理操作(INSERT、UPDATE、DELETE),您可以將大型作業劃分成較小的作業,利用該分區表結構。許多這些較小的作業可以並發運行(對不同的分區進行),以便減少整個批處理操作的占用時間,從而使另一應用程序進程可以更早地訪問該表。
分區表以類似的方式使實用程序作業只作用於所選擇的分區,從而允許其他作業或應用程序進程並發訪問表中的其他分區。在許多情況下,DB2 UDB 實用程序本身就具有利用分區表的能力,而在其他情況下,它只為用戶提供了設計其工作負載的選項,以便支持 DB2 UDB 數據的更高並發性級別。
鎖升級
DB2 UDB 使用升級技術來平衡鎖定性能開銷的並發性需求。當一個應用程序進程持有單個表或表空間上的大量頁面鎖、行鎖或 LOB 鎖時,DB2 UDB 就獲取該資源上的表或表空間鎖,然後釋放該資源上以前的頁面鎖、行鎖或 LOB 鎖。該過程稱作 鎖升級。
如果一個使用分區鎖定(帶有 LOCKPART YES 的 CREATE 或 ALTER)的表上發生鎖升級,那麼,就只升級當前被鎖定的分區,未鎖定的分區仍舊未鎖定。一旦表空間中發生了鎖升級,那麼就要用表空間鎖來鎖定隨後訪問的未鎖定分區。
在執行應用程序時,DB2 UDB 首先使用頁面鎖或行鎖,並且只要該進程訪問相對較少的頁面或行,還會繼續這樣做。當該應用程序訪問許多頁面或行時,DB2 UDB 將變為使用表鎖、表空間鎖或分區鎖。調用鎖升級的准確時間是由 LOCKSIZE 和 LOCKMAX.的值決定的。
LOCKSIZE
LOCKSIZE 是 CREATE/ALTER TABLESPACE 語句的選項,在應用程序進程訪問表空間中的表時,控制 DB2 UDB 獲取的何種類型的鎖(即,它決定該鎖的“大小”,這有時也稱作鎖粒度)。該選項的可以是 LOB、TABLESPACE、TABLE、PAGE、ROW 和 ANY。
CREATE TABLESPACE 語句的 LOCKSIZE 參數默認為 ANY。LOCKSIZE ANY 允許 DB2 UDB 選擇鎖大小。DB2 UDB 通常將 LOCKSIZE PAGE 用於非 LOB 的表,而將 LOCKSIZE TABLESPACE 用於 LOB 表。
我建議在創建表空間時使用該默認值,除非您有理由進行其他選擇。如果您選擇修改 LOCKSIZE,那麼就要根據使用該表空間的應用程序的性能監控結果和並發性特點來做決定。
使用何種大小的鎖
在 DB2 V4 中才開始可以使用行級鎖。之前,數據頁是最小的鎖定單元。I/T 行業中的許多人都假定行鎖是並發性問題的靈丹妙藥,但實際上,它並不能解決所有的並發性問題。在經歷許多鎖等待、死鎖和超時的環境中,它也許提供了較大的改進。但在其他情形下,DB2 UDB 可能在獲取更多鎖上消耗資源,同時無法成比例地提高並發性。
因為 IRLM 獲取、維護和釋放行鎖所需的處理與頁面鎖需要的大致相同,所以關於指定鎖大小的決定其實就是在較高的鎖定開銷與並發性的潛在提高之間進行權衡。
因此,至於是使用頁面鎖還是使用行鎖,這取決於您的數據和應用程序的特點。如果您覺察到使用頁面鎖定級別的表空間的數據頁上存在大量競爭,那麼就請考慮使用行鎖。通過在行級別而非頁面級別上進行鎖定,可以極大地減少與其他應用程序進程的競爭,特別在訪問是在隨機的情況下。
但是,如果多個應用程序正以不同的順序更新某一頁上的相同行,那麼行鎖導致的競爭甚至可能比頁面的還要多。這是因為,通過頁面鎖,第二個以及隨後的應用程序在訪問該頁面之前,都必須等待第一個應用程序完成,而它們就可能超時。通過行鎖,多個應用程序可以同時訪問同一頁上的行,但如果它們試圖訪問相同的行集,就可能死鎖。
使用 LOCKSIZE TABLESPACE 或 LOCKSIZE TABLE 之前,用戶必須相當確定沒有其他重要的應用程序進程需要並發訪問該對象。在任何表上指定 LOCKSIZE ROW 之前,極為明智的做法就是先對增加鎖開銷來提高並發性是否值得進行估計。
小型表
如果一個 DB2 UDB 表兼具尺寸小和高使用率的特點,那麼就考慮使用 LOCKSIZE ROW,特別是每個頁面中有許多行的時候。此外,因為表十分小,鎖粒度將不會給鎖開銷帶來較大的性能影響。
DB2 UDB 對象和授權的分組
通常總是將與同一應用程序邏輯相關的表分組到一個數據庫中。理想情況是,每一個應用程序進程都引用盡可能少的 DB2 UDB 數據庫。而且,要設法給用戶不同的授權 ID 來使用不同的 DB2 UDB 數據庫。實際上,這將增加可能應用程序進程的數目,同時可能減少每個應用程序進程可以訪問的數據庫數目。因此一般說來,“組合相似的事物”和“分開不同的事物”。
應用程序設計考慮
正如初始數據庫設計一樣,早在開發新的 DB2 UDB 應用程序之時就開始考慮並發性問題也很重要。更好的做法是,在實現之前,就將並發性原則包含到生產中去。然而,有時候並發性的提高可能取決於較新的 DB2 UDB 版本中的新功能以及與之接合的其他系統。在這些情況下,可能需要修改現有的應用程序,以便提高並發性水平。而在其他情況下,可能要等並發性問題發展和增長到一定的時候,才有必要對應用程序進行大量修改。以下是一些用以評估在某一時刻是開發新的 DB2 UDB 應用程序,還是修改現有的 DB2 UDB 應用程序的選項。
盡可能晚地訪問關鍵資源
例如,設計和編寫應用程序,以便 SQL 語句獲取和更新 DB2 UDB 數據的時間盡可能地接近提交點。注意該問題將減少鎖定數據的時間總量,從而減少其他應用程序進程無法使用該數據的時間。
盡可能快地提交工作
通常,在達到數據一致性臨界點之後,盡快發出 COMMIT 語句是一種較好的 SQL 編程技術。該實踐將有助於避免不必要的鎖競爭,甚至在只讀應用程序中也應該采用該實踐。類似地,我們建議應用程序中一檢測出 SQL 故障,就發出 ROLLBACK 語句。這將阻止未成功的 SQL 語句不必要地、過長時間地鎖定 DB2 UDB 資源。
盡可能快地關閉游標
盡可能快地關閉游標是另一個可以幫助提高並發性的編程實踐。通過盡快發出 CLOSE CURSOR 語句,應用程序將釋放那些鎖以及它們所持有的 DB2 UDB 資源。
應用程序重試邏輯
我建議應用程序中包含重試邏輯,即當程序接收死鎖或超時狀況的指示時,該應用程序代碼應重試該操作,甚至允許重試多次。這就使得應用程序有可能從該狀況中恢復,而無需操作人員從外部干涉或提供幫助。SQLCA 中的字段 SQLERRD(3) 返回一個理由碼,指示是否發生了死鎖或超時。