# 事务

仅支持云函数端使用,wx-server-sdk 最低版本要求 1.7.0

# 介绍

如果原子操作符(如 incmuladdToSet)和嵌套记录的数据结构设计无法满足需求,需要更高可自定义的事务操作,如跨多个记录或跨多集合的原子操作时(比如两个账户之间转账),可以使用云数据库事务能力。

# 快照隔离

事务过程采用的是快照隔离,在快照隔离中会保证:

  1. 事务期间,读操作返回的是对象的快照,而非实际数据
  2. 事务期间,写操作会:1. 改变快照,保证接下来的读的一致性;2. 给对象加上事务锁
  3. 事务锁:如果对象上存在事务锁,那么:1. 其它事务的写入会直接失败;2. 普通的更新操作会被阻塞,直到事务锁释放或者超时
  4. 事务提交后,操作完毕的快照会被原子性地写入数据库中

# 单记录操作

在事务中不支持批量操作(where 语句),只支持单记录操作(collection.doc, collection.add),这可以避免大量锁冲突、保证运行效率,并且大多数情况下单记录操作足够满足需求,因为在事务中是可以对多个单个记录进行操作的,也就是可以比如说在一个事务中同时对集合 A 的记录 xy 两个记录操作、又对集合 B 的记录 z 操作。

# API

事务提供两种操作风格的接口,一个是简易的、带有冲突自动重试的 runTransaction 接口,一个是流程自定义控制的 startTransaction 接口。详细定义可参见 API 文档。

通过 runTransaction 回调中获得的参数 transaction 或通过 startTransaction 获得的返回值 transaction,我们将其类比为 db 对象,只是在其上进行的操作将在事务内的快照完成,保证原子性。transaction 上提供的接口树形图一览:

transaction
|-- collection       获取集合引用
|   |-- doc          获取记录引用
|   |   |-- get      获取记录内容
|   |   |-- update   更新记录内容
|   |   |-- set      替换记录内容
|   |   |-- remove   删除记录
|   |-- add          新增记录   
|-- rollback         终止事务并回滚
|-- commit           提交事务(仅在使用 startTransaction 时需调用)  

以下提供一个使用 runTransaction 接口的,两个账户之间进行转账的简易示例。注意使用 runTransaction 时,传入的回调即事务执行函数必须为 async 异步函数或返回 Promise 的函数,当事务执行函数返回时,SDK 会认为用户逻辑已完成,自动提交(commit)事务,因此务必确保用户事务逻辑完成后才在 async 异步函数中返回或 resolve Promise。同时在使用事务时建议初始化 db 时指定 throwOnNotFoundfalse,指定 false 后可使得 doc.get 在找不到记录时不抛出异常。

const cloud = require('wx-server-sdk')
cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
})
const db = cloud.database({
  // 该参数从 wx-server-sdk 1.7.0 开始支持,默认为 true,指定 false 后可使得 doc.get 在找不到记录时不抛出异常
  throwOnNotFound: false,
})
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)
          }
        })

        // 会作为 runTransaction resolve 的结果返回
        return {
          aaaAccount: aaaRes.data.amount - 10,
        }
      } else {
        // 会作为 runTransaction reject 的结果出去
        await transaction.rollback(-100)
      }
    })

    console.log(`transaction succeeded`, result)

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

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

# 隔离等级

目前数据库是快照隔离,没有串行化隔离,无法避免写偏(write skew)的情况。

# FAQ

部分环境的数据库可能会无法使用,可以在社区发帖,我们会有专人处理。当报错信息是 [object Object](callback err is not instance of Error)[BadRequest] Not Found 时,请到社区反馈。