MongoDB中ObjectId的誤區(qū)及引起的一系列問題
近期對兩個應用進行改造,在上線過程中出現一系列問題(其中一部分是由于ObjectId誤區(qū)導致的)
先來了解下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個字節(jié)其實隱藏了文檔創(chuàng)建的時間,并且時間戳處在于字符的最前面,這就意味著ObjectId大致會按照插入進行排序,這對于某些方面起到很大作用,如 作為索引提高搜索效率等等。使用時間戳還有一個好處是,某些客戶端驅動可以通過ObjectId解析出該記錄是何時插入的,這也解答了我們平時快速連續(xù)創(chuàng) 建多個Objectid時,會發(fā)現前幾位數字很少發(fā)現變化的現實,因為使用的是當前時間,很多用戶擔心要對服務器進行時間同步,其實這個時間戳的真實值并 不重要,只要其總不停增加就好。
Machine
接下來的三個字節(jié),就是 2cdcd2 ,這三個字節(jié)是所在主機的唯一標識符,一般是機器主機名的散列值,這樣就確保了不同主機生成不同的機器hash值,確保在分布式中不造成沖突,這也就是在同一臺機器生成的objectid中間的字符串都是一模一樣的原因。
pid
上面的Machine是為了確保在不同機器產生的objectid不沖突,而pid就是為了在同一臺機器不同的mongodb進程產生了objectid不沖突,接下來的0936兩位就是產生objectid的進程標識符。
increment
前面的九個字節(jié)是保證了一秒內不同機器不同進程生成objectid不沖突,這后面的三個字節(jié)a8b817,是一個自動增加的計數器,用來確保在同一秒內產生的objectid也不會發(fā)現沖突,允許256的3次方等于16777216條記錄的唯一性。
ObjectId唯一性
大家可能會覺得,在某種程度上已經可以保證唯一了,不管在客戶端還是在服務端。
誤區(qū) 一 、文檔順序和插入順序一致?
單線程情況
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);
誤區(qū) 二 、多客戶端高并發(fā)時,是否可以保證順序(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.單點插入,原來分布到幾個點的插入操作,現在統(tǒng)一由一個點查詢,保證機器與進程號相同,使用counter算子使記錄有序。
這里,我們用到了第一種辦法。
誤區(qū) 三 、不在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是由客戶端生成的,并不是不設置就由服務端生成的。
誤區(qū) 四 、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");
獲取自增變量會使用這種方法編寫,但是,我們執(zhí)行完成后會發(fā)現。
findAndModify操作,是先執(zhí)行了find,再執(zhí)行了modify,所以當result為null時,應該新增并返回0
以上所述是小編給大家介紹的MongoDB中ObjectId的誤區(qū)及引起的一系列問題,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!
相關文章
java開發(fā)https請求ssl不受信任問題解決方法
這篇文章主要介紹了java開發(fā)https請求ssl不受信任問題解決方法,具有一定借鑒價值,需要的朋友可以參考下2018-01-01Spring?Cloud中Sentinel的兩種限流模式介紹
如何使用Sentinel做流量控制呢?這篇文章就來為大家詳細介紹了Spring?Cloud中Sentinel的兩種限流模式,感興趣的小伙伴可以跟隨小編一起學習一下2023-05-05