使用更新指令(如 inc、mul、addToSet)可以對云數(shù)據(jù)庫的一條記錄和記錄內(nèi)的子文檔(結(jié)合反范式化設(shè)計)進(jìn)行原子操作,但是如果要跨多個記錄或跨多個集合的原子操作時,就需要使用云數(shù)據(jù)庫的事務(wù)能力。
關(guān)系型數(shù)據(jù)庫是很難做到通過一個語句對數(shù)據(jù)強(qiáng)制一致性的需求來表示的,只能依賴事務(wù)。但是云開發(fā)數(shù)據(jù)庫由于可以反范式化設(shè)計內(nèi)嵌子文檔,以及更新指定可以對單個記錄或同一個記錄內(nèi)的子文檔進(jìn)行原子操作,所以通常情況下,云開發(fā)數(shù)據(jù)庫不必使用事務(wù)。
比如調(diào)整某個訂單項目的數(shù)量之后,應(yīng)該同時更新該訂單的總費(fèi)用,我們可以設(shè)計采用如下方式設(shè)計該集合,比如訂單的集合為order:
{
"_id": "2020030922100983",
"userID": "124785",
"total":117,
"orders": [{
"item":"蘋果",
"price":15,
"number":3
},{
"item":"火龍果",
"price":18,
"number":4
}]
}
客戶在下單的時候經(jīng)常會調(diào)整訂單內(nèi)某個商品比如蘋果的購買數(shù)量,而下單的總價又必須同步更新,不能購買數(shù)量減少了,但是總價不變,這兩個操作必須同時進(jìn)行,如果是使用關(guān)系型數(shù)據(jù)庫,則需要先通過兩次查詢,更新完數(shù)據(jù)之后,再存儲進(jìn)數(shù)據(jù)庫,這個很容易出現(xiàn)有的成功,有的沒有成功的情況。但是云開發(fā)的數(shù)據(jù)庫則可以借助于更新指令做到一條更新來實(shí)現(xiàn)兩個數(shù)據(jù)同時成功或失?。?/p>
db.collection('order').doc('2020030922100983')
.update({
data: {
"orders.0.number": _.inc(1),
"total":_.inc(15)
}
})
這個操作只是在單個記錄里進(jìn)行,那要實(shí)現(xiàn)跨記錄要進(jìn)行原子操作呢?更新指令其實(shí)是可以做到事務(wù)仿真的,但是比較麻煩,這時就建議用事務(wù)了。
事務(wù)就是一段數(shù)據(jù)庫語句的批處理,但是這個批處理是一個atom(原子),多個增刪改的操作是綁定在一起的,不可分割,要么都執(zhí)行,要么回滾(rollback)都不執(zhí)行。比如銀行轉(zhuǎn)賬,需要做到一個賬戶的錢匯出去了,那另外一個賬戶就一定會收到錢,不能錢匯出去了,但是錢沒有到另外一個的賬上;也就是要執(zhí)行轉(zhuǎn)賬這個事務(wù),會對A用戶的賬戶數(shù)據(jù)和B用戶的賬戶數(shù)據(jù)做增刪改的處理,這兩個處理必須一起成功一起失敗。
一般來說,事務(wù)是必須滿足4個條件(ACID): Atomicity(原子性)、Consistency(穩(wěn)定性)、Isolation(隔離性)、Durability(可靠性):
(1)不支持批量操作,只支持單記錄操作
在事務(wù)中不支持批量操作(where 語句),只支持單記錄操作(collection.doc, collection.add),這可以避免大量鎖沖突、保證運(yùn)行效率,并且大多數(shù)情況下,單記錄操作足夠滿足需求,因?yàn)樵谑聞?wù)中是可以對多個單個記錄進(jìn)行操作的,也就是可以比如說在一個事務(wù)中同時對集合 A 的記錄 x 和 y 兩個記錄操作、又對集合 B 的記錄 z 操作。
(2)云數(shù)據(jù)庫采用的是快照隔離
對于兩個并發(fā)執(zhí)行的事務(wù)來說,如果涉及到操作同一條記錄的時候,可能會發(fā)生問題。因?yàn)椴l(fā)操作會帶來數(shù)據(jù)的不一致性,包括臟讀、不可重復(fù)讀、幻讀等。
云開發(fā)的數(shù)據(jù)庫系統(tǒng)的事務(wù)過程采用的是快照隔離(Snapshot isolation),可以避免并發(fā)操作帶來數(shù)據(jù)不一致的問題。
云開發(fā)數(shù)據(jù)庫的事務(wù)提供兩種操作風(fēng)格的接口,一個是簡易的、帶有沖突自動重試的 runTransaction 接口,一個是流程自定義控制的 startTransaction 接口。通過 runTransaction 回調(diào)中獲得的參數(shù) transaction 或通過 startTransaction 獲得的返回值 transaction,我們將其類比為 db 對象,只是在其上進(jìn)行的操作將在事務(wù)內(nèi)的快照完成,保證原子性。transaction 上提供的接口樹形圖一覽:
transaction
|-- collection 獲取集合引用
| |-- doc 獲取記錄引用
| | |-- get 獲取記錄內(nèi)容
| | |-- update 更新記錄內(nèi)容
| | |-- set 替換記錄內(nèi)容
| | |-- remove 刪除記錄
| |-- add 新增記錄
|-- rollback 終止事務(wù)并回滾
|-- commit 提交事務(wù)(僅在使用 startTransaction 時需調(diào)用)
以下提供一個使用 runTransaction 接口的,兩個賬戶之間進(jìn)行轉(zhuǎn)賬的簡易示例。事務(wù)執(zhí)行函數(shù)由開發(fā)者傳入,函數(shù)接收一個參數(shù) transaction,其上提供 collection 方法和 rollback 方法。collection 方法用于取數(shù)據(jù)庫集合記錄引用進(jìn)行操作,rollback 方法用于在不想繼續(xù)執(zhí)行事務(wù)時終止并回滾事務(wù)。
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
const _ = db.command
exports.main = async (event) => {
try {
const result = await db.runTransaction(async transaction => {
const aaaRes = await transaction.collection('account').doc('aaa').get()
const bbbRes = await transaction.collection('account').doc('bbb').get()
if (aaaRes.data && bbbRes.data) {
const updateAAARes = await transaction.collection('account').doc('aaa').update({
data: {
amount: _.inc(-10)
}
})
const updateBBBRes = await transaction.collection('account').doc('bbb').update({
data: {
amount: _.inc(10)
}
})
console.log(`transaction succeeded`, result)
return {
aaaAccount: aaaRes.data.amount - 10,
}
} else {
await transaction.rollback(-100)
}
})
return {
success: true,
aaaAccount: result.aaaAccount,
}
} catch (e) {
console.error(`事務(wù)報錯`, e)
return {
success: false,
error: e
}
}
}
事務(wù)執(zhí)行函數(shù)必須為 async 異步函數(shù)或返回 Promise 的函數(shù),當(dāng)事務(wù)執(zhí)行函數(shù)返回時,SDK 會認(rèn)為用戶邏輯已完成,自動提交(commit)事務(wù),因此務(wù)必確保用戶事務(wù)邏輯完成后才在 async 異步函數(shù)中返回或 resolve Promise。
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database({
throwOnNotFound: false,
})
const _ = db.command
exports.main = async (event) => {
try {
const transaction = await db.startTransaction()
const aaaRes = await transaction.collection('account').doc('aaa').get()
const bbbRes = await transaction.collection('account').doc('bbb').get()
if (aaaRes.data && bbbRes.data) {
const updateAAARes = await transaction.collection('account').doc('aaa').update({
data: {
amount: _.inc(-10)
}
})
const updateBBBRes = await transaction.collection('account').doc('bbb').update({
data: {
amount: _.inc(10)
}
})
await transaction.commit()
return {
success: true,
aaaAccount: aaaRes.data.amount - 10,
}
} else {
await transaction.rollback()
return {
success: false,
error: `rollback`,
rollbackCode: -100,
}
}
} catch (e) {
console.error(`事務(wù)報錯`, e)
}
}
也就是說對于多用戶同時操作(主要是寫)數(shù)據(jù)庫的并發(fā)處理問題,我們不僅可以使用原子更新,還可以使用事務(wù)。其中原子更新主要用戶操作單個記錄內(nèi)的字段或單個記錄里內(nèi)嵌的數(shù)組對象里的字段,而事務(wù)則主要是用于跨記錄和跨集合的處理。
更多建議: