相信使用過主流的關係型數據庫的朋友對「事務(Transactions)」不會太陌生,它可讓咱們把對多張表的屢次數據庫操做整合爲一次原子操做,這在高併發場景下能夠保證多個數據操做之間的互不干擾;而且一旦在這些操做過程任一環節中出現了錯誤,事務會停止而且讓數據回滾,這使得同時在多張表中修改數據的時候保證了數據的一致性。javascript
之前 MongoDB 是不支持事務的,所以開發者在須要用到事務的時候,不得不借用其餘工具,在業務代碼層面去彌補數據庫的不足。隨着 4.0 版本的發佈,MongoDB 也爲咱們帶來了原生的事務操做,下面就讓咱們一塊兒來認識它,並經過簡單的例子瞭解如何去使用。html
副本集是 MongoDB 的一種主副節點架構,它使數據獲得最大的可用性,避免單點故障引發的整個服務不能訪問的狀況的發生。目前 MongoDB 的多表事務操做僅支持在副本集上運行,想要在本地環境安裝運行副本集能夠藉助一個工具包——run-rs,如下的文章中有詳細的使用說明:前端
事務和會話(Sessions)關聯,一個會話同一時刻只能開啓一個事務操做,當一個會話斷開,這個會話中的事務也會結束。git
在當前會話中開始一次事務,事務開啓後就能夠開始進行數據操做。在事務中執行的數據操做是對外隔離的,也就是說事務中的操做是原子性的。github
提交事務,將事務中對數據的修改進行保存,而後結束當前事務,一次事務在提交以前的數據操做對外都是不可見的。redis
停止當前的事務,並將事務中執行過的數據修改回滾。mongodb
當事務運行中報錯,catch 到的錯誤對象中會包含一個屬性名爲 errorLabels 的數組,當這個數組中包含如下2個元素的時候,表明咱們能夠從新發起相應的事務操做。數據庫
通過上面的鋪墊,你是否是已經火燒眉毛想知道究竟應該怎麼寫代碼去完成一次完整的事務操做?下面咱們就簡單寫一個例子:json
場景描述: 假設一個交易系統中有2張表——記錄商品的名稱、庫存數量等信息的表 commodities,和記錄訂單的表 orders。當用戶下單的時候,首先要找到 commodities 表中對應的商品,判斷庫存數量是否知足該筆訂單的需求,是的話則減去相應的值,而後在 orders 表中插入一條訂單數據。在高併發場景下,可能在查詢庫存數量和減小庫存的過程當中,又收到了一次新的建立訂單請求,這個時候可能就會出問題,由於新的請求在查詢庫存的時候,上一次操做還未完成減小庫存的操做,這個時候查詢到的庫存數量多是充足的,因而開始執行後續的操做,實際上可能上一次操做減小了庫存後,庫存的數量就已經不足了,因而新的下單請求可能就會致使實際建立的訂單數量超過庫存數量。
以往要解決這個問題,咱們能夠用給商品數據「加鎖」的方式,好比基於 Redis 的各類鎖,同一時刻只容許一個訂單操做一個商品數據,這種方案能解決問題,缺點就是代碼更復雜了,而且性能會比較低。若是用數據庫事務的方式就能夠簡潔不少:
commodities 表數據(stock 爲庫存):
{ "_id" : ObjectId("5af0776263426f87dd69319a"), "name" : "滅霸原味手套", "stock" : 5 }
{ "_id" : ObjectId("5af0776263426f87dd693198"), "name" : "雷神專用鐵錘", "stock" : 2 }
複製代碼
orders 表數據:
{ "_id" : ObjectId("5af07daa051d92f02462644c"), "commodity": ObjectId("5af0776263426f87dd69319a"), "amount": 2 }
{ "_id" : ObjectId("5af07daa051d92f02462644b"), "commodity": ObjectId("5af0776263426f87dd693198"), "amount": 3 }
複製代碼
經過一次事務完成建立訂單操做(mongo Shell):
// 執行 txnFunc 而且在遇到 TransientTransactionError 的時候重試
function runTransactionWithRetry(txnFunc, session) {
while (true) {
try {
txnFunc(session); // 執行事務
break;
} catch (error) {
if (
error.hasOwnProperty('errorLabels') &&
error.errorLabels.includes('TransientTransactionError')
) {
print('TransientTransactionError, retrying transaction ...');
continue;
} else {
throw error;
}
}
}
}
// 提交事務而且在遇到 UnknownTransactionCommitResult 的時候重試
function commitWithRetry(session) {
while (true) {
try {
session.commitTransaction();
print('Transaction committed.');
break;
} catch (error) {
if (
error.hasOwnProperty('errorLabels') &&
error.errorLabels.includes('UnknownTransactionCommitResult')
) {
print('UnknownTransactionCommitResult, retrying commit operation ...');
continue;
} else {
print('Error during commit ...');
throw error;
}
}
}
}
// 在一次事務中完成建立訂單操做
function createOrder(session) {
var commoditiesCollection = session.getDatabase('mall').commodities;
var ordersCollection = session.getDatabase('mall').orders;
// 假設該筆訂單中商品的數量
var orderAmount = 3;
// 假設商品的ID
var commodityID = ObjectId('5af0776263426f87dd69319a');
session.startTransaction({
readConcern: { level: 'snapshot' },
writeConcern: { w: 'majority' },
});
try {
var { stock } = commoditiesCollection.findOne({ _id: commodityID });
if (stock < orderAmount) {
print('Stock is not enough');
session.abortTransaction();
throw new Error('Stock is not enough');
}
commoditiesCollection.updateOne(
{ _id: commodityID },
{ $inc: { stock: -orderAmount } }
);
ordersCollection.insertOne({
commodity: commodityID,
amount: orderAmount,
});
} catch (error) {
print('Caught exception during transaction, aborting.');
session.abortTransaction();
throw error;
}
commitWithRetry(session);
}
// 發起一次會話
var session = db.getMongo().startSession({ readPreference: { mode: 'primary' } });
try {
runTransactionWithRetry(createOrder, session);
} catch (error) {
// 錯誤處理
} finally {
session.endSession();
}
複製代碼
上面的代碼看着感受不少,其實 runTransactionWithRetry 和 commitWithRetry 這兩個函數都是能夠抽離出來成爲公共函數的,不須要每次操做都重複書寫。用上了事務以後,由於事務中的數據操做都是一次原子操做,因此咱們就不須要考慮分佈併發致使的數據一致性的問題,是否是感受簡單了許多?
你可能注意到了,代碼中在執行 startTransaction 的時候設置了兩個參數——readConcern 和 writeConcern,這是 MongoDB 讀寫操做的確認級別,在這裏用於在副本集中平衡數據讀寫操做的可靠性和性能,若是在這裏展開就太多了,因此感興趣的朋友建議去閱讀官方文檔瞭解一下:
readConcern:
writeConcern:
文 / Tony段
本文已由做者受權發佈,版權屬於創宇前端。歡迎註明出處轉載本文。本文連接:knownsec-fed.com/2018-08-24-…
想要訂閱更多來自知道創宇開發一線的分享,請搜索關注咱們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,咱們會盡量回復。
感謝您的閱讀。