MongoDB中ObjectId的误区,以及引起的一系列问题

近期对两个应用进行改造,在上线过程中出现一系列问题(其中一部分是由于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查询语句

		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

时间: 2024-08-30 03:04:57

MongoDB中ObjectId的误区,以及引起的一系列问题的相关文章

MongoDB中的_id和ObjectId

ObjectId是"_id"的默认类型.它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它. 这是MongoDB采用ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个 服务器上同步自动增加主键值既费力还费时.MongoDB从一开始就设计用来作为分布式数据库,处理多个节 点是一个核心要求.后面会讲到ObjectId类型在分片环境中容易生成得多. ObjectId使用12字节的存储空间,每个字节两位十六进制数字,是一个24位的字符串.由于看

mongodb中的_id的ObjectId的生成规则

MongoDB中存储的文档必须有一个"_id" .这个键值可以是任何类型,默认是ObjectID对象.在一个集合里,每个文档都有一个唯一的“_id”,确保集合里的每个文档都能被唯一标示. ObjectID使用12字节的存储空间,是一个由24个16进制数字组成的字符串. ObjectId的12个字节按照如下方式生成 时间戳(1571234567123) 机器码(主机标识符) PID(进程id) 计数器 0,1,2,3 4,5,6 7,8 9,10,11 前四位是时间戳,可以提供秒级别的唯

MongoDB中常用的find

接着前一篇文章,下面主要介绍一下MongoDB中常用的find操作. 先打开MongoDB shell,通过下面一组命令插入一些数据. 1 post1 = {"title":"learn MongoDB", "author":"Wilber", "date":new Date(), "score":90} 2 post2 = {"title":"learn

MongoDB中空间数据的存储和操作

本文使用官方C# Driver,实现在MongoDB中存储,查询空间数据(矢量) 空间数据的存储 本例中,从一个矢量文件(shapefile格式)中读取矢量要素空间信息以及属性表,并写入到MongoDB中去,其中读取shapefile文件以及将空间信息转成json的功能通过Ogr库实现 //打开MongoDB的Collection MongoDatabase db = server.GetDatabase("aa"); MongoCollection colSheng = db.Get

MongoDB中索引的创建和使用详解

索引通常能够极大的提高查询的效率.在系统中使用查询时,应该考虑建立相关的索引.在MongoDB中创建索引相对比较容易. mongodb中的索引在概念上和大多数关系型数据库如MySQL是一样的.当你在某种情况下需要在MySQL中建立索引,这样的情景同样适合于MongoDB. 基本操作 索引是一种数据结构,他搜集一个集合中文档特定字段的值.MongoDB的查询优化器能够使用这种数据结构来快速的对集合(collection)中的文档(collection)进行寻找和排序.准确来说,这些索引是通过B-T

MongoDB 基础(三)mongodb 中的索引使用

MongoDB中的索引和其他数据库索引类似,也是使用B-Tree结构.MongoDB的索引是在collection级别上的,并且支持在任何列或者集合内的文档的子列中创建索引. 下面是官方给出的一个使用索引查询和排序的一个结构图. 所有的MongoDB集合默认都有一个唯一索引在字段"_id"上,如果应用程序没有为 "_id"列定义一个值,MongoDB将创建一个带有ObjectId值的列.(ObjectId是基于 时间.计算机ID.进程ID.本地进程计数器 生成的)

Spring中映射Mongodb中注解的解释

spring-data-mongodb中的实体映射是通过MongoMappingConverter这个类实现的.它可以通过注释把java类转换为mongodb的文档. 它有以下几种注释: @Id - 文档的唯一标识,在mongodb中为ObjectId,它是唯一的,通过时间戳+机器标识+进程ID+自增计数器(确保同一秒内产生的Id不会冲突)构成. @Document - 把一个java类声明为mongodb的文档,可以通过collection参数指定这个类对应的文档.@Document(coll

MongoDB中insert方法、update方法、save方法简单对比

MongoDB中insert方法.update方法.save方法简单对比 1.update方法 该方法用于更新数据,是对文档中的数据进行更新,改变则更新,没改变则不变. 2.insert方法 该方法用于插入数据到文档中,也就是给文档添加新数据. 3.save方法 该方法同样用于插入数据到文档中,功能是类似于insert方法的.与insert方法不同的是, save方法是遍历文档,逐条将数据插入进去的,而insert方法是将整个文档整体插入进去的. 由两个方法的源码可以看出来. save方法的写法

在MongoDB中实现聚合函数

在MongoDB中实现聚合函数 随着组织产生的数据爆炸性增长,从GB到TB,从TB到PB,传统的数据库已经无法通过垂直扩展来管理如此之大数据.传统方法存储和处理数据的成本将会随着数据量增长而显著增加.这使得很多组织都在寻找一种经济的解决方案,比如NoSQL数据库,它提供了所需的数据存储和处理能力.扩展性和成本效率.NoSQL数据库不使用SQL作为查询语言.这种数据库有多种不同的类型,比如文档结构存储.键值结构存储.图结构.对象数据库等等. 我们在本文中使用的NoSQL是MongoDB,它是一种开