近期對兩個應用進行改造,在上線過程中出現一系列問題(其中一部分是由於ObjectId誤區導致的)
先來了解下ObjectId:
TimeStamp
前4位是一個unix的時間戳,是一個int類別,我們將上面的例子中的objectid的前4位進行提取“4df2dcec”,然後再將他們安裝十六進制專為十進制:“1307761900”,這個數字就是一個時間戳,為了讓效果更佳明顯,我們將這個時間戳轉換成我們習慣的時間格式(精確到秒)
$ date -d '1970-01-01 UTC 1307761900 sec' -u
2011年06月11日星期六03:11:40 UTC
前4個字節其實隱藏了文檔創建的時間,並且時間戳處在於字符的最前面,這就意味著ObjectId大致會按照插入進行排序,這對於某些方面起到很大作用,如作為索引提高搜索效率等等。使用時間戳還有一個好處是,某些客戶端驅動可以通過ObjectId解析出該記錄是何時插入的,這也解答了我們平時快速連續創建多個Objectid時,會發現前幾位數字很少發現變化的現實,因為使用的是當前時間,很多用戶擔心要對服務器進行時間同步,其實這個時間戳的真實值並不重要,只要其總不停增加就好。
Machine
接下來的三個字節,就是2cdcd2 ,這三個字節是所在主機的唯一標識符,一般是機器主機名的散列值,這樣就確保了不同主機生成不同的機器hash值,確保在分佈式中不造成衝突,這也就是在同一台機器生成的objectid中間的字符串都是一模一樣的原因。
pid
上面的Machine是為了確保在不同機器產生的objectid不衝突,而pid就是為了在同一台機器不同的mongodb進程產生了objectid不衝突,接下來的0936兩位就是產生objectid的進程標識符。
increment
前面的九個字節是保證了一秒內不同機器不同進程生成objectid不衝突,這後面的三個字節a8b817,是一個自動增加的計數器,用來確保在同一秒內產生的objectid也不會發現衝突,允許256的3次方等於16777216條記錄的唯一性。
ObjectId唯一性
大家可能會覺得,在某種程度上已經可以保證唯一了,不管在客戶端還是在服務端。
誤區一、文檔順序和插入順序一致?
單線程情況
ObjectId中的timestamp、machine、pid、inc都可以保證唯一,因為在同一台機器,同一個進程。
這裡有一個問題,mongodb的操作時多線程的。 a、b、c...幾個線程進行入庫操作時,不能保證哪一條可以在另外一條之前,所以會是亂序的。
多線程、多機器或多進程情況
再看下ObjectId中mache、pid不能保證唯一。那麼則數據更加會是亂序的。
解決辦法:
由於collection集合中數據是無序的(包括capped collection),那麼,最簡單的辦法是對ObjectId進行排序。
可以使用兩種方法排序,
1.mongoDB查詢語句
jQuery query = new Query(); if (id != null) { jquery.addCriteria(Criteria.where("_id").gt(id)); } jquery.with(new Sort(Sort.Direction.ASC, "_id"));2.java.util.PriorityQueue
Comparator<DBObject> comparator = new Comparator<DBObject>() { @Override public int compare(DBObject o1, DBObject o2) { return ((ObjectId)o1.get("_id")).compareTo((ObjectId)o2.get("_id")); } }; PriorityQueue<DBObject> queue = new PriorityQueue<DBObject>(200,comparator);誤區二、多客戶端高並發時,是否可以保證順序(sort之後)?
如果一直保證寫入遠遠大於讀出(間隔一秒以上),這樣是永遠不會出現亂序的情況。
我們來看下樣例
現在看到圖中,取出數據兩次
第一次
4df2dcec aaaa ffff 36a8b813
4df2dcec aaaa eeee 36a8b813
4df2dcec bbbb 1111 36a8b814
第二次
4df2dcec bbbb 1111 36a8b813
4df2dcec aaaa ffff 36a8b814
4df2dcec aaaa eeee 36a8b814
現在如果取第一次的最大值(4df2dcec bbbb 1111 36a8b814)做下次查詢的結果,那麼就會漏掉
第二次的三條,因為(4df2dcec bbbb 1111 36a8b814)大於第二次取的所有記錄。
所以會導致丟數據的情況。
解決辦法:
由於ObjectId的時間戳截止到秒,而counter算子前四位又為機器與進程號。
1.處理一定時間間隔前的記錄(一秒以上),這樣即使機器和進程號導致亂序,間隔前也不會出現亂序情況。
2.單點插入,原來分佈到幾個點的插入操作,現在統一由一個點查詢,保證機器與進程號相同,使用counter算子使記錄有序。
這裡,我們用到了第一種辦法。
誤區三、不在DBObject設置_id使用mongoDB設置ObjectId?
mongoDB插入操作時,new DBBasicObject()時,大家看到_id是沒有被填值的,除非手工的設置_id。那麼是否是服務端設置的呢?
大家來看下插入操作的代碼:
實現類
public WriteResult insert(List<DBObject> list, com.mongodb.WriteConcern concern, DBEncoder encoder ){ if (concern == null) { throw new IllegalArgumentException("Write concern can not be null"); } return insert(list, true, concern, encoder); }可以看到需要添加,默認都為添加
protected WriteResult insert(List<DBObject> list, boolean shouldApply , com.mongodb.WriteConcern concern, DBEncoder encoder ){ if (encoder == null) encoder = DefaultDBEncoder.FACTORY.create(); if ( willTrace() ) { for (DBObject o : list) { trace( "save: " + _fullNameSpace + " " + JSON.serialize( o ) ); } } if ( shouldApply ){ for (DBObject o : list) { apply(o); _checkObject(o, false, false); Object id = o.get("_id"); if (id instanceof ObjectId) { ((ObjectId) id).notNew(); } } } WriteResult last = null; int cur = 0; int maxsize = _mongo.getMaxBsonObjectSize(); while ( cur < list.size() ) { OutMessage om = OutMessage.insert( this , encoder, concern ); for ( ; cur < list.size(); cur++ ){ DBObject o = list.get(cur); om.putObject( o ); // limit for batch insert is 4 x maxbson on server, use 2 x to be safe if ( om.size() > 2 * maxsize ){ cur++; break; } } last = _connector.say( _db , om , concern ); } return last; }自動添加ObjectId的操作
/** * calls {@link DBCollection#apply(com.mongodb.DBObject, boolean)} with ensureID=true * @param o <code>DBObject</code> to which to add fields * @return the modified parameter object */ public Object apply( DBObject o ){ return apply( o , true ); } /** * calls {@link DBCollection#doapply(com.mongodb.DBObject)}, optionally adding an automatic _id field * @param jo object to add fields to * @param ensureID whether to add an <code>_id</code> field * @return the modified object <code>o</code> */ public Object apply( DBObject jo , boolean ensureID ){ Object id = jo.get( "_id" ); if ( ensureID && id == null ){ id = ObjectId.get(); jo.put( "_id" , id ); } doapply( jo ); return id; }可以看到,mongoDB的驅動包中是會自動添加ObjectId的。
save的方法
public WriteResult save( DBObject jo, WriteConcern concern ){ if ( checkReadOnly( true ) ) return null; _checkObject( jo , false , false ); Object id = jo.get( "_id" ); if ( id == null || ( id instanceof ObjectId && ((ObjectId)id).isNew() ) ){ if ( id != null && id instanceof ObjectId ) ((ObjectId)id).notNew(); if ( concern == null ) return insert( jo ); else return insert( jo, concern ); } DBObject q = new BasicDBObject(); q.put( "_id" , id ); if ( concern == null ) return update( q , jo , true , false ); else return update( q , jo , true , false , concern ); }綜上所述,默認情況下ObjectId是由客戶端生成的,並不是不設置就由服務端生成的。
誤區四、findAndModify是否真的可以獲取到自增變量?
DBObject update = new BasicDBObject("$inc", new BasicDBObject("counter", 1)); DBObject query = new BasicDBObject("_id", key); DBObject result = getMongoTemplate().getCollection(collectionName).findAndModify(query, update); if (result == null) { DBObject doc = new BasicDBObject(); doc.put("counter", 1L); doc.put("_id", key); // insert(collectionName, doc); getMongoTemplate().save(doc, collectionName); return 1L; } return (Long) result.get("counter");獲取自增變量會使用這種方法編寫,但是,我們執行完成後會發現。
findAndModify操作,是先執行了find,再執行了modify,所以當result為null時,應該新增並返回0
以上所述是小編給大家介紹的MongoDB中ObjectId的誤區及引起的一系列問題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對武林網網站的支持!