`
eyesmore
  • 浏览: 363921 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

分布式并发计数器:播放数统计MongoDB实现

阅读更多

 

分布式并发计数,以视频站点播放数统计为例(本质是{vid->count}映射关系),内容提要:

  • Upsert+$INC解决并发计数
  • findAndModify解决写时返回结果
  • JAVA实现:findAndModify+upsert+$INC三剑客
  • 谢绝ObjectId,用vid直接做_id

(1)Upsert+$INC解决并发计数

 

vv_stat_mongodb

 

第一点:第一次update的时候,提示“ok”;但是查询的时候,发现提示“ok”其实依然失败了。这点表现了MongoDB的fire-and-forget特性,默认情况下MongoDB不执行getLastError(),只管发送写请求,不等待写请求的响应,也就是不管能否写成功。听说MongoDB并发控制策略上用了个“DB级别的全局锁(不是表级,关系型一般了不起是全表锁定,MongoDB却是DB级的)”,难道为了弥补这个影响写性能的缺陷,来了个“fire-and-forget”,这样全局锁对客户来说就的确没有等待的影响了,这是不是太投机了呀?!

第二点:第二次更新操作,多了第三个参数“true”,表示upsert标记(upsert=update+insert),语义是:如果第一个参数Query文档匹配到了,则执行update;如果没匹配到,则insert。同时第二个参数Modifier文档使用了$inc,表示原子累加操作。这两个特性的完美结合,非常适用于“互联网的PV实时统计或视频网站的播放数实时统计”。如果这个逻辑,用关系型实现,那代码要复杂许多。

 

(2)findAndModify解决写时返回结果


上面说了,“upsert标记+$inc修饰器”完美组合就能轻松搞定“分布式并发计数器”的应用场景(比如:PV统计,视频站点的VV统计)。但是我们以VV统计为例,一个真正实时计数器在执行写操作的时候,往往还需要同时返回更新后的VV数,也就是说写操作的同时应该伴随读操作。显然“upsert标记+$inc修饰器”组合还无法满足需求?那么MongoDB 还会不会有新特性呢?很遗憾,MongoDB的确可以有这种场景的操作“findAndModify”,但是“findAndModify”有两个遗憾(据《MongoDB权威指南》介绍):
(1)    丢失upsert特性: 可以返回更新后,或者更新前的文档的状态,但是不具备upsert标记。也就是Query必须匹配上,否则无法更新,也无法插入。
(2)    性能慢:findAndModify据《MongoDB权威指南》介绍,它的时间开销=find一次+update一次+getLastError一次,三者顺序执行所需要的时间。之前我们说,MongoDB很可能是因为“fire-and-forget”,不等待响应(响应需要通过getLastError指令获得),来规避全局锁的开销,现在findAndModify的的确确需要返回结果,所以这个开销是免不了的。
这么看来,findAndModify有点鸡肋?!《MongoDB权威指南》中介绍findAndModify的时候,举了一个例子,大概可以理解为“分布式任务队列”(分布式任务队列也是架构中常用的模型,理念上是MQ,或者AOP,适合“异步事件”的场景),用findAndModify比用乐观锁解决race condition的问题上要方便些。先不管findAndModify是否鸡肋,先看看findAndModify的API吧: http://www.mongodb.org/display/DOCS/findAndModify+Command  。

MongoDB 1.3+ supports a "find, modify, and return" command.  This command can be used to atomically modify a document (at most one) and return it. Note that, by default, the document returned will not include the modifications made on the update.
If you don't need to return the document, you can use Update (which can affect multiple documents, as well).

官方建议:如果你打算用findAndModify,而不用update,那么一个前提条件是:你想知道更新后的结果(文档状态,不仅仅是getLastError信息)。如果你并不想知道更新后的结果,那么你更应该选择update操作。

 

findAndModify_upsert

 

Oh, my god,看到上面这幅图,我只能说MongoDB很年轻,因为年轻有很多鸡肋式的不足,因为年轻更有日新月异的成长。

 

(3)JAVA实现:findAndModify+upsert+$INC三剑客
我们简单来段JAVA代码,开发包是:
https://github.com/mongodb/mongo-java-driver/blame/master/src/main/com/mongodb/DBCollection.java

 

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;

public static void main(String[] args) throws Exception {
		
		String host = "XXX";
		int port = XXX;
		String dbname = "XXX";
		String username = "XXX";
		String passwd = "XXX";
		
		Mongo mongo = new Mongo(host, port);
		DB db = mongo.getDB(dbname);
		boolean authed = db.authenticate(username, passwd.toCharArray());
		System.out.println("认证:"+authed);
		System.out.println("DB下所有集合:"+db.getCollectionNames());
		
		//CH3: 获取DB
		String collectionName = "vv_stat";
		DBCollection collection = db.getCollection(collectionName);
		
		//public DBObject findAndModify(DBObject query, DBObject fields, DBObject sort, boolean remove, DBObject update, boolean returnNew, boolean upsert)
		//REFER:  https://github.com/mongodb/mongo-java-driver/blame/master/src/main/com/mongodb/DBCollection.java
		
		String vid = "10086";
		int count = 38;
		
		BasicDBObject query = new BasicDBObject().append("vid", vid);
		BasicDBObject fields = new BasicDBObject().append("vid", 1).append("count", 1);
		BasicDBObject sort = new BasicDBObject().append("vid", 1);
		boolean remove = false;
		BasicDBObject update = new BasicDBObject().append("$inc", new BasicDBObject("count",count));
		boolean returnNew = true;
		boolean upsert = true;
		DBObject r = collection.findAndModify(query,fields,sort,remove,update,returnNew,upsert);
		System.out.println("统计结果:"+r);
		
	}

 

输出:
统计结果:{ "_id" : { "$oid" : "5029dc79bc7ee3893a74cffe"} , "count" : 38 , "vid" : "10086"}

很完美了!至少功能上是完全符合我们的需求了,性能的问题暂不理会。

 

 

(4)谢绝ObjectId,用vid直接作为_id
上面的输出{ "_id" : { "$oid" : "5029dc79bc7ee3893a74cffe"} , "count" : 38 , "vid" : "10086"},我们看到插入的文档除了vid和count,还有_id,因为每个Doc都必须有_id,而且它是唯一索引。播放数统计就是从vid到count映射关系,数据量大了,为了提高查询效率,我们要对vid做索引,而且是唯一索引,那为什么要浪费原有的_id呢?其实_id天然就是NoSQL特性之Key/Value的友好支持。于是,我们应该把vid存储在_id上。

BasicDBObject query = new BasicDBObject().append("_id", vid);
BasicDBObject fields = new BasicDBObject().append("_id", 1).append("count", 1);
BasicDBObject sort = new BasicDBObject().append("_id", 1);
boolean remove = false;
BasicDBObject update = new BasicDBObject().append("$inc", new BasicDBObject("count",count));
boolean returnNew = true;
boolean upsert = true;
DBObject r = collection.findAndModify(query,fields,sort,remove,update,returnNew,upsert);
System.out.println("统计结果:"+r);

 

输出:“统计结果:{ "_id" : "10086" , "count" : 38}”

 

 

  • 大小: 39.3 KB
  • 大小: 12.1 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics