# 事务

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

### 介绍

如果原子操作符（如 [`inc`](/minigame/dev/wxcloud/reference-sdk-api/database/command/Command.inc)、[`mul`](/minigame/dev/wxcloud/reference-sdk-api/database/command/Command.mul)、[`addToSet`](/minigame/dev/wxcloud/reference-sdk-api/database/command/Command.addToSet)）和嵌套记录的数据结构设计无法满足需求，需要更高可自定义的事务操作，如跨多个记录或跨多集合的原子操作时（比如两个账户之间转账），可以使用云数据库事务能力。

#### 快照隔离

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

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

#### 单记录操作

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

### API

事务提供两种操作风格的接口，一个是简易的、带有冲突自动重试的 [`runTransaction`](/minigame/dev/wxcloud/reference-sdk-api/database/Database.runTransaction) 接口，一个是流程自定义控制的 [`startTransaction`](/minigame/dev/wxcloud/reference-sdk-api/database/Database.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` 时指定 `throwOnNotFound` 为 `false`，指定 false 后可使得 doc.get 在找不到记录时不抛出异常。

```js
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

部分环境的数据库可能会无法使用，可以在社区发帖，我们会有专人处理。当报错信息是 `<a href="callback err is not instance of Error" target="_blank">object Object</a>` 或 `[BadRequest] Not Found` 时，请到社区反馈。
