事务

仅支持云函数端使用,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 上提供的接口树形图一览:

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

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

  1. const cloud = require('wx-server-sdk')
  2. cloud.init({
  3. env: cloud.DYNAMIC_CURRENT_ENV
  4. })
  5. const db = cloud.database({
  6. // 该参数从 wx-server-sdk 1.7.0 开始支持,默认为 true,指定 false 后可使得 doc.get 在找不到记录时不抛出异常
  7. throwOnNotFound: false,
  8. })
  9. const _ = db.command
  10. exports.main = async (event) => {
  11. try {
  12. const result = await db.runTransaction(async transaction => {
  13. const aaaRes = await transaction.collection('account').doc('aaa').get()
  14. const bbbRes = await transaction.collection('account').doc('bbb').get()
  15. if (aaaRes.data && bbbRes.data) {
  16. const updateAAARes = await transaction.collection('account').doc('aaa').update({
  17. data: {
  18. amount: _.inc(-10)
  19. }
  20. })
  21. const updateBBBRes = await transaction.collection('account').doc('bbb').update({
  22. data: {
  23. amount: _.inc(10)
  24. }
  25. })
  26. console.log(`transaction succeeded`, result)
  27. // 会作为 runTransaction resolve 的结果返回
  28. return {
  29. aaaAccount: aaaRes.data.amount - 10,
  30. }
  31. } else {
  32. // 会作为 runTransaction reject 的结果出去
  33. await transaction.rollback(-100)
  34. }
  35. })
  36. return {
  37. success: true,
  38. aaaAccount: result.aaaAccount,
  39. }
  40. } catch (e) {
  41. console.error(`transaction error`, e)
  42. return {
  43. success: false,
  44. error: e
  45. }
  46. }
  47. }

隔离等级

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

FAQ

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