专栏首页Nodejs技术栈MongoDB 多文档事务实践篇—教你如何在 Node.js 中应用

MongoDB 多文档事务实践篇—教你如何在 Node.js 中应用

MongoDB 在单文档操作中具有原子性,在多文档操作中就不再具有此特性,通常需要借助事务来实现 ACID 特性。

事务 API 介绍

客户端对于事务的操作,都由 MongoDB Client Driver 实现提供相应的 API 接口。MongoDB 4.0 之后才支持事务,对于客户端驱动版本也要选择相对应版本。

本文采用 MongoDB Client Driver 3.5 版本

会话 Session

Session 是 MongoDB 3.6 之后引入的概念,在以前的版本中,Mongod 进程中的每一个请求会创建一个上下文(OperationContext),可以理解为一个单行事务,这个单行事务中对于数据、索引、oplog 的修改都是原子性的

MongoDB 3.6 之后的 Session 本质上也是一个上下文,在这个 Session 会话中多个请求共享一个上下文,为多文档事务实现提供了基础。

一个知识点:为何 db.coll.count() 在宕机崩溃后经常就不准了?

原因在于 表记录数的更新独立于数据更新的事务之外,参考文章 mongoing.com/archives/5476。

事务函数

  • startTransaction()

开启一个新的事务,之后即可进行 CRUD 操作。

  • commitTransaction()

提交事务保存数据,在提交之前事务中的变更的数据对外是不可见的。

  • abortTransaction()

事务回滚,例如,一部分数据更新失败,对已修改过的数据也进行回滚。

  • endSession()

结束本次会话。

Mongo Shell 中简单实现

var session = db.getMongo().startSession();
session.startTransaction({readConcern: { level: 'majority' },writeConcern: { w: 'majority' }});
var coll = session.getDatabase('test').getCollection('user');

coll.update({name: 'Jack'}, {$set: {age: 18}})

// 成功提交事务
session.commitTransaction();

// 失败事务回滚
session.abortTransaction();

MongoDB 事务在 Nodejs 中的实践

为了更好的理解 MongoDB 事务在 Node.js 中如何应用,列举一个例子进行说明。

假设我们现在有这样一个商城商品下单场景,分为一个商品表(存储商品数据、库存信息),另一个订单表(存储订单记录)。每次下单之前需要先校验库存是否大于 0,大于 0 的时候扣减商品库存、创建订单,否则,提示库存不足无法下单。

数据模型

// goods
{
    "_id": ObjectId("5e3b839ec2d95bfeecaad6b8"),
    "goodId":"g1000", // 商品 Id
    "name":"测试商品1", // 商品名称
    "stock":2, // 商品库存
    "price":100 // 商品金额
}
// db.goods.insert({ "goodId" : "g1000", "name" : "测试商品1", "stock" : 2, "price" : 100 })
// order_goods
{
    "_id":ObjectId("5e3b8401c2d95bfeecaad6b9"),
    "id":"o10000", // 订单id
    "goodId":"g1000", // 订单对应的商品 Id
    "price":100 // 订单金额
}
// db.order_goods.insert({ id: "o10000", goodId: "g1000", price: 100 })

Node.js 操作 MongoDB 原生 API 实现

注意:在一个事务操作中 readPreference 必须设置为 primary 节点,不能是 secondary 节点。

db.js

链接 MongoDB,初始化一个实例。

const MongoClient = require('mongodb').MongoClient;
const dbConnectionUrl = 'mongodb://192.168.6.131:27017,192.168.6.131:27018,192.168.6.131:27019/?replicaSet=May&readPreference=secondaryPreferred';
const client = new MongoClient(dbConnectionUrl, {
  useUnifiedTopology: true,
});

let instance = null;

module.exports = {
  dbInstance: async () => {
    if (instance) {
      return instance;
    }

    try {
      instance = await client.connect();
    } catch(err) {
      console.log(`[MongoDB connection] ERROR: ${err}`);
      throw err;
    }

    process.on('exit', () => {
      instance.close();
    });

    return instance;
  }
};

index.js

const db = require('./db');

const testTransaction = async (goodId) => {
  const client = await db.dbInstance();
  const transactionOptions = {
    readConcern: { level: 'majority' },
    writeConcern: { w: 'majority' },
    readPreference: 'primary',
  };

  const session = client.startSession();
  console.log('事务状态:', session.transaction.state);

  try {
    session.startTransaction(transactionOptions);
    console.log('事务状态:', session.transaction.state);

    const goodsColl = await client.db('test').collection('goods');
    const orderGoodsColl = await client.db('test').collection('order_goods');
    const { stock, price } = await goodsColl.findOne({ goodId }, { session });
    
    console.log('事务状态:', session.transaction.state);
    
    if (stock <= 0) {
        throw new Error('库存不足');
    }

    await goodsColl.updateOne({ goodId }, {
        $inc: { stock: -1 } // 库存减 1
    })
    await orderGoodsColl.insertOne({ id: Math.floor(Math.random() * 1000),  goodId, price  }, { session });
    await session.commitTransaction();
  } catch(err) {
    console.log(`[MongoDB transaction] ERROR: ${err}`);
    await session.abortTransaction();
  } finally {
    await session.endSession();
    console.log('事务状态:', session.transaction.state);
  }
}

testTransaction('g1000')

运行测试

每一次事务函数执行之后,查看当前事务状态。

node index
事务状态:NO_TRANSACTION
事务状态:STARTING_TRANSACTION
事务状态:TRANSACTION_IN_PROGRESS
事务状态:TRANSACTION_COMMITTED

本文分享自微信公众号 - Nodejs技术栈(NodejsDeveloper),作者:五月君

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 基于Egg框架的日志链路追踪实践

    实现全链路日志追踪,便于日志监控、问题排查、接口响应耗时数据统计等,首先 API 接口服务接收到调用方请求,根据调用方传的 traceId,在该次调用链中处理业...

    五月君
  • 一篇文章构建你的 Node.js 知识体系

    最近读《重学前端》,开篇就是让你拥有自己的知识体系图谱,后续学的东西补充到相应的模块,既可以加深对原有知识的理解,又可以强化记忆,很不错的学习方案。

    五月君
  • Nodejs 进阶:解答 Cluster 模块的几个疑问

    ?在 PM2 的配置文件中可以设置 exec_model:'cluster' 和 instances 两个属性来设置开启多个进程,PM2 其实主要也是利用 N...

    五月君
  • 《数据可视化基础》第九章:比例可视化(二)

    在上面说到堆叠条形图的时候,我们说到,由于内部比例相对变化的问题。所以不建议用堆叠的条形图来可视化时间序列的数据。但是如果只有两个分组的话,那么就可以使用堆叠的...

    匹咔球
  • Simditor修改缩进为首行缩进

    kongxx
  • 未找到入口 app.json 文件,或者文件读取失败,请检查后重新编译。

    1. 打开文件project.config.json,找到miniprogramRoot 如下图:

    honey缘木鱼
  • 通过canvas转换颜色为RGBA格式及性能问题 注意性能问题

    前端编程过程中,经常会遇到要把各种颜色格式(比如 “red”、“#F00”、“#FF0000”等)转换成RGBA格式,搜索网络也可以发现一堆的解决方案:

    用户3158888
  • 从零开始写一个Exporter

    上一篇文章中已经给大家整体的介绍了开源监控系统Prometheus,其中Exporter作为整个系统的Agent端,通过HTTP接口暴露需要监控的数据。那么如何...

    用户2937493
  • mongodb与mysql相比的优缺点

    与关系型数据库相比,MongoDB的优点: ①弱一致性(最终一致),更能保证用户的访问速度: 举例来说,在 传统的关系型数据库中,一个COUNT类型的操作会锁...

    wangxl
  • Qt/Qml获取1970年1月1日到现在的时间长度

    Qt君

扫码关注云+社区

领取腾讯云代金券