云開發(fā) 原子操作和事務(wù)

2020-07-22 15:32 更新

使用更新指令(如 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ù)與ACID

事務(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è)處理必須一起成功一起失敗。

1.ACID

一般來說,事務(wù)是必須滿足4個(gè)條件(ACID): Atomicity(原子性)、Consistency(穩(wěn)定性)、Isolation(隔離性)、Durability(可靠性):

  • 原子性:整個(gè)事務(wù)中的所有操作要么全部提交成功,要么全部失敗回滾,對(duì)于一個(gè)事務(wù)來說,不可能只執(zhí)行其中一部分操作,

  • 一致性:事務(wù)的執(zhí)行不能破壞數(shù)據(jù)庫(kù)數(shù)據(jù)的完整性和一致性,一個(gè)事務(wù)在執(zhí)行前后,數(shù)據(jù)庫(kù)都必須處于一致性狀態(tài)。換句話說,事務(wù)的執(zhí)行結(jié)果必須是使數(shù)據(jù)庫(kù)從一個(gè)一致性狀態(tài)轉(zhuǎn)變到另一個(gè)一致性狀態(tài)。比如在執(zhí)行事務(wù)前,A用戶賬戶有50元,B用戶賬戶有150元;執(zhí)行B轉(zhuǎn)給A 50元事務(wù)后,兩個(gè)用戶賬戶總和還是200元。

  • 隔離性:事務(wù)的隔離性是指在并發(fā)環(huán)境中,當(dāng)不同的事務(wù)同時(shí)操縱相同的數(shù)據(jù)時(shí),每個(gè)事務(wù)都有各自的完整數(shù)據(jù)空間事務(wù)之間,互不干擾。比如在線銀行,同時(shí)轉(zhuǎn)賬的人雖然很多,但是不會(huì)出現(xiàn)影響A與B之間的轉(zhuǎn)賬;

  • 可靠性:即使發(fā)生系統(tǒng)崩潰或機(jī)器宕機(jī)等故障,只要數(shù)據(jù)庫(kù)能夠重新啟動(dòng),那么一定能夠?qū)⑵浠謴?fù)到事務(wù)成功結(jié)束時(shí)的狀態(tài),已提交事務(wù)的更新不會(huì)丟失。

2.云函數(shù)事務(wù)注意事項(xiàng)

(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ù)讀、幻讀等。

  • 臟讀:指當(dāng)一個(gè)事務(wù)正在訪問數(shù)據(jù),并且對(duì)數(shù)據(jù)進(jìn)行了修改,而這種修改還沒有提交到數(shù)據(jù)庫(kù)中,這時(shí),另外一個(gè)事務(wù)也訪問這個(gè)數(shù)據(jù),然后使用了這個(gè)數(shù)據(jù);

  • 不可重復(fù)讀:在一個(gè)事務(wù)內(nèi)兩次讀到的數(shù)據(jù)是不一樣的,受到另一個(gè)事務(wù)修改后提交的影響,因此稱為是不可重復(fù)讀

  • 幻讀:第一個(gè)事務(wù)對(duì)表進(jìn)行讀取,當(dāng)?shù)诙€(gè)事務(wù)對(duì)表進(jìn)行增加或刪除操作事務(wù)提交后,第一個(gè)事務(wù)再次讀取,會(huì)出現(xiàn)增加或減少行數(shù)的情況

云開發(fā)的數(shù)據(jù)庫(kù)系統(tǒng)的事務(wù)過程采用的是快照隔離(Snapshot isolation),可以避免并發(fā)操作帶來數(shù)據(jù)不一致的問題。

  • 事務(wù)期間,讀操作返回的是對(duì)象的快照,而非實(shí)際數(shù)據(jù)

  • 事務(wù)期間,寫操作會(huì):1. 改變快照,保證接下來的讀的一致性;2. 給對(duì)象加上事務(wù)鎖

  • 事務(wù)鎖:如果對(duì)象上存在事務(wù)鎖,那么:1. 其它事務(wù)的寫入會(huì)直接失敗;2. 普通的更新操作會(huì)被阻塞,直到事務(wù)鎖釋放或者超時(shí)

  • 事務(wù)提交后,操作完畢的快照會(huì)被原子性地寫入數(shù)據(jù)庫(kù)中

三、事務(wù)操作的兩套API

云開發(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)用)  

1.通過 runTransaction 回調(diào)獲得 transaction

以下提供一個(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。

2.通過 startTransaction 獲得transaction

  • db.startTransaction(),開啟一個(gè)新的事務(wù),之后即可進(jìn)行 CRUD 操作;

  • db.startTransaction().transaction.commit(),提交事務(wù)保存數(shù)據(jù),在提交之前事務(wù)中的變更的數(shù)據(jù)對(duì)外是不可見的;

  • db.startTransaction().rollback(),事務(wù)終止并回滾事務(wù),例如,一部分?jǐn)?shù)據(jù)更新失敗,對(duì)已修改過的數(shù)據(jù)也進(jìn)行回滾。

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ù)則主要是用于跨記錄和跨集合的處理。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)