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