MongoDB中ObjectId的誤區(qū)及引起的一系列問(wèn)題
近期對(duì)兩個(gè)應(yīng)用進(jìn)行改造,在上線過(guò)程中出現(xiàn)一系列問(wèn)題(其中一部分是由于ObjectId誤區(qū)導(dǎo)致的)
先來(lái)了解下ObjectId:
TimeStamp
前 4位是一個(gè)unix的時(shí)間戳,是一個(gè)int類(lèi)別,我們將上面的例子中的objectid的前4位進(jìn)行提取“4df2dcec”,然后再將他們安裝十六進(jìn)制 專(zhuān)為十進(jìn)制:“1307761900”,這個(gè)數(shù)字就是一個(gè)時(shí)間戳,為了讓效果更佳明顯,我們將這個(gè)時(shí)間戳轉(zhuǎn)換成我們習(xí)慣的時(shí)間格式(精確到秒)
$ date -d '1970-01-01 UTC 1307761900 sec' -u
2011年 06月 11日 星期六 03:11:40 UTC
前 4個(gè)字節(jié)其實(shí)隱藏了文檔創(chuàng)建的時(shí)間,并且時(shí)間戳處在于字符的最前面,這就意味著ObjectId大致會(huì)按照插入進(jìn)行排序,這對(duì)于某些方面起到很大作用,如 作為索引提高搜索效率等等。使用時(shí)間戳還有一個(gè)好處是,某些客戶端驅(qū)動(dòng)可以通過(guò)ObjectId解析出該記錄是何時(shí)插入的,這也解答了我們平時(shí)快速連續(xù)創(chuàng) 建多個(gè)Objectid時(shí),會(huì)發(fā)現(xiàn)前幾位數(shù)字很少發(fā)現(xiàn)變化的現(xiàn)實(shí),因?yàn)槭褂玫氖钱?dāng)前時(shí)間,很多用戶擔(dān)心要對(duì)服務(wù)器進(jìn)行時(shí)間同步,其實(shí)這個(gè)時(shí)間戳的真實(shí)值并 不重要,只要其總不停增加就好。
Machine
接下來(lái)的三個(gè)字節(jié),就是 2cdcd2 ,這三個(gè)字節(jié)是所在主機(jī)的唯一標(biāo)識(shí)符,一般是機(jī)器主機(jī)名的散列值,這樣就確保了不同主機(jī)生成不同的機(jī)器hash值,確保在分布式中不造成沖突,這也就是在同一臺(tái)機(jī)器生成的objectid中間的字符串都是一模一樣的原因。
pid
上面的Machine是為了確保在不同機(jī)器產(chǎn)生的objectid不沖突,而pid就是為了在同一臺(tái)機(jī)器不同的mongodb進(jìn)程產(chǎn)生了objectid不沖突,接下來(lái)的0936兩位就是產(chǎn)生objectid的進(jìn)程標(biāo)識(shí)符。
increment
前面的九個(gè)字節(jié)是保證了一秒內(nèi)不同機(jī)器不同進(jìn)程生成objectid不沖突,這后面的三個(gè)字節(jié)a8b817,是一個(gè)自動(dòng)增加的計(jì)數(shù)器,用來(lái)確保在同一秒內(nèi)產(chǎn)生的objectid也不會(huì)發(fā)現(xiàn)沖突,允許256的3次方等于16777216條記錄的唯一性。
ObjectId唯一性
大家可能會(huì)覺(jué)得,在某種程度上已經(jīng)可以保證唯一了,不管在客戶端還是在服務(wù)端。
誤區(qū) 一 、文檔順序和插入順序一致?
單線程情況
ObjectId中的timestamp、machine、pid、inc都可以保證唯一,因?yàn)樵谕慌_(tái)機(jī)器,同一個(gè)進(jìn)程。
這里有一個(gè)問(wèn)題,mongodb的操作時(shí)多線程的。a、b、c...幾個(gè)線程進(jìn)行入庫(kù)操作時(shí),不能保證哪一條可以在另外一條之前,所以會(huì)是亂序的。
多線程、多機(jī)器或多進(jìn)程情況
再看下ObjectId中mache、pid不能保證唯一。那么則數(shù)據(jù)更加會(huì)是亂序的。
解決辦法:
由于collection集合中數(shù)據(jù)是無(wú)序的(包括capped collection),那么,最簡(jiǎn)單的辦法是對(duì)ObjectId進(jìn)行排序。
可以使用兩種方法排序,
1.mongoDB查詢語(yǔ)句
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ā)時(shí),是否可以保證順序(sort之后)?
如果一直保證寫(xiě)入遠(yuǎn)遠(yuǎn)大于讀出(間隔一秒以上),這樣是永遠(yuǎn)不會(huì)出現(xiàn)亂序的情況。
我們來(lái)看下樣例
現(xiàn)在看到圖中,取出數(shù)據(jù)兩次
第一次
4df2dcec aaaa ffff 36a8b813
4df2dcec aaaa eeee 36a8b813
4df2dcec bbbb 1111 36a8b814
第二次
4df2dcec bbbb 1111 36a8b813
4df2dcec aaaa ffff 36a8b814
4df2dcec aaaa eeee 36a8b814
現(xiàn)在如果取第一次的最大值(4df2dcec bbbb 1111 36a8b814)做下次查詢的結(jié)果,那么就會(huì)漏掉
第二次的三條,因?yàn)椋?df2dcec bbbb 1111 36a8b814)大于第二次取的所有記錄。
所以會(huì)導(dǎo)致丟數(shù)據(jù)的情況。
解決辦法:
由于ObjectId的時(shí)間戳截止到秒,而counter算子前四位又為機(jī)器與進(jìn)程號(hào)。
1.處理一定時(shí)間間隔前的記錄(一秒以上),這樣即使機(jī)器和進(jìn)程號(hào)導(dǎo)致亂序,間隔前也不會(huì)出現(xiàn)亂序情況。
2.單點(diǎn)插入,原來(lái)分布到幾個(gè)點(diǎn)的插入操作,現(xiàn)在統(tǒng)一由一個(gè)點(diǎn)查詢,保證機(jī)器與進(jìn)程號(hào)相同,使用counter算子使記錄有序。
這里,我們用到了第一種辦法。
誤區(qū) 三 、不在DBObject設(shè)置_id使用mongoDB設(shè)置ObjectId?
mongoDB插入操作時(shí),new DBBasicObject()時(shí),大家看到_id是沒(méi)有被填值的,除非手工的設(shè)置_id。那么是否是服務(wù)端設(shè)置的呢?
大家來(lái)看下插入操作的代碼:
實(shí)現(xiàn)類(lèi)
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); }
可以看到需要添加,默認(rèn)都為添加
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; }
自動(dòng)添加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的驅(qū)動(dòng)包中是會(huì)自動(dòng)添加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 ); }
綜上所述,默認(rèn)情況下ObjectId是由客戶端生成的,并不是不設(shè)置就由服務(wù)端生成的。
誤區(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");
獲取自增變量會(huì)使用這種方法編寫(xiě),但是,我們執(zhí)行完成后會(huì)發(fā)現(xiàn)。
findAndModify操作,是先執(zhí)行了find,再執(zhí)行了modify,所以當(dāng)result為null時(shí),應(yīng)該新增并返回0
以上所述是小編給大家介紹的MongoDB中ObjectId的誤區(qū)及引起的一系列問(wèn)題,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
解決java調(diào)用python代碼返回值中文亂碼問(wèn)題
這篇文章主要介紹了解決java調(diào)用python代碼返回值中文亂碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05JavaCV與FFmpeg音視頻流處理技巧總結(jié)大全
JavaCV是一個(gè)開(kāi)源的Java接口,它為幾個(gè)著名的計(jì)算機(jī)視覺(jué)庫(kù)(如OpenCV、FFmpeg)提供了Java封裝,這篇文章主要給大家介紹了關(guān)于JavaCV與FFmpeg音視頻流處理技巧總結(jié)的相關(guān)資料,需要的朋友可以參考下2024-05-05java開(kāi)發(fā)https請(qǐng)求ssl不受信任問(wèn)題解決方法
這篇文章主要介紹了java開(kāi)發(fā)https請(qǐng)求ssl不受信任問(wèn)題解決方法,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01淺談讓@Value更方便的Spring自定義轉(zhuǎn)換類(lèi)
Spring為大家內(nèi)置了不少開(kāi)箱即用的轉(zhuǎn)換類(lèi),如字符串轉(zhuǎn)數(shù)字、字符串轉(zhuǎn)時(shí)間等,但有時(shí)候需要使用自定義的屬性,則需要自定義轉(zhuǎn)換類(lèi)了2021-06-06Spring?Cloud中Sentinel的兩種限流模式介紹
如何使用Sentinel做流量控制呢?這篇文章就來(lái)為大家詳細(xì)介紹了Spring?Cloud中Sentinel的兩種限流模式,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-05-05