用【庫存】看懂雲開發資料庫事務

騰訊雲開發TCB發表於2020-05-31

在正常使用資料庫(CRUD)的情況下,這些操作都會順利進行所有資料都會被成功更新,由於某些特定的業務場景,需要進行一系列的操作,在這過程中必須保證每一步的操作都正常執行,如果任何一個環節出了差錯,比如更新庫存資訊發生異常,這終將會導致資料庫的資訊混亂而不可預測,資料庫事務正是用來保證這種一系列操作的穩定性技術

什麼是事務?

資料庫事務是資料庫管理系統執行過程中的一個邏輯單位,由一個有限的資料庫操作序列構成,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部資料庫操作組成。

事務的特性

ACID,指資料庫事務正確執行的四個主要特性的縮寫,一個事務,必需要具有這四種基本特性,否則在事務過程當中無法保證資料的正確性

1:原子性(Atomicity)

指的是一個事務內所有操作共同組成一個原子包,要麼全部成功,要麼全部失敗。

假如在資料庫中對一個屬性進行了更新,但是執行到一半的時候出現了異常,這樣就可能使得操作後的資料與我們預期的資料不同,所以原子性要求你這個方法要麼全部執行成功,要麼全部失敗

2:一致性(Consistency)

指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。

在原子性中規定方法中的操作都執行或者都不執行,但並沒有說要所有操作一起執行,所以操作的執行也是有先後順序的,那我們要是在執行一半時查詢資料庫,那我們會得到中間的更新的屬性?一致性規定提交前後只存在兩個狀態,提交前的狀態和提交後的狀態

3:隔離性(Isolation)

指的是資料庫允許多個併發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料的不一致。

多個事務可能操作同一資料庫資源,不同的事務為了保證隔離性,如果沒有隔離會造成幾種問題

  1. 事務A讀到事務B修改卻未提交的資料,事務B回滾資料修改操作,導致了事務A獲得資料是髒資料

  2. 事務A先讀取資料,事務B對資料進行修改,事務B再一次讀取該行資料時就會造成前後兩次讀取結果不一致

  3. 事務A讀取資料,事務B對其進行操作時,當事務A重新讀取該段資料時會造成前後兩次查詢的資料不一致的現象

目前雲開發資料庫使用的是快照隔離,具體將在下面進行介紹

4:永續性(Durability)

事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失

如果沒有永續性的特性,一旦資料庫出現異常,資料將會丟失

擁有永續性事務一旦提交後,資料庫中的資料必須被永久的儲存下來,即使伺服器系統崩潰或伺服器當機等故障,只要資料庫重新啟動,那麼一定能夠將其恢復到事務成功結束後的狀態。

雲開發資料庫事務

介紹

雲開發資料庫本身有提供(如 inc、mul、addToSet)等原子性操作符號和巢狀記錄的資料結構設計,如跨多個記錄或跨多集合的原子操作時,可以使用雲資料庫事務能力。

隔離性

雲開發資料庫事務過程中採用的快照隔離級別(snapshot),在事務期間,讀操作返回的是物件的快照,而非實際資料,事務期間寫操作執行時:

  1. 改變快照,保證接下來的讀的一致性;
  2. 給物件加上事務鎖

事務鎖

資料物件存在事務鎖對資料寫入的影響:

  1. 其它事務的寫入會直接失敗;
  2. 普通的更新操作會被阻塞,直到事務鎖釋放或者超時事務提交後,操作完畢的快照會被原子性地寫入資料庫中

單記錄操作

雲開發資料庫事務中不支援批量操作,只支援單記錄操作比如(collection.doc, collection.add),單記錄操作可避免大量鎖衝突、保證執行效率,並且大多數情況下單記錄操作足夠滿足需求,因為在事務中是可以對多個單個記錄進行操作的,也就是可以在一個事務中同時對集合 A 的記錄 x 和 y 兩個記錄操作、又對集合 B 的記錄 z 操作,接下來會通過小示例來進行演示。

事務 API

雲開發資料庫事務提供兩種操作風格的介面,一個是簡易的、帶有衝突自動重試的runTransaction介面,一個是流程自定義控制的startTransaction介面。

