事务操作
事务操作确保多个数据库操作要么全部成功,要么全部失败,保证数据的一致性。云开发支持数据库事务,并保证事务的 ACID 特性。
注意
目前数据库事务只支持在服务端运行,只有 node-sdk
支持事务。
使用场景
在大多数场景中,单文档完全可以满足需求。但在一些场景中,使用数据库事务的优势更明显:
- 从传统关系型数据库迁移到云开发:数据模型平滑迁移,业务代码改造成本低
- 涉及多个文档/多个集合的业务流程:保证一系列读写操作完全成功或者完全失败,防止出现中间态
支持的方法
事务流程方法
API | 说明 |
---|---|
startTransaction | 发起事务 |
commit | 提交事务 |
rollback | 回滚事务 |
runTransaction | 自动提交事务 |
事务中的数据操作
API | 说明 |
---|---|
get | 查询文档 |
add | 插入文档 |
delete | 删除文档 |
update | 更新文档 |
set | 更新文档,文档不存在时,会自动创建 |
基础事务
参数说明
参数 | 类型 | 必填 | 说明 |
---|---|---|---|
updateFunction | function | ✅ 是 | 事务更新函数,接收 transaction 参数 |
代码示例
// 基础事务操作
const result = await db.runTransaction(async transaction => {
// 在事务中进行多个操作
const todoDoc = await transaction.collection('todos').doc('todo-id').get()
if (!todoDoc.data) {
throw new Error('待办事项不存在')
}
// 更新待办事项状态
await transaction.collection('todos').doc('todo-id').update({
data: {
completed: true,
completedAt: new Date()
}
})
// 更新用户统计
await transaction.collection('users').doc('user-id').update({
data: {
completedTodos: db.command.inc(1)
}
})
return { success: true }
})
console.log('事务执行结果:', result)
返回结果
{
result: { success: true }, // updateFunction 的返回值
errMsg: "runTransaction:ok"
}
复杂事务示例
清空购物车场景
以"清空购物车"的需求为例,展示数据库事务在复杂业务场景中的使用:
// Node.js 环境
const cloudbase = require('@cloudbase/node-sdk')
const app = cloudbase.init({})
const db = app.database()
exports.main = async (event, context) => {
const userId = 'user1'
const transaction = await db.startTransaction()
try {
const usersCollection = transaction.collection('users')
const goodsCollection = transaction.collection('goods')
// 1. 获取用户信息
const user = await usersCollection.doc(userId).get()
const { cart, balance } = user.data
// 2. 获取购物车数据和对应的商品信息
const goods = []
for (const { id } of cart) {
const good = await goodsCollection.doc(id).get()
goods.push(good.data)
}
let totalPrice = 0
for (let i = 0; i < cart.length; ++i) {
// 3. 计算购物车中的商品总价
totalPrice += cart[i].num * goods[i].price
// 4. 更新商品库存
await goodsCollection.doc(goods[i]._id).set({
inventory: goods[i].inventory - cart[i].num
})
}
// 5. 更新用户账户余额和清空购物车
await usersCollection.doc(userId).set({
balance: balance - totalPrice,
cart: []
})
await transaction.commit()
return { success: true, totalPrice }
} catch (error) {
await transaction.rollback()
throw error
}
}
事务原理
快照隔离
- 在调用
db.startTransaction()
开始事务后,"快照"是在第一次读之后才会生成 - 在没有调用
transaction.commit()
提交事务前,所有的读写操作都是在"快照"上进行 - 在成功提交事务后,"快照"上的数据才会落盘,相关文档数据完成更新
锁与写冲突
- 当事务修改文档时,会锁定相关文档,使其不受其他更改的影响,直到事务结束
- 如果一个事务无法获取到试图修改的文档的锁,事务会终止,并出现写冲突
- 读取文档的操作不需要与文档修改相同的锁
错误处理
推荐在进行事务操作时,使用 try-catch
来捕获异常:
try {
const transaction = await db.startTransaction()
// 涉及文档更改的事务操作
await transaction.collection('orders').add({
data: { /* 订单数据 */ }
})
// 如果发生错误,抛出异常触发回滚
if (someCondition) {
throw new Error('业务逻辑错误')
}
await transaction.collection('products').doc('product-id').update({
data: { stock: db.command.inc(-1) }
})
await transaction.commit()
} catch (error) {
console.error('事务失败,已自动回滚:', error)
// 发生写冲突时,进行异常处理
}
事务限制
操作限制
限制项 | 说明 | 影响 |
---|---|---|
操作数量 | 单个事务最多 100 个操作 | 需要合理规划事务范围 |
执行时间 | 事务执行时间不能超过 30 秒 | 避免复杂计算和外部调用 |
文档操作 | 仅支持 doc 方法,不支持 where 方法 | 当前事务仅支持单文档操作 |
DDL 操作 | 避免在 DDL 操作期间进行事务 | 可能导致事务无法获得锁 |
注意事项
// ❌ 错误示例:在事务中进行外部调用
await db.runTransaction(async transaction => {
// 不要在事务中调用云函数或外部 API
const result = await wx.cloud.callFunction({
name: 'someFunction'
})
// 事务操作...
})
// ✅ 正确示例:先获取数据,再执行事务
const externalData = await wx.cloud.callFunction({
name: 'someFunction'
})
await db.runTransaction(async transaction => {
// 使用预先获取的数据
await transaction.collection('data').add({
data: externalData.result
})
})
最佳实践
事务设计原则
- 避免长时间运行的事务:事务运行时间过长(超过 30s)可能会被自动终止
- 避免执行过多操作:当一个事务中的操作过多时,可能会影响数据库性能
- 避免在 DDL 操作时进行事务:在创建索引、删除数据库期间,事务可能无法获得锁
- 配合 try-catch 捕获异常:尽早发现和处理写冲突、网络异常等问题
性能优化
- 将事务拆分成更小的事务:防止长时间运行和操作过多的情况
- 索引优化:为事务中查询的字段建立索引
- 数据预取:在事务外预先获取需要的数据