標籤:mongodb 事務 復原 兩部提交 rollback
執行兩步提交
概述這部分提供了多記錄更新或者多記錄事務,使用兩步提交來完成多記錄寫入的模板。另外,可以擴充此方法來提供rollback-like功能。
背景MongoDB對於單條記錄的操作是原子性的;但是涉及多條記錄的操作卻不是原子性的。由於記錄可能是相當複雜,並且有內嵌記錄,單記錄原子性操作提供了實際中常用的必要支援。
除了單記錄的原子性操作,還有許多情況需要多記錄操作事務,當執行一個包含一些列操作的事務時,就有以下要求:
原子性:如果一個操作失敗,事務中之前的操作需要復原到之前的狀態
一致性:如果一個重大失誤,比如網路故障,硬體故障,中斷了事務,資料庫必須能夠恢複到之前的狀態
對於需要多記錄操作的事務,可以在應用中實現兩步提交的方法,來提供多記錄更新支援。使用這種方法保證了一致性,並且萬一出現錯誤,事務的執行狀態是可恢複的。然而在這個過程中,記錄處於未定的資料和狀態。
注意:因為MongoDB只有單記錄操作是原子性的,兩步提交只能提供語義上的“類事務”功能。對於應用來說,使其能夠回到在兩步提交中的某個狀態的中間資料或者復原資料。
模板考慮以下情景:
要將資金從賬戶A轉移到賬戶B,在關係型資料庫中,可以在一個事務中從A中減去資金,同時在B中加上。在MongoDB中,可以類比兩步提交來獲得相同結果。
這個例子使用兩個集合
1.accounts,用於儲存賬戶資訊
2.transactions,用於儲存資金轉移事務的資訊
初始化賬戶資訊db.accounts.insert(
[
{ _id: "A", balance: 1000, pendingTransactions: [] },
{ _id: "B", balance: 1000, pendingTransactions: [] }
]
);
初始化轉賬記錄對於每次資金轉移操作,將轉賬資訊添加到transactions集合中,插入的記錄包含以下資訊:
source和destination欄位,引用自ccounts集合中的_id欄位
value欄位,聲明轉移數值
state欄位,表明當前轉移狀態,值可以是initial,pending, applied, done, canceling, 或者 canceled.
lastModified欄位,反應最後修改日期
從A轉賬100到B,初始化transactions記錄:
db.transactions.insert({ _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() });
使用兩步提交進行轉賬1.從transactions集合中,找到state為initial的記錄。此時transactions集合中只有一條記錄,即剛插入的那條。在包含其他記錄的集合中,除非你聲明了其他查詢條件,否則這個查詢將返回任何state為initial的記錄。
var t = db.transactions.findOne( { state: "initial" } );
在MongoDB的shell中輸入t,查看t的內容,類似於:
{ "_id" : 1, "source" : "A", "destination" : "B", "value" : 100, "state" : "initial", "lastModified":??}
2.更新事務狀態為pending
設定state為pending,lastModified為目前時間
db.transactions.update(
{ _id: t._id, state: "initial" },
{
$set: { state: "pending" },
$currentDate: { lastModified: true }
}
)
在更新操作中,state:‘initial‘確保沒有其他進程已經更新了這條記錄。如果nMatched和nModified是0,回到第一步,擷取一個新的事務,重新開始這個過程。
3.在兩個賬戶中應用該事務
使用update方法將事務t應用到兩個賬戶中。在更新條件中,包含條件pendingTransactions:{$ne:t._id},以避免重複應用該事務。
db.accounts.update(
{ _id: t.source, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } }
)
db.accounts.update(
{ _id: t.destination, pendingTransactions: { $ne: t._id } },
{ $inc: { balance: t.value }, $push: { pendingTransactions: t._id } }
)
從A帳號減去t.value,給B賬戶加上t.value,同時給每個賬戶的pendingTransactions數組添加事務id
4.更新事務狀態為applied
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "applied" },
$currentDate: { lastModified: true }
}
)
5.更新賬戶pendingTransactions數組
db.accounts.update(
{ _id: t.source, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{ $pull: { pendingTransactions: t._id } }
)
從兩個賬戶中移除已應用的事務。
6.更新事務狀態為done
db.transactions.update(
{ _id: t._id, state: "applied" },
{
$set: { state: "done" },
$currentDate: { lastModified: true }
}
)
從失敗情境中恢複資料事務最重要的不是以上這個例子提供的原型,而是當事務沒有完全執行成功的時候,從各種失敗情境中恢複資料的可能性。
恢複操作
兩步提交模型允許應用程式重新執行事務操作序列,以保證資料一致性。在程式啟動時,或者定時執行恢複操作,來抓取任何未完成的事務。
恢複到資料一致的狀態的時間取決於應用程式多久需要恢複每個事務。
以下恢複操作使用lastModified日期作為pending狀態的事務是否需要復原的標識符。如果pending或者applied狀態的事務在最近的30分鐘內沒有被更新,說明這些事務需要被恢複。可以使用不同的條件來決定是否需要恢複。
Pending狀態的事務恢複事務狀態在pending之後,applied之前
例:
擷取三十分鐘內未成功的事務記錄
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );
然後回到“3.在兩個賬戶中應用該事務”這一步
Applied狀態的事務例:
擷取三十分鐘內未成功的事務記錄
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );
然後回到“5.更新賬戶pendingTransactions數組”這一步
復原操作有些情況下,可能需要復原,或者撤銷操作,比如,應用程式需要取消事務,或者其中一個賬戶不存在或者被凍結。
Applied狀態的事務在“4.更新事務狀態為applied”這一步之後,不應該再復原事務,而是應該完成當前事務,然後建立一個新的事務來把資料修改回來。
Pending狀態的事務在“2.更新事務狀態為pending”這一步之後,“4.更新事務狀態為applied”這一步之前,可以通過以下步驟復原事務:
1.更新事務狀態為取消中
db.transactions.update(
{ _id: t._id, state: "pending" },
{
$set: { state: "canceling" },
$currentDate: { lastModified: true }
}
)
2.在兩個賬戶中取消操作
如果事務已經應用,需要回退這個事務以取消在兩個賬戶上的操作。在更新的條件中,包含pendingTransactions:t._id,以便在pending transaction已經被應用的時候更新賬戶。
更新目標賬戶,減去事務中給其增加的值,cong pendingTransactions數組中移除事務_id
db.accounts.update(
{ _id: t.destination, pendingTransactions: t._id },
{
$inc: { balance: -t.value },
$pull: { pendingTransactions: t._id }
}
)
如果pending transaction還沒有被應用到這個賬戶中,將不會有記錄匹配查詢條件。
3.更新事務狀態為已取消
db.transactions.update(
{ _id: t._id, state: "canceling" },
{
$set: { state: "cancelled" },
$currentDate: { lastModified: true }
}
)
更新事務狀態為cancelled來標誌事務已取消。
多應用情景由於事務的存在,多個應用可以同時建立和執行操作,而不會產生資料不一致或者衝突。在之前的例子中,更新或者復原記錄,包含state欄位的更新條件防止不同應用重複提交事務
例如,app1和app2同時擷取了一個在initial狀態的事務。app1在app2開始前提交了整個事務。當app2試圖更新事務狀態為pending的時候,包含state:‘initial‘的更新條件將不會匹配任何記錄,
同時nMatched和nModified將為0.這就表明app2需要回到第一步,重啟一個不同的事務過程。
當多個應用啟動並執行時候,關鍵在有只有一個應用可以及時處理指定的事務。這樣的話,即使在有符合更新條件的記錄,也可以在事務記錄中建立一個標記來標誌應用正在處理這個事務。使用findAndModify()方法來修改事務,並且回退。
t = db.transactions.findAndModify(
{
query: { state: "initial", application: { $exists: false } },
update:
{
$set: { state: "pending", application: "App1" },
$currentDate: { lastModified: true }
},
new: true
}
)
修正後的事務操作確保只有標識符匹配的應用可以提交該事務。
如果app1在事務執行中失敗,可以使用之前講的恢複操作,但是應用程式需要在應用事務之前確保“擁有”該事務。
例如,尋找並恢複pending狀態的job
var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find(
{
application: "App1",
state: "pending",
lastModified: { $lt: dateThreshold }
}
)
在生產環境中使用兩步提交以上的例子是有意寫的很簡單。例如,它假設一個賬戶的復原操作總是可能的,並且賬戶可以儲存負值。
生產環境可能會更加負值,通常來說,賬戶需要當前賬戶值,信用,欠款等多種資訊。
對於所有事務,要確保使用的是write concern許可權等級。
MongoDB 操作手冊CRUD 事務 兩步提交