使用小示例

假設有以下場景:

某倉庫有1000箱醫用口罩,A醫院需要800箱、B醫院需要300箱並提交申請,倉庫的管理模式是先收到提交申請在進行庫存商品確認完畢後,進行領用。

在無事務的情況下虛擬碼自上而下執行

const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database()
const _ = db.command


exports.main = async (event, context) => {
// 醫院
 await db.collection('resource').doc('A'||'B')
    .update({
      data: {
        resource: _.inc(-800||-300)
      },
    })

// 倉庫
 await db.collection('store').doc('store')
    .update({
      data: {
        resource: _.inc(+800||+300),
      },
    })
}
// 判斷是否滿足要求
if('倉庫庫存' >'領用數量' ){
await db.collection('store').doc('store')
    .update({
      data: {
        count:_inc(-800||-300),
      },
    })
 }eles{

    '回退的業務邏輯'
       
 }

}


根據以上的程式碼執行結果來看:

  1. A/B醫院提交了領用口罩的申請;
  2. 倉庫接收了B醫院提交的申請;
  3. 判斷是否符合數量要求

執行到3時候發現倉庫庫存,並不能滿足醫院的領取要求時,需要將提交申請退還給醫院,並處理一些退回的邏輯。

該情況下需要處理操作量大、複雜度高、在高併發的執行情況下會導致一些具體的操作沒有完成比如:

  1. 醫院提交了申請,倉庫並沒有收到;

  2. 醫院提交了申請,倉庫收到申請,並沒有執行發放,也沒有退還給醫院;

事務的情況下虛擬碼自上而下執行

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 result = await db.runTransaction(async transaction => {
     
      const resource = await transaction.collection('resource').doc('A'||'B').get()
      const store = await transaction.collection('store').doc('store').get()

        const updateResource = await transaction.collection('resource').doc('A'||'B').update({
          data: {
           resource: _.inc(-800||-300)
          }
        })

        const updateStoreResource = await transaction.collection('store').doc('store').update({
          data: {
           resource: _.inc(+800||+300),
          }
        })
        
        if(store.data.count > 800||300){
         const updateStoreCount = await transaction.collection('store').doc('store').update({
              data: {
                 count:_inc(-800||-300),
                  }
                })
         // 會作為 runTransaction resolve 的結果返回
          return {
              resourceAccount: resource.data.count + 800||300,
            }

        }else{

        // 會作為 runTransaction reject 的結果出去
        await transaction.rollback('領取失敗')

        }
       
    })

    return {
      success: true,
      resourceAccount: result.resourceAccount,
    }
  } catch (e) {
    console.error(`transaction error`, e)

    return {
      success: false,
      error: e
    }
  }
}


根據以上的程式碼執行結果來看:

1.首先讀取了A/B醫院與倉庫的記錄快照;
2.醫院提交申請,減少對應的數量;
3.倉庫接收到醫院的提交申請;
4.判斷倉庫中的數量是否滿足本次領取的數量;

執行到4時候發現倉庫庫存,並不能滿足醫院的領取要求時,事務會將所有更改的記錄還原到讀取記錄快照時的資料,也就是說這些執行步驟要不就都成功,要不就都失敗,資料回滾,不需要過多的回退邏輯

未使用事務 VS 使用事務

未使用事務

  1. 由於操作量大,複雜度高,在加上出現高併發的情況就會有資料不一致的情況出現;
  2. 回退邏輯複雜;

使用事務

  1. 事務由一個有限的資料庫操作序列構成,這些操作要麼全部執行,要麼全部不執行,保證了資料一致性;
  2. 在執行事務之後保留了資料物件的快照,執行中出現任何問題可直接回滾;

總結

在使用雲開發資料庫中,如果僅僅是涉及單記錄的修改,完全可以使用如 inc、mul、addToSet)等原子性操作符號,涉及到跨集合以及多個記錄同時修改並需要保證一致性的情況,那事務功能將是最好的選擇。

公眾號:騰訊云云開發

騰訊云云開發:https://cloudbase.net

雲開發控制檯:https://console.cloud.tencent.com/tcb?from=12304


更多精彩
掃描二維碼瞭解更多

img

相關文章