为MongoDB编写Js维护脚本

最近项目上有一个需求,要对存储在MongoDB中的用户行为数据定期进行统计分析。

先使用PHP实现原型,发现因为数据量很大,大量时间都花在MongoDB服务器和Web服务器之间的数据交换上。考虑到这一点,必须在MongoDB服务器上进行本地计算,将结果保存起来,再使用PHP访问并展示给用户。

查阅文档得知,MongoDB可以执行JS脚本,这样思路就清楚了,用JS脚本实现统计的功能,再用crontab定期执行。

现在和大家分享一下在完成这个任务的过程中,遇到的一些问题和解决思路。(必须要说的是,MongoDB的官方文档对服务器端JS编程的文档极度缺乏,很多命令都是通过Google才找到的。)

在命令行输出信息

输出字符串:

print( 'Hello World' );

输出对象:

var obj = { 'key' : 'value' };

printjson( obj );

切换当前数据库

在Mongo JS Shell中,切换当前数据库的命令是:

use xxx

但是令人困惑的是,在JS脚本中使用use会报告语法错误。需要使用connect命令,并将句柄保存起来才能使用:

var db = connect( 'user' );

db.user.find();

加载外部JS文件

因为需求比较复杂,很快代码就臃肿不堪,需要把各种Helper函数等提取到单独的文件。想要实现这一点,就需要可以在JS脚本中加载其他文件。可以使用load命令:

load( 'helpers/time.js' );

需要注意的是,在load命令中使用相对路径时,当前路径是执行mongo命令时shell所在路径。这就需要在shell中执行mongo之前,先cd到脚本所在的路径,否则会找不到需要加载的脚本。

时间戳转换

MongoDB中每条记录的时间戳是标准UNIX时间戳,而JS中没有操作时间戳的原生函数,需要自己实现:

function date2timestamp ( date ) {

  return Math.floor( date.getTime() / 1000 );

}

function timestamp2date ( timestamp ) {

  return new Date( timestamp * 1000 );

}

function now2timestamp ( ) {

  return date2timestamp( new Date() );

}

在本次需求中,需要按日生成统计记录。这时需要一个对阅读友好的格式来表示日期,同时还要能方便的在JS中还原成Date对象。由于不想自己实现一个date parse(担心性能),而JS原生的Date.parse()只支持mm/dd/yyyy或mm-dd-yyyy的格式,所以选择了前者:

function date2day ( date ) {

  return ( '0' + ( date.getMonth() + 1 ) ).substr( -2 ) + '/' + ( '0' + date.getDate() ).substr( -2 ) + '/' + date.getFullYear();

}

function day2date ( day ) {

  return new Date( Date.parse( day ) );

}

显示进度

在执行每个步骤前先输出信息,这是常识:

print( 'scan [ user ] for information...' );

userDB.user.find().forEach(function( doc ){

  ....

})

在数据量大或者计算量大的时候,单个步骤可能执行很长时间。这时我们想监控计算的进度,以评价脚本的运行效率。于是写了一个progress:

var progress_update_counts = 100;

var progress_update_seconds = 3;

function createProgress ( total ) {

  return {

    start : now2timestamp(),

    timer : now2timestamp(),

    total : total,

    count : 0,

    pass : function () {

      if ( ( ++this.count % progress_update_counts == 0 ) && ( now2timestamp() - this.timer > progress_update_seconds ) ) {

        print( "\t" + this.count + ' / ' + this.total + "\t" + ( this.count / this.total * 100 ).toFixed( 2 ) + '%' );

        this.timer = now2timestamp();

      }

    },

    done : function () {

      print( "\t" + this.total + ' / ' + this.total + "\t100%\tCost " + ( now2timestamp() - this.start ) + ' seconds.' );

    }

  };

}

简单解释一下逻辑:

1、初始化progress的时候记录下当前的时间和需要计算的总量;

2、每计算完一项时调用progress.pass(),此时progress内部的计数器自增,然后每当计数增加一个指定的量级(太大失去统计意义,太小影响性能,目前设为100)时,如果上一次输出的时间已经超过指定的周期(太大失去统计意义,太小影响性能,目前设为3秒)则输出当前进度。

3、 计算完成后调用progress.done(),输出完成的进度及总消耗时间。

使用示例如下:

print( 'scan [ user ] for information...' );

var progress = createProgress( userDB.user.count() );

userDb.user.find().forEach(function( doc ){

  ....

  progress.pass();

});

progress.done();

输出示例如下:

scan [ user ] for information...

    3200 / 10000    32.00%

    6400 / 10000    64.00%

    9600 / 10000    96.00%

    10000 / 10000   100.00%    Cost 10 seconds.

改写缓存

在扫描用户行为数据时需要不断更新统计结果记录,仔细分析会发现同一条记录被多次改写,而实际保留的只有最终值,之前的记录写入操作完全是浪费。应该先将改写的动作缓存在内存里,等扫描操作结束后在批量写入到数据库中。于是写了一个buffer:

function createIncreaseBuffer ( collection ) {

  return {

    buffer : {},

    collection : collection,

    push : function ( id, key, value ) {

      var buffer = this.buffer;

      if ( !buffer[ id ] ) buffer[ id ] = {};

      if ( !buffer[ id ][ key ] ) buffer[ id ][ key ] = 0;

      buffer[ id ][ key ] += value;

    },

    flush : function () {

      var collection = this.collection;

      for ( var id in this.buffer ) {

        collection.update( { _id : ObjectId( id ) }, { $inc : this.buffer[ id ] } );

      }

      this.buffer = {};

    }

  }

}

使用示例:

print( 'scan [ user ] for information...' );

var progress = createProgress( userDB.user.count() );

var buffer = createIncreaseBuffer( reportDB.user_report );

userDb.user.find().forEach(function( doc ){

  ....

  buffer.push( doc._id, 'count', 1 );

  progress.pass();

});

buffer.flush();

progress.done();

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值