精华 [技术讨论]mongodb驱动的正确使用方法
发布于 11 年前 作者 shiedman 56022 次浏览 最后一次编辑是 8 年前

mongo数据库在nodejs平台有2个常用驱动,mongodb和mongoose,mongodb接口非常接近mongo数据库原生的操作方式,是helloworld之类演示代码的首选mongo数据库连接驱动,因此成为大部分nodejs初学者最先接触的mongo数据库驱动。初学者在学会mongo连接的同时,却也可悲的被helloword这种演示性质的数据库操作习惯潜移默化了。 cat test.js

var server_options={};
var db_options={
        w:-1,// 设置w=-1是mongodb 1.2后的强制要求,见官方api文档
        logger:{
        doDebug:true,
        debug:function(msg,obj){
            console.log('[debug]',msg);
        },
        log:function(msg,obj){
            console.log('[log]',msg);
        },
        error:function(msg,obj){
            console.log('[error]',msg);
        }
    }
};
var mongodb = require("mongodb"),
    mongoserver = new mongodb.Server('localhost', 27017,server_options ),
    db = new mongodb.Db('test', mongoserver, db_options);

function test_save(){
    //前一个db和后一个db,是同一个对象。
    db.open(function(err,db){
        if(err)return console.error(err);
        console.log('* mongodb connected');
        db.collection('foo').save({test:1},function(err,result){
            console.log(result);
            db.close();
        });
    })
}
test_save();

这是个随处可见,大家非常熟悉的mongo数据库helloword代码:设置连接参数,open, 访问collection, close。 唯一不同的是为了显示代码背后的实际运作,我给db_options加上一个日志(logger)选项。 node test.js, 客户端输出信息:

[debug] opened connection
[debug] opened connection
[debug] opened connection
[debug] opened connection
[debug] opened connection
[debug] writing command to mongodb
* mongodb connected
[debug] writing command to mongodb
{ test: 1, _id: 51908a93718f6ad00c000001 }
[debug] closed connection
[debug] closed connection
[debug] closed connection
[debug] closed connection
[debug] closed connection

服务端mongo数据库的输出日志:

Mon May 13 12:54:33 [initandlisten] connection accepted from 127.0.0.1:2815 #51
Mon May 13 12:54:33 [initandlisten] connection accepted from 127.0.0.1:2816 #52
Mon May 13 12:54:33 [initandlisten] connection accepted from 127.0.0.1:2817 #53
Mon May 13 12:54:33 [initandlisten] connection accepted from 127.0.0.1:2818 #54
Mon May 13 12:54:33 [initandlisten] connection accepted from 127.0.0.1:2819 #55
Mon May 13 12:54:33 [conn51] end connection 127.0.0.1:2815
Mon May 13 12:54:33 [conn52] end connection 127.0.0.1:2816
Mon May 13 12:54:33 [conn53] end connection 127.0.0.1:2817
Mon May 13 12:54:33 [conn54] end connection 127.0.0.1:2818
Mon May 13 12:54:33 [conn55] end connection 127.0.0.1:2819

客户端和服务器端的日志都表明,db.open了5个连接,而并非一般同学所想象的1个连接。why?这是因为mongoserver = new mongodb.Server('localhost', 27017,server_options )的server_options有个poolSize选项,默认值是5(见官方api文档)。db对象不仅扮演着与mongo数据库通讯的中间人角色,还同时是一个连接池。默认设置的情况下,helloword代码打开一个有着5个连接的连接池,然后再关闭这个连接池。作为ran and quit的演示,这个操作流程当然没问题,但如果放到http server的应用场景就成大问题了。每次http请求都打开5个数据库连接而后又关闭5个数据库连接,对性能影响可想而知,更为糟糕是open and close的操作流程会导致一个潜在的并发错误。 cat server_1.js

var server_options={};
var db_options={w:-1};

var mongodb = require("mongodb"),
    mongoserver = new mongodb.Server('localhost', 27017,server_options ),
    db = new mongodb.Db('test', mongoserver, db_options);

var http=require('http');

var server=http.createServer(function(req,res){
    db.open(function(err,db){
        if(err)return console.error(err);
        console.log('* mongodb connected');
        db.collection('foo').save({test:1},function(err,result){
            res.end(JSON.stringify(result,null,2));
            db.close();
        });
    })

});
server.listen(8080,function(){
    console.log('server listen to %d',this.address().port);
});
setTimeout(function(){
    //http.get('http://localhost:8080',function(res){console.log('request ok')});
    //http.get('http://localhost:8080',function(res){console.log('request ok')});
},2000);

