本文將介紹關于事務以及Read/Write Concern的最佳實踐,幫助您更好地使用云數據庫 MongoDB 版的事務以及Read/Write Concern功能。
背景信息
MongoDB 4.0版本支持了單機事務(副本集事務),可以在副本集內的一個或多個集合進行事務操作。MongoDB 4.2版本支持了分布式事務(分片事務),可以跨多個分片執行多個集合的不同文檔事務操作。
在MongoDB中,對于對單個文檔的操作,系統始終保證其原子性。由于MongoDB文檔結構的靈活性,業務側總是可以使用嵌入式文檔和數組結構來構造聯系更緊密的單個文檔結構,而不是像傳統關系型數據庫那樣創建多個符合范式規則的集合并進行交互查詢或聯合更新。所以對于許多MongoDB的實際應用場景,在合理的數據建模下,單文檔原子性保證已經消除了對分布式事務的需求。
當然,一些特殊的應用場景(比如金融、會計等)依然對于分布式事務有著強烈的需求。在4.2以上版本完全支持分布式文檔以后,MongoDB也可以很好地支持這部分需求。
事務
基礎信息
MongoDB事務的使用和交互習慣與關系型數據庫的事務基本一致,API使用方法也類似,無額外的學習成本。
以下示例為一個完整事務,您可以看到API(startTransaction/abortTransaction/commitTransaction
)以及相關聯的session
和readConcern/writeConcern
設置。
// 創建集合
db.getSiblingDB("mydb1").foo.insertOne(
{abc: 0},
{ writeConcern: { w: "majority", wtimeout: 2000 } }
)
db.getSiblingDB("mydb2").bar.insertOne(
{xyz: 0},
{ writeConcern: { w: "majority", wtimeout: 2000 } }
)
// 開啟一個會話
session = db.getMongo().startSession( { readPreference: { mode: "primary" } } );
coll1 = session.getDatabase("mydb1").foo;
coll2 = session.getDatabase("mydb2").bar;
// 開啟一個事務
session.startTransaction( { readConcern: { level: "local" }, writeConcern: { w: "majority" } } );
// 在事務中執行若干操作
try {
coll1.insertOne( { abc: 1 } );
coll2.insertOne( { xyz: 999 } );
} catch (error) {
// 遇到問題時中止事務
session.abortTransaction();
throw error;
}
// 提交事務
session.commitTransaction();
session.endSession();
事務的使用說明如下:
事務需要與會話(session)關聯,一個會話同一時間只能有一個未完成事務。如果會話結束,其關聯的未完成事務也會回滾。
一個分布式事務可以同時包含對不同庫表的不同文檔的操作。
在事務執行期間,事務能夠讀取自己未提交的寫操作,但事務外部的其他操作不會讀取到事務中未提交的寫操作。
在事務提交之前,不會將未提交的寫入數據復制到從節點。一旦事務被提交,寫入的數據將被復制并自動應用于所有副本集中的從節點。
在修改文檔時,事務將鎖定文檔,使文檔無法被其他操作更改,直至事務完成。如果一個事務無法獲得它需要修改文檔上的鎖,可能是因為另一個事務已經持有該鎖,那么該事務將在5毫秒后立即終止(該時間由maxTransactionLockRequestTimeoutMillis內核參數控制),并提示寫沖突(WriteConflicts)。
事務有重試機制,如果遇到了暫時的可重試錯誤(比如網絡臨時中斷),事務會自動重試。而且客戶端對重試操作無感知。
事務有生命周期,運行超過60秒的事務將會被后臺線程強制終止(該時間由transactionLifetimeLimitSeconds內核參數控制)。
使用限制
分布式事務不能創建新的集合和索引。
事務不能寫入capped集合。
事務不能使用快照(snapshot)的Read Concern讀取capped集合(該限制存在于MongoDB 5.0及以上版本)。
事務不能讀寫
config/admin/local
庫里的集合。事務不能寫形如
system.*
的系統庫表。事務不支持
explain
。事務無法通過
getMore
讀取事務外創建的游標;事務外也無法通過getMore
讀取事務內創建的游標。事務內的第一個操作不能為
killCursors/hello
等。事務內無法執行非CURD的命令,包括
listCollections/listIndexes/createUser/getParameter/count
等。分布式事務不支持將分片上的writeConcernMajorityJournalDefault參數設置為false。
分布式事務不支持帶arbiters的分片。
最佳實踐
優先考慮使用單機事務而不是分布式事務
在大多數場景下,分布式事務的性能要差于單機事務或不使用事務的寫入,因為涉及到事務的操作需要有處理更多復雜場景的邏輯。在MongoDB中,非范式化的數據模型(指嵌入式文檔和數組結構)仍然是您數據建模的最佳選擇。合理的數據建模加單機事務完全可以處理絕大多數場景下應用的事務需求。
避免執行長事務
默認情況下,MongoDB將自動中止任何運行超過60秒的分布式事務。為了解決超時問題,您應該將事務分解為更小的部分,以便在配置的時間限制內執行。您還需要確保已經優化過查詢語句,查詢語句具有適當的索引覆蓋率,以便在事務中快速地訪問數據。
避免在一個事務中修改過多文檔
在一個事務中可以讀取的文檔數量沒有硬性限制,但修改的文檔數量太多時可能會增加主從同步的壓力,從而導致從節點數據同步落后或者其他問題。推薦在一個事務中修改的文檔數量不超過1000。對于需要修改超過1000個文檔的事務,建議您將該事務分解為分批處理文檔的多個事務。
避免執行超大事務(超出16 MB)
在MongoDB 4.0中,事務用單個oplog條目表示,oplog條目大小必須在16 MB以內。MongoDB事務的更新操作僅在oplog中存儲更新的增量內容(即更改的內容),而插入操作將存儲整個文檔。因此,事務中所有語句的oplog記錄組合必須小于16 MB,如果超過這個限制,事務將被中止并完全回滾。建議您將超大事務分解為較小的操作集,這些操作集可以用16 MB或更少的空間表示。
從MongoDB 4.2起,MongoDB開始支持創建多個oplog條目來存儲一個事務中的所有寫操作,相當于消除了單個超大事務16MB限制。但是依然建議您將事務的大小控制在16 MB以內,過大的事務還可能引起其他問題。
客戶端需要有合理處理事務回滾(abort)的邏輯
當事務異常中止時,將向驅動程序返回一個異常并回滾。您應該為應用程序添加捕獲并重試因臨時異常(如主從切換,節點故障等)而終止事務的邏輯。由于Retryable Writes機制,MongoDB驅動程序將自動重試事務的提交,但應用程序側依然需要處理那些無法被自動重試機制處理的事務異常及錯誤,包括TransactionTooLarge
、TransactionTooOld
、TransactionExceededLifetimeLimitSeconds
等錯誤。
避免在事務中執行任何DDL操作
DDL操作(如createIndex
或dropDatabase
)會被對應庫表正在運行的活動事務所阻塞。當DDL操作被阻塞時,所有嘗試訪問相同庫表的事務都將無法在限定時間內獲得鎖,從而導致新事務中止。
MongoDB 4.4及以后版本優化了相關限制(由shouldMultiDocTxnCreateCollectionAndIndexes參數控制),您可以在分布式事務中執行createCollection
或createIndex
操作,但上述操作依舊存在以下限制:
只能隱式創建。
只能對當前不存在的集合執行。
只能對空集合執行。
因此,建議您避免在事務中執行DDL操作。
盡可能早地主動回滾不打算提交的事務以及遇到報錯的事務
所有未提交事務所涉及的修改都會駐留在WiredTiger引擎緩存中。如果系統中同時有好幾個不打算提交的事務或遇到報錯的事務,可能會導致WiredTiger引擎的緩存面臨很大壓力,進而引起其他問題。您應盡量控制事務操作的時長,盡早回滾不會提交的事務來釋放資源。
若事務經常因為獲取鎖超時而回滾,可以適當調大相關超時參數
默認情況下,事務里的操作如果在5毫秒內獲取不到需要的鎖就會自動回滾。當一個事務回滾或者提交時,事務會釋放所有占用的鎖。如果經常遇到事務因為鎖獲取超時而回滾的情況,可以適當調大maxTransactionLockRequestTimeoutMillis參數的值來規避。
如果調大參數依然不能解決此問題,請您重新審視事務里的操作,檢查事務中是否包含了可能會長時間占用鎖的操作(比如DDL、待優化的查詢),并對其進行優化。
盡量避免在事務內外同時修改同一文檔而導致寫沖突
如果事務正在進行,事務外部的寫操作修改了一個文檔,而事務中的操作也試圖修改該文檔,事務將由于寫沖突(Write Conflicts)而回滾。如果事務正在進行,并且已經獲取了修改文檔需要的鎖,那么當事務外部的寫操作試圖修改該文檔時,外部寫操作將會等待,直到事務結束。
發生寫沖突時,事務外的寫操作既不會失敗也不會返回報錯給客戶端,MongoDB內部會不斷重試并且在writeConflicts
計數器上加一,直到成功為止。從客戶端的視角來看,操作并沒有異常,只是請求耗時比較久。
少量的寫沖突一般不會產生很大影響,但是如果存在大量的寫沖突,則有可能導致數據庫性能退化。您可以通過審計日志或慢日志確認是否存在寫沖突過多的問題。
內核缺陷風險說明
創建長時間運行的大事務,或者試圖在單個事務中執行過多的操作,都會給WiredTiger存儲引擎的緩存帶來很大壓力。因為自最早的已創建未提交事務起,WiredTiger緩存必須能為所有后續的寫入維持相關數據和狀態。由于事務在運行時使用相同的快照,因此,在整個事務運行期間,新的寫操作會持續累積在WiredTiger緩存中。當前運行在舊快照上的事務在提交或中止之前,緩存中的這些寫操作都不能被逐出。而長事務引起的WiredTiger緩存壓力超載(wt cache使用率以及dirty使用率超閾值)通常會帶來更多的問題,包括數據庫卡頓、請求延時大幅增加、CPU使用率滿等問題,甚至出現“死鎖”,導致業務受損。更多關于內核風險的介紹,請參見SERVER-50365和SERVER-51281。
云數據庫 MongoDB 版建議所有重度使用事務的業務都將MongoDB實例升級至5.0及以上的版本來規避相關風險和隱患。
Read Concern
基礎信息
控制一致性和隔離級別的Read Concern包括以下幾種:
"local"
:副本集架構下讀主或從節點時的默認級別,從本地讀取,可能會讀到被回滾的數據。"available"
:分片集群架構下讀從節點時的默認級別,可能讀到會被回滾的數據。讀數據前不會進行shardVersion的檢查,因此可能讀到孤立文檔。優點是提供了最優的訪問延遲。"majority"
:讀取的是已被大多數節點確認的數據,即不會被回滾的數據。"linearizable"
:數據一致性要求最嚴格的線性化級別,讀操作需要等待所有前序寫入都已經被大多數節點確認。性能最差,只能在主節點上使用。"snapshot"
:基于快照讀取,同樣讀取的是已被大多數節點確認的數據,只不過可以關聯某一個特定時間點的快照,比如配合atClusterTime使用。
Read Concern的使用說明如下:
無論Read Concern的級別如何,某一個mongod節點上最新的數據都不代表副本集中最近版本的數據。
可以為不同的操作指定不同的Read Concern,4.4及以上版本也可以設置服務器端默認的Read Concern,操作的Read Concern優先級高于服務器端設置的Read Concern。
當讀取
local
庫時,您指定的Read Concern會被忽略,您總是能在local
庫里讀到所有本地數據。分布式事務里僅支持3種Read Concern級別,分別為
"local"
、"majority"
、"snapshot"
。因果一致性會話里,必須使用
"majority"
級別的Read Concern。
最佳實踐
對于分布式事務,只需設置事務的Read Concern,無需設置事務里每個操作
不需要為事務里的每個操作指定Read Concern。事務級別的Read Concern會覆蓋其他地方設置或默認的Read Concern。
與Write Concern一樣,Read Concern可以應用于對數據庫執行的任何查詢,而不管操作是對單個或一組文檔進行常規讀取,還是封裝在多文檔讀取事務中。
一般場景盡量使用"majority"
級別的Read Concern
為了確保隔離和一致性,建議將Read Concern級別設置為"majority"
,只有當數據被復制到副本集中的大多數節點時,應用程序才能讀取到該數據,因此在選舉新的主節點時,數據不會回滾。
Read Your Own Write的場景需要讀主并使用"local"
或"linearizable"
級別的Read Concern
為了能在寫操作完成后能盡快讀到寫入的修改,需要讀主并使用"local"
或"linearizable"
的Read Concern。當配套的Write Concern為"majority"
時,也可以使用"majority"
的Read Concern。
從MongoDB 3.6版本開始,您也可以使用因果一致性會話來滿足此場景。
一致性要求最強的場景需要配套maxTimeMS
超時來使用"linearizable"
的Read Concern
級別為"linearizable"
的Read Concern可以確保在讀取時節點仍然是副本集的主節點,并且如果隨后另一個節點被選為新的主節點,則它返回的數據不會被回滾。而這個級別會對延遲產生重大影響,因此需要配套使用maxTimeMS
超時來避免大多數節點不可用的情況下讀操作被無限期地阻塞。
Write Concern
基礎信息
指定Write Concern的格式如下。關于Write Concern的更多介紹,請參見Write Concern。
{ w: <value>, j: <boolean>, wtimeout: <number> }
控制數據持久化保證級別的Write Concern可以簡單分為以下幾種主要級別:
{w: 0}
:表示寫(write)不確認,不確認寫操作是否完成,可能發生寫入數據丟失。{w: 1}
:表示寫(write)確認,為MongoDB 5.0版本以前的默認行為。確認寫操作在內存中已完成,但由于還沒有持久化,依然可能發生數據丟失。{j: true}
:表示日志(journal)確認。確認寫操作已完成并刷到持久化存儲的WAL中,寫操作不會丟失。{ w: "majority" }
:表示大多數(majority),為MongoDB 5.0及以上版本的默認行為。等待寫操作被復制到副本集中大多數節點上后才確認,數據不會被回滾。副本確認:等待寫操作被復制到副本集中指定數量的節點上后才確認。
自定義確認:可以通過settings.getLastErrorModes參數指定其他自定義的帶tag確認方式。
Write Concern的使用說明如下:
您可以在任何寫操作或者事務中指定Write Concern,未顯示指定時會使用默認值。
說明從MongoDB 5.0版本開始,標準3副本拓撲結構下,默認的全局Write Concern已經從之前的
{w:1}
調整為{w:"majority"}
。這可能會導致您將數據庫版本升級至5.0及以上版本后出現性能退化問題。副本集中的隱藏(Hidden)節點、延遲(Delayed)節點、或其他優先級為0的可投票節點均可以視為
"majority"
中的一員。您可以為不同的操作指定不同的Write Concern,4.4及以上版本也可以設置服務器端默認的Write Concern,操作的Write Concern優先級高于服務器端設置的Write Concern。
當寫入
local
庫時,您指定的Write Concern會被忽略。因果一致性會話里,必須使用
"majority"
的Write Concern。
最佳實踐
對于分布式事務而言,只需設置事務的Write Concern,無需設置事務里每個操作
為事務內的各個寫入操作設置Write Concern會返回錯誤。
一般場景盡量使用"majority"
的Write Concern
“majority”
的Write Concern可以確保副本集中大部分節點已經確認寫入操作,即便此時發生節點故障或異常切換也不會產生數據丟失或者回滾的風險。
對寫入性能要求高的情況酌情考慮使用{w:1}的Write Concern,并關注從節點復制延遲
{w:1}
的Write Concern通常能帶來更好的寫入性能,適合重寫入的場景。但應合理關注監控中的從節點復制延遲,當延遲過大時可能會出現主節點異常ROLLBACK的問題。而且當復制延遲超過了oplog的保留時長后,從節點將進入異常的RECOVERING狀態且無法自愈,降低實例可用性,應優先關注并處理。
云數據庫 MongoDB 版5.0以下版本進行批量灌數據或DTS遷移時可能會遇到上述延遲過大出現異常的問題,出現該問題時,建議您使用"majority"
的Write Concern。
對于不同操作設置最適合的Write Concern
Write Concern可以在單個操作的粒度上進行設置。業務側可以根據實際操作的需要來設置不同的Write Concern。比如金融交易數據使用帶Write Concern的事務來確保原子性;核心玩家數據使用"majority"
的Write Concern確保不會被回滾;日志數據使用默認或者{w:1}
的Write Concern即可。
MongoDB在設計上為開發者提供了極強的靈活性,讓不同的業務都可以根據自己的需要來設置合理的選項。