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
相关文章
相关标签/搜索