node server.js, 浏览器访问ok,但如果做ab类并发测试或者把倒数2、3行的注释去掉,问题就来了。

c:\nodejs\node_modules\mongodb\lib\mongodb\db.js:224
    throw new Error("db object already connecting, open cannot be called multi
          ^
Error: db object already connecting, open cannot be called multiple times

想象db对象是一扇门,open操作相当于开门,开门后才能阅读房间里面的书籍(数据)。当请求1开门后,紧随而来的请求2也想开门但是开不了,因为请求1还没关门(db.close),门还处于“打开”的状态。其实呢,请求2完全没必要再开门,直接尾随请求1进门即可。错误的根源在于我们要打开一扇已经打开的门。how to fix? easy, 从open and close 行为改为 open once and reuse anywhere。程序启动的时候db.open一次,每次http请求直接访问数据库,扔掉db.open/db.close这2个多余的操作。

cat server_2.js

var server_options={'auto_reconnect':true,poolSize:5};
var db_options={w:-1};

var mongodb = require("mongodb"),
    mongoserver = new mongodb.Server('localhost', 27017,server_options ),
    db = new mongodb.Db('test', mongoserver, db_options);

db.open(function(err,db){
    if(err)throw err;
    console.info('mongodb connected');
});
var http=require('http');

var server=http.createServer(function(req,res){
    db.collection('foo').save({test:1},function(err,result){
        res.end(JSON.stringify(result,null,2));
    });

});
server.listen(8080,function(){
    console.log('server listen to %d',this.address().port);
});
setTimeout(function(){
    http.get('http://localhost:8080',function(res){console.log('request ok')});
    http.get('http://localhost:8080',function(res){console.log('request ok')});
},2000);

这样改后,虽然没报错了,却引入另一个潜在的问题:当并发访问>5时,因为同时可用的底层数据库连接只有5,从而导致了阻塞。

<span>===================================我是分隔线=====================================</span>

实际应用场景中,直接引用db对象并不是一个好主意。默认情况下,db的poolSize=5,意味着并发只有5, 要提高并发的话,把poolSize拉到10? 20? 50? 100? NO,我们需要的是能动态调整连接数的连接池,既能满足高峰期的连接数要求,也能在空闲期释放闲置的连接,而不是象mongodb的内置连接池那样保持固定连接数。怎么办?重新发明轮子吗?不,重用已有的连接池模块generic_pool。 cat server_3.js

var http=require('http'),
    mongodb = require("mongodb"),
    poolModule = require('generic-pool');

var pool = poolModule.Pool({
    name     : 'mongodb',
    create   : function(callback) {
        var server_options={'auto_reconnect':false,poolSize:1};
        var db_options={w:-1};
        var mongoserver = new mongodb.Server('localhost', 27017,server_options );
        var db=new mongodb.Db('test', mongoserver, db_options);
        db.open(function(err,db){
            if(err)return callback(err);
            callback(null,db);
        });
    },
    destroy  : function(db) { db.close(); },
    max      : 10,//根据应用的可能最高并发数设置
    idleTimeoutMillis : 30000,
    log : false 
});

var server=http.createServer(function(req,res){
    pool.acquire(function(err, db) {
        if (err) {
            res.statusCode=500;
            res.end(JSON.stringify(err,null,2));
        } else {
            db.collection('foo').save({test:1},function(err,result){
                res.end(JSON.stringify(result,null,2));
                pool.release(db);
            });
        }
    });
});
server.listen(8080,function(){
    console.log('server listen to %d',this.address().port);
});
setTimeout(function(){
    http.get('http://localhost:8080',function(res){console.log('request ok')});
    http.get('http://localhost:8080',function(res){console.log('request ok')});
},2000);

将poolSize设为1,一个db对象只负责一个底层的数据库连接,generic_pool通过控制db对象的数目,间接控制实际的数据库连接数目。如果poolSize还采取默认值5,1db=5连接,由于每次http请求期间我们实际使用到的只是1个连接,其他4个连接根本用不上,处于闲置状态,其结果是浪费资源,拖慢响应速度。

以上。

备注1:本文mongo数据库设置均采用本机安装的默认设置。 备注2:当需建模的业务对象较多时,使用mongoose驱动是个好主意,它自带的连接池比mongodb实用性强。 备注3:mongodb自1.2以后,官方推荐的连接方式改为MongoClient,将参数设置简化为一个URL(详细见http://mongodb.github.io/node-mongodb-native/driver-articles/mongoclient.html)。目前monodb的版本已经1.3,大家也该是时候转变了吧。 以上文generic_pool连接池的初始化为例,看看新旧连接的对比: 旧连接方法

var pool = poolModule.Pool({
    name     : 'mongodb',
    create   : function(callback) {
        var server_options={'auto_reconnect':false,poolSize:1};
        var db_options={w:-1};
        var mongoserver = new mongodb.Server('localhost', 27017,server_options );
        var db=new mongodb.Db('test', mongoserver, db_options);
        db.open(function(err,db){
            if(err)return callback(err);
            callback(null,db);
        });
    },
    //......more code here
});

新连接方法

var pool = poolModule.Pool({
    name     : 'mongodb',
    create   : function(callback) {
        mongodb.MongoClient.connect('mongodb://localhost/test', {
            server:{poolSize:1}
        }, function(err,db){
            callback(err,db);
        });
    },
    //more code here
});
22 回复

好文,收藏。尤其连接池方面,很多人在论坛问。

前段时间在做项目的时候发现这个问题了,我就自己造轮子了,自己写了一个对象池。不过也就几行代码

不错,高质量的好文啊~

一样的,就是opiton里的create函数换一下就可以啦, 可以看一下我写的另外一篇文章,对象池

在用poolModule包装感觉有点多此一举了,就多了个idleTimeoutMillis参数,无形中增加了复杂度

好文一篇,对于我这种菜鸟很适合!呵呵

好文啊,感谢楼上让它浮上来

2014-05-14T17:13:40.171+0800 [clientcursormon] connections:15 2014-05-14T17:16:48.734+0800 [initandlisten] connection accepted from 127.0.0.1: 3257 #88 (16 connections now open) 2014-05-14T17:16:50.453+0800 [initandlisten] connection accepted from 127.0.0.1: 3265 #89 (17 connections now open) 2014-05-14T17:16:52.078+0800 [initandlisten] connection accepted from 127.0.0.1: 3271 #90 (18 connections now open) 我想问的问题是,这种方式连接数一直增长到最高并发数吗?什么时候关闭连接?新手求解答!

使用你给的方法,测试了一性能问题。发现了一个大的疑问! 当连接池poolSize设为10时,插入100万条数据,用时78472毫秒。当连接池poolSize设为1时,插入100万条数据,用时53454毫秒。 也就是说当连接数为1时,反而比连接数为10时用时更少!那要连接池还有什么用呢????

一下是测试代码,在Linux下运行 var server_options={‘auto_reconnect’:true,poolSize:1}; console.log(“poolSize:”+server_options.poolSize); var db_options={w:-1};

var mongodb = require(“mongodb”), mongoserver = new mongodb.Server(‘localhost’, 27017,server_options ), db = new mongodb.Db(‘test’, mongoserver, db_options);

db.open(function(err,db){ if(err)throw err; console.info(‘mongodb connected’); });

var time_start; function a(x){ db.collection(‘foo’).save({test:1},function(err,result){ if(x==1000000){ var now1=new Date(); var time_end=now1.getTime(); console.info(‘diff:’+(time_end-time_start)); }

	});

} setTimeout(function(){ var now=new Date(); time_start=now.getTime();

for(var i=1;i<=1000000;i++)
{

	a(i);
		
}

},2000)

还有一个问题,多个JS怎么共用一个链接?

今天遇到了这个问题,在使用generic-pool的时候用了第一种写法,即是在使用时创建mongo,结果在达到连接池的min数量后出现 Error: db object already connecting, open cannot be called multiple times,如果达到了连接池的最小连接数,再往上不会自动调用create返回一个新的连接吗?而出现的错误似乎是表明我们重复引用了同一个数据库连接,望解答 T_T

mongodb 自己已经有pool了。既然是pool,就不要反复的去open close。

@JacksonTian 今天刚想通是为什么,tks

官网的doc好像都用MongoClient了 貌似不用用pool 了?

回到顶部