MongoDB中ObjectId的誤區,以及引發的一系列問題

近期對兩個應用進行改造,在上線過程當中出現一系列問題(其中一部分是因爲ObjectId誤區致使的)java

先來了解下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查詢語句
Query query = new Query();
		if (id != null)
		{
			query.addCriteria(Criteria.where("_id").gt(id));
		}
		query.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
相關文章
相關標籤/搜索