注册

MongoDB 事务支持详解

MongoDB 事务支持详解

MongoDB 4.0版本开始支持了多文档事务,这是MongoDB一个重要的里程碑,意味着Mongodb可以用来存储具有ACID特性的关系型数据了。

事务的基本概念

事务是指一组数据库操作,它们被视为一个工作单元,要么全部执行成功,要么全部失败执行回滚。MongoDB中的事务遵循的是“all-or-nothing”的原则。

一个事务通常包括以下四个属性:“原子性”、“一致性”、“隔离性”和“持久性”。

  • 原子性:事务执行的所有操作作为一个整体被执行,要么所有操作都成功完成,要么所有操作都不执行。如果在事务执行期间发生错误,所有的操作都将被回滚。
  • 一致性:事务执行前后的状态应该保持一致,事务结束后应该符合预期的状态。
  • 隔离性:多个事务并发执行时,事务之间应该是相互隔离,每个事务应该感觉不到其他事务的存在。
  • 持久性:一旦事务提交,那么事务执行的结果必须得到持久化,不能丢失。

事务的使用限制

在使用MongoDB事务时,可能会遇到一些限制:

  • 处理事务的集合必须是分片集合或副本集合;
  • 在同一事务中,不能对非同一分片的集合进行读写操作;
  • 在同一事务中,不能跨越包含多个文档的单个shard或primary replica set;
  • 在事务中,所有读写操作必须在一个session中完成。

事务的使用

在MongoDB中,事务的使用可以通过以下方式进行:

1. 开始事务

首先,需要使用MongoDB的事务API获取一个会话对象,然后使用这个会话对象开始一个事务。

session.startTransaction();

2. 执行事务中的操作

执行事务中的所有操作,这些操作可以包括增删改查等操作。

db.collection1.insertOne({name: 'Tom', age: 25}, {session});
db.collection2.updateOne({name: 'Tom'}, {$set: {age: 26}}, {session});
db.collection3.deleteOne({name: 'Tom'}, {session});

需要注意的是,每个操作中都需要传入会话对象,这样MongoDB才能知道这些操作属于哪个事务。

3. 提交或回滚事务

当事务中的所有操作都执行完毕后,可以使用以下方式提交事务:

session.commitTransaction();

如果事务执行过程中出现了错误,需要回滚事务:

session.abortTransaction();

为了保障事务操作的一致性和隔离性,建议使用带有事务回调函数的方式。

4. 带有事务回调函数

将事务中的所有操作放入一个回调函数中,当事务操作成功执行时,自动提交事务,否则回滚事务。

session.withTransaction(async () => {
  db.collection1.insertOne({name: 'Tom', age: 25}, {session});
  db.collection2.updateOne({name: 'Tom'}, {$set: {age: 26}}, {session});
  db.collection3.deleteOne({name: 'Tom'}, {session});
});

事务的示例

1. 插入一条文档并同时修改另一条文档

假设某个文档有这样的结构:

{
  "_id": 1,
  "name": "Tom",
  "age": 25,
  "bankAccount": {
    "balance": 100,
    "currency": "USD"
  }
}

现在,我们需要同时将Tom的年龄加1,并将Tom的银行账户中的余额减去50美元。这个操作就需要使用事务。

session.withTransaction(async () => {
  const tom = await db.collection('users').findOne({_id: 1}, {session});
  const balance = tom.bankAccount.balance;
  if (balance >= 50) {
    await db.collection('users').updateOne({_id: 1}, {$inc: {age: 1}}, {session});
    await db.collection('users').updateOne({_id: 1}, {$inc: {'bankAccount.balance': -50}}, {session});
  } else {
    throw new Error('Not enough balance.');
  }
});

代码中,首先使用findOne方法查询Tom的文档,并将会话对象传递给了findOne方法。然后获取Tom的余额,根据余额进行后续的操作。注意在操作中需要使用 $inc 操作符对Tom的年龄和余额进行修改。

2. 跨集合的事务

一个跨集合的事务例子是,将订单和订单明细同时插入到数据库中。

session.withTransaction(async () => {
  const orderId = await db.collection('orders').insertOne({orderNo: 'ORD001', amount: 1000}, {session});
  const orderDetailId = await db.collection('orderDetails').insertOne({orderId: orderId, productId: 1, quantity: 10, price: 100}, {session});
});

在这个例子中,我们首先向订单集合中插入一个订单,然后向订单明细集合中插入一个订单明细。事务保证了整个过程的原子性,如果订单或订单明细中有一个插入失败,那么整个过程会自动回滚。

总结

MongoDB的事务支持是一个令人激动的特性,它可以为我们提供关系型数据库的高度可靠性和ACID特性。在使用MongoDB的事务时,需要注意支持事务的集合类型、事务的使用限制和事务API的使用。