如何在Node.js Web应用程序中管理MongoDB连接?

358

我正在使用node-mongodb-native驱动程序和MongoDB编写一个网站。

我有关于如何管理连接的几个问题:

  1. 在所有请求中只使用一个MongoDB连接是否足够?这样做会有性能问题吗?如果可以,我可以设置一个全局连接来在整个应用程序中使用吗?

  2. 如果不行,当请求到达时开启新连接然后在处理完请求后关闭它是否好?打开和关闭连接是否昂贵?

  3. 应该使用全局连接池吗?我听说这个驱动程序已经具备原生连接池了。这是一个好选择吗?

  4. 如果我使用连接池,需要使用多少个连接?

  5. 还有其他需要注意的事项吗?


@IonicãBizãu,抱歉,我已经很久没有使用nodejs了,所以我没有看到它。谢谢你的评论~ - Freewind
Connection class and Promise 连接类和 PromiseGlobal Variable 全局变量 - antelove
13个回答

536

Node.js自带MongoDB驱动程序的主要贡献者说:

你可以在应用程序启动时使用 MongoClient.connect 一次,并重复使用db对象。它不是单例连接池,每个 .connect 都会创建一个新的连接池。

因此,为了直接回答您的问题,请重用从MongoClient.connect()返回的 db 对象。这将提供连接池,并与在每个数据库操作上打开/关闭连接相比,将提供明显的速度提升。


4
这是连接MongoDB数据库的JavaScript代码示例,使用了Node.js驱动程序中的MongoClient.connect()函数。您可以访问此链接以获取有关如何在应用程序中使用该函数的详细信息:http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html#mongoclient-connect - AndrewJM
9
这是正确答案。已接受的答案非常错误,因为它建议在每个请求中打开一个连接池,然后在此操作完成后关闭它。这种架构非常糟糕。 - Saransh Mohapatra
11
这是一个正确的答案。我的天啊,想象一下每次我做事情都要打开和关闭它,仅仅为了我的插入件就要花费350K美元每小时!这就像攻击我自己的服务器一样。 - Maziyar
2
你什么时候会关闭连接?除了停止应用服务器之外,为什么还想要关闭它?如果你停止应用服务器,连接会自动关闭吗? - darkace
5
最新的 SDK 是否仍适用于这个答案?现在 connect 方法返回一个客户端(不是数据库)。我们是否仍应该假定相同的模式?只调用一次 connect 方法,只关闭一次连接? - Nathan H
显示剩余12条评论

65

当 Node.js 应用程序启动时,打开一个新连接并重用现有的 db 连接对象:

/server.js

import express from 'express';
import Promise from 'bluebird';
import logger from 'winston';
import { MongoClient } from 'mongodb';
import config from './config';
import usersRestApi from './api/users';

const app = express();

app.use('/api/users', usersRestApi);

app.get('/', (req, res) => {
  res.send('Hello World');
});

// Create a MongoDB connection pool and start the application
// after the database connection is ready
MongoClient.connect(config.database.url, { promiseLibrary: Promise }, (err, db) => {
  if (err) {
    logger.warn(`Failed to connect to the database. ${err.stack}`);
  }
  app.locals.db = db;
  app.listen(config.port, () => {
    logger.info(`Node.js app is listening at http://localhost:${config.port}`);
  });
});

/api/users.js

import { Router } from 'express';
import { ObjectID } from 'mongodb';

const router = new Router();

router.get('/:id', async (req, res, next) => {
  try {
    const db = req.app.locals.db;
    const id = new ObjectID(req.params.id);
    const user = await db.collection('user').findOne({ _id: id }, {
      email: 1,
      firstName: 1,
      lastName: 1
    });

    if (user) {
      user.id = req.params.id;
      res.send(user);
    } else {
      res.sendStatus(404);
    }
  } catch (err) {
    next(err);
  }
});

export default router;

来源:如何在 Node.js/Express 应用中打开数据库连接


1
这将创建一个数据库连接...如果您想使用池,您必须在每次使用时创建/关闭。 - amcdnl
6
以前从未听说过app.locals,但很高兴你在这里向我介绍了它们。 - Z_z_Z
2
帮了我很大的忙!我犯了一个错误,为每个请求创建/关闭数据库连接,这导致我的应用程序性能下降。 - Leandro Lima
我需要感谢你对我迄今为止最有影响力的性能优化。 - user2364424

27

这里有一些代码可以管理您的MongoDB连接。

var MongoClient = require('mongodb').MongoClient;
var url = require("../config.json")["MongoDBURL"]

var option = {
  db:{
    numberOfRetries : 5
  },
  server: {
    auto_reconnect: true,
    poolSize : 40,
    socketOptions: {
        connectTimeoutMS: 500
    }
  },
  replSet: {},
  mongos: {}
};

function MongoPool(){}

var p_db;

function initPool(cb){
  MongoClient.connect(url, option, function(err, db) {
    if (err) throw err;

    p_db = db;
    if(cb && typeof(cb) == 'function')
        cb(p_db);
  });
  return MongoPool;
}

MongoPool.initPool = initPool;

function getInstance(cb){
  if(!p_db){
    initPool(cb)
  }
  else{
    if(cb && typeof(cb) == 'function')
      cb(p_db);
  }
}
MongoPool.getInstance = getInstance;

module.exports = MongoPool;
当您启动服务器时,请调用initPool
require("mongo-pool").initPool();

然后在任何其他模块中,您可以执行以下操作:

var MongoPool = require("mongo-pool");
MongoPool.getInstance(function (db){
    // Query your MongoDB database.
});

这基于MongoDB文档。请查看。


3
自5.x版本以来的更新:变量选项为:{ numberOfRetries: 5, // 重试次数 auto_reconnect: true, // 自动重连 poolSize: 40, // 连接池大小 connectTimeoutMS: 30000 // 连接超时时间(毫秒) } - Blair
@Yaki,你能否请看一下这个问题?https://stackoverflow.com/questions/72439419/why-are-mongod-to-nodejs-connections-bidirectional-doubling-the-amount-of-conne - MartianMartian

23

在单个自包含模块中管理mongo连接池。这种方法提供了两个好处。首先,它使您的代码模块化且更容易测试。其次,您不必将数据库连接混杂在请求对象中,因为这不是数据库连接对象所在的位置。(考虑到JavaScript的性质,我认为将任何内容混入库代码构造的对象中非常危险)。因此,您只需要考虑一个导出两个方法的模块。connect = () => Promiseget = () => dbConnectionObject

有了这样的模块,您可以首先连接到数据库。

// runs in boot.js or what ever file your application starts with
const db = require('./myAwesomeDbModule');
db.connect()
    .then(() => console.log('database connected'))
    .then(() => bootMyApplication())
    .catch((e) => {
        console.error(e);
        // Always hard exit on a database connection error
        process.exit(1);
    });

当应用程序在运行时,只需调用get()即可获取数据库连接。

const db = require('./myAwesomeDbModule');
db.get().find(...)... // I have excluded code here to keep the example  simple

如果您按照以下方式设置数据库模块,不仅可以确保应用程序在没有数据库连接的情况下不会启动,还可以全局访问数据库连接池,并在没有连接时出错。

// myAwesomeDbModule.js
let connection = null;

module.exports.connect = () => new Promise((resolve, reject) => {
    MongoClient.connect(url, option, function(err, db) {
        if (err) { reject(err); return; };
        resolve(db);
        connection = db;
    });
});

module.exports.get = () => {
    if(!connection) {
        throw new Error('Call connect first!');
    }

    return connection;
}

更好的做法是,你可以直接摒弃connect()函数,让get()函数检查连接是否为空,如果为空则为你调用connect()函数。同时,让get()函数始终返回一个promise。这就是我管理连接的方式,非常有效。这是单例模式的一种应用。 - java-addict301
@java-addict301 虽然该方法提供了更简化的 API,但它确实有两个缺点。第一个是没有定义的方法来检查连接错误。每次调用 get() 时,您都必须在内联处处理它。我喜欢尽早失败与数据库连接,通常我不会让应用程序在没有数据库连接的情况下启动。另一个问题是吞吐量。因为您没有活动连接,所以您可能需要等待更长时间的第一次 get() 调用,这是您无法控制的。这可能会扭曲您的报告指标。 - Stewart
1
@Stewart 我构建应用程序/服务的方法通常是在启动时从数据库检索配置。这样,如果无法访问数据库,则应用程序将无法启动。而且,由于第一个请求始终在启动时进行,因此该设计不会出现任何指标问题。同时,在单例中重新抛出连接异常,并提供明确的错误提示信息,表明无法连接。由于应用程序在使用连接时需要捕获数据库错误,因此这不会导致任何额外的内联处理。 - java-addict301
@Stewart 如果在连接后的某个阶段Mongo连接中断,你会如何处理?在这种情况下,所有对get()的调用都将失败,直到节点应用程序重新启动。 - Ayan
2
嗨@Ayan。这里需要注意的是,当我们调用get()时,我们获取的是连接池而不是单个连接。连接池,顾名思义,是数据库连接的逻辑集合。如果池中没有连接,则驱动程序将尝试打开一个连接。一旦该连接打开,它就会被使用并返回到池中。下次访问池时,可能会重用此连接。好处在于池将为我们管理连接,因此如果连接断开,我们可能永远不会知道,因为池将为我们打开一个新连接。 - Stewart
显示剩余2条评论

12
如果您使用Express.js,则可以使用express-mongo-db来缓存和共享MongoDB连接,而无需使用池(因为被接受的答案说这是共享连接的正确方式)。
如果没有 - 您可以查看其源代码并在另一个框架中使用它。

8

您应该创建一个服务连接,然后在需要时重用它。

// db.service.js
import { MongoClient } from "mongodb";
import database from "../config/database";

const dbService = {
  db: undefined,
  connect: callback => {
    MongoClient.connect(database.uri, function(err, data) {
      if (err) {
        MongoClient.close();
        callback(err);
      }
      dbService.db = data;
      console.log("Connected to database");
      callback(null);
    });
  }
};

export default dbService;

我的App.js示例

// App Start
dbService.connect(err => {
  if (err) {
    console.log("Error: ", err);
    process.exit(1);
  }

  server.listen(config.port, () => {
    console.log(`Api runnning at ${config.port}`);
  });
});

使用以下代码,在您想要的任何地方都可以使用它:

import dbService from "db.service.js"
const db = dbService.db

1
如果Mongo无法连接,MongoClient.close()会产生一个错误。但这是原始问题的一个好解决方案。 - Himanshu

6
我在我的应用程序中使用了generic-pool与redis连接,我强烈推荐它。它是通用的,我确信它可以与mysql一起使用,所以我认为你不会有任何问题,也可以与mongo一起使用。 https://github.com/coopernurse/node-pool

Mongo已经在驱动程序中执行连接池,但是我已将我的Mongo连接映射到与node-pool匹配的接口中,这样所有连接都遵循相同的模式,即使在Mongo的情况下,清理实际上并不触发任何操作。 - Tracker1

2

我在我的项目中实现了以下代码,以实现连接池,在我的代码中它将创建一个最小的连接并重复使用可用的连接。

/* Mongo.js*/

var MongoClient = require('mongodb').MongoClient;
var url = "mongodb://localhost:27017/yourdatabasename"; 
var assert = require('assert');

var connection=[];
// Create the database connection
establishConnection = function(callback){

                MongoClient.connect(url, { poolSize: 10 },function(err, db) {
                    assert.equal(null, err);

                        connection = db
                        if(typeof callback === 'function' && callback())
                            callback(connection)

                    }

                )



}

function getconnection(){
    return connection
}

module.exports = {

    establishConnection:establishConnection,
    getconnection:getconnection
}

/*app.js*/
// establish one connection with all other routes will use.
var db = require('./routes/mongo')

db.establishConnection();

//you can also call with callback if you wanna create any collection at starting
/*
db.establishConnection(function(conn){
  conn.createCollection("collectionName", function(err, res) {
    if (err) throw err;
    console.log("Collection created!");
  });
};
*/

// anyother route.js

var db = require('./mongo')

router.get('/', function(req, res, next) {
    var connection = db.getconnection()
    res.send("Hello");

});

2
如果使用Express,还有另一种更直接的方法,那就是利用Express内置的功能在应用程序中的路由和模块之间共享数据。有一个名为app.locals的对象,我们可以将属性附加到它上面并从路由内部访问它。要使用它,请在您的app.js文件中实例化mongo连接。
var app = express();

MongoClient.connect('mongodb://localhost:27017/')
.then(client =>{
  const db = client.db('your-db');
  const collection = db.collection('your-collection');
  app.locals.collection = collection;
});
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              // view engine setup
app.set('views', path.join(__dirname, 'views'));

现在,您可以使用以下代码在路由中访问此数据库连接或任何其他要共享在应用程序模块中的数据:req.app.locals,而无需创建和要求其他模块。
app.get('/', (req, res) => {
  const collection = req.app.locals.collection;
  collection.find({}).toArray()
  .then(response => res.status(200).json(response))
  .catch(error => console.error(error));
});

这种方法确保您的应用程序在运行期间始终保持数据库连接,除非您选择随时关闭它。您可以通过 req.app.locals.your-collection 轻松访问它,并且不需要创建任何其他模块。

1

如果有人想要在2021年使用Typescript编写程序,这是我正在使用的:

import { MongoClient, Collection } from "mongodb";

const FILE_DB_HOST = process.env.FILE_DB_HOST as string;
const FILE_DB_DATABASE = process.env.FILE_DB_DATABASE as string;
const FILES_COLLECTION = process.env.FILES_COLLECTION as string;

if (!FILE_DB_HOST || !FILE_DB_DATABASE || !FILES_COLLECTION) {
  throw "Missing FILE_DB_HOST, FILE_DB_DATABASE, or FILES_COLLECTION environment variables.";
}

const client = new MongoClient(FILE_DB_HOST, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

class Mongoose {
  static FilesCollection: Collection;

  static async init() {
    const connection = await client.connect();
    const FileDB = connection.db(FILE_DB_DATABASE);
    Mongoose.FilesCollection = FileDB.collection(FILES_COLLECTION);
  }
}


Mongoose.init();

export default Mongoose;

我相信如果请求发生得太早(在Mongo.init()完成之前),会抛出错误,因为Mongoose.FilesCollection将是未定义的。

import { Request, Response, NextFunction } from "express";
import Mongoose from "../../mongoose";

export default async function GetFile(req: Request, res: Response, next: NextFunction) {
  const files = Mongoose.FilesCollection;
  const file = await files.findOne({ fileName: "hello" });
  res.send(file);
}

例如,如果您调用files.findOne({ ... })并且Mongoose.FilesCollection未定义,则会出现错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接