MongoDB的ObjectId

前段时间有个朋友问我,分布式主键生成策略在我们这边是怎么实现的,当时我给的答案是sequence,当然这在不高并发的情况下是没有任何问题,实际上,我们的主键生成是可控的,但如果是在分布式高并发的情况下,那肯定是有问题的。
突然想起mongodb的objectid,记得以前看过文档,objectid是一种轻量型的,不同的机器都能用全局唯一的同种方法轻量的生成它,而不是采用传统的自增的主键策略,因为在多台服务器上同步自动增加主键既费力又费时,不得不佩服,mongodb从开始设计就被定义为分布式数据库。
下面深入一点来翻翻这个Objectid的底细,在mongodb集合中的每个document中都必须有一个”_id”建,这个键的值可以是任何类型的,在默认的情况下是个Objectid对象。
当我们让一个collection中插入一条不带_id的记录,系统会自动地生成一个_id的key

> db.t_test.insert({“name”:”cyz”})
> db.t_test.findOne({“name”:”cyz”})
{ “_id” : ObjectId(“4df2dcec2cdcd20936a8b817″), “name” : “cyz” }

可以发现这里多出一个Objectid类型的_id,当然了,这个_id是系统默认生成的,你也可以为其指定一个值,不过在同一collections中该值必须是唯一的
把 ObjectId(“4df2dcec2cdcd20936a8b817″)这串值拿出来并对照官网的解析来深入分析。
“4df2dcec2cdcd20936a8b817″ 以这段字符串为例来进行解析,这是一个24位的字符串,看起来很长,很难理解,实际上它是由ObjectId(string)所创建的一组十六进制的字符,每个字节两位的十六进制数字,总共使用了12字节的存储空间,可能有些朋友会感到很奇怪,居然是用了12个字节,而mysql的INT类型也只有4个字节,不过按照现在的存储设备,多出来的这点字节也应该不会成为什么瓶颈,实际上,mongodb在设计上无处不在的体现着用空间换时间的思想,接下看吧
下面是官网指定Bson中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条记录的唯一性。

客户端生成
mongodb产生objectid还有一个更大的优势,就是mongodb可以通过自身的服务来产生objectid,也可以通过客户端的驱动程序来产生,如果你仔细看文档你会感叹,mongodb的设计无处不在的使
用空间换时间的思想,比较objectid是轻量级,但服务端产生也必须开销时间,所以能从服务器转移到客户端驱动程序完成的就尽量的转移,必须将事务扔给客户端来完成,减低服务端的开销,另还有一点原因就是扩展应用层比扩展数据库层要变量得多。
好吧,既然我们了解到我们的程序产生objectid是在客户端完成,那再继续,进一步了解,打开mongodb java driver源码,无源码可以到mongodb官网进行下载,下面摘录部分代码

public class ObjectId implements Comparable<ObjectId> , java.io.Serializable {

    final int _time;
    final int _machine;
    final int _inc;

    public ObjectId( byte[] b ){
        if ( b.length != 12 )
            throw new IllegalArgumentException( “need 12 bytes“ );
        ByteBuffer bb = ByteBuffer.wrap( b );
        _time = bb.getInt();
        _machine = bb.getInt();
        _inc = bb.getInt();
        _new = false;
    }
    
    public ObjectId( int time , int machine , int inc ){
        _time = time;
        _machine = machine;
        _inc = inc;
        _new = false;
    }
    
    public ObjectId(){
        _time = (int) (System.currentTimeMillis() / 1000);
        _machine = _genmachine;
        _inc = _nextInc.getAndIncrement();
        _new = true;
    }

(完整代码请查看源码)

这里可以发现ObjectId的构建可以有多种方式,可以由自己制定字节,也可以指定时间,机器码和自增值,这里重点看看驱动程序默认的构建,也就是public ObjectId()
可以看到objectid主要由_time _machine _inc 所组成,其中 _time直接由(System.currentTimeMillis() / 1000)计算出所谓的时间戳,这里很简单,接下来是重点,主要看看机器码和进程码的构建

 private static final int _genmachine;
    static {
        try {
            final int machinePiece;//机器码块
            {
                StringBuilder sb = new StringBuilder();
                Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();//NetworkInterface此类表示一个由名称和分配给此接口的 IP 地址列表组成的网络接口,它用于标识将多播组加入的本地接口,这里通过NetworkInterface此机器上所有的接口
                while ( e.hasMoreElements() ){
                    NetworkInterface ni = e.nextElement();
                    sb.append( ni.toString() );
                }
                machinePiece = sb.toString().hashCode() << 16; //将得到所有接口的字符串进行取散列值
                LOGGER.fine( “machine piece post: “ + Integer.toHexString( machinePiece ) );
            }
            final int processPiece;//进程块
            {
                int processId = new java.util.Random().nextInt();
                try {
                    processId = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().hashCode();//RuntimeMXBean是Java虚拟机的运行时系统的管理接口,这里是返回表示正在运行的 Java 虚拟机的名称,并进行取散列值。
                }
                catch ( Throwable t ){
                }
                ClassLoader loader = ObjectId.class.getClassLoader();
                int loaderId = loader != null ? System.identityHashCode(loader) : 0;
                StringBuilder sb = new StringBuilder();
                sb.append(Integer.toHexString(processId));
                sb.append(Integer.toHexString(loaderId));
                processPiece = sb.toString().hashCode() & 0xFFFF;
                LOGGER.fine( “process piece: “ + Integer.toHexString( processPiece ) );
            }
            _genmachine = machinePiece | processPiece; //最后将机器码块的散列值与进程块的散列值进行位或运算,得到 _genmachine 
            LOGGER.fine( “machine : “ + Integer.toHexString( _genmachine ) );
        }
        catch ( java.io.IOException ioe ){
            throw new RuntimeException( ioe );
        }
    }

 Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
               while ( e.hasMoreElements() ){
                    NetworkInterface ni = e.nextElement();
                    sb.append( ni.toString() );
                }
 machinePiece = sb.toString().hashCode() << 16;
这里的NetworkInterface.getNetworkInterfaces();取得的接口通常是按名称(如 “le0″)区分的,大约是下面的类型

name:lo (Software Loopback Interface 1) index: 1 addresses:
/0:0:0:0:0:0:0:1;
/127.0.0.1;
name:net0 (WAN Miniport (SSTP)) index: 2 addresses:
name:net1 (WAN Miniport (IKEv2)) index: 3 addresses:
name:net2 (WAN Miniport (L2TP)) index: 4 addresses:
name:net3 (WAN Miniport (PPTP)) index: 5 addresses:
name:ppp0 (WAN Miniport (PPPOE)) index: 6 addresses:

这里为什么要采取这样方面进行取散列值,感觉有些不太理解,应该网络接口本身相对而言是并不稳定的

int processId = new java.util.Random().nextInt();
 try {
        processId = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().hashCode();
 }
 catch ( Throwable t ){
}
RuntimeMXBean是Java虚拟机的运行时系统的管理接口,这里是返回表示正在运行的 Java 虚拟机的名称,并进行取散列值,如果在这过程中出现异常,processId 将以随机数的方式继续计算
_genmachine = machinePiece | processPiece;

最后将机器码块的散列值与进程块的散列值进行位或运算,当然这里是十进制,你把这里的十进制专为十六进制,就会发现这块的值就是生产objectid中间部分的值,这里的构建跟服务端的构建是有些不一样的,不过最基本的构建元素还是一致的,就是TimeStamp,Machine ,pid,increment。
mongodb的ObejctId生产思想在很多方面挺值得我们借鉴的,特别是在大型分布式的开发,如何构建轻量级的生产,如何将生产的负载进行转移,如何以空间换取时间提高生产的最大优化等等。

—————————————-

时间: 2024-08-07 00:00:00

MongoDB的ObjectId的相关文章

mongodb通过ObjectId按照时间筛选备份数据库

要完成上述引言里的需求,我们这里从mongo的ObjectId入手,我们知道ObjectId的前四个字节是时间戳,那么我们可以在mongodump -q 来筛选记录.如果你不了解mongo ObjectId的构造请参看<_id和ObjectId>. 首先我们的使用场景是输入一个时间格式,然后返回该时间的最小ObjectId值,然后只要 在MongoDB shell中运行: function objectIdWithTimestamp(timestamp) {    // Convert str

MongoDB开发学习(1)开天辟地,经典入门

一,简介  MongoDB是一个基于分布式文件存储的数据库.由C++语言编写.旨在为WEB应用提供可扩展的高性能数据存储解决方案.  MongoDB是一个高性能,开源,无模式的文档型数据库,是当前NoSql数据库中比较热门的一种.      MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的.他支持的数据结构非常松散,是类似json的bjson格式,因此可以存储比较复杂的数据类型.Mongo最大的特点是他支持的查询语言非常强大,其语法有点类

PHP库 查询Mongodb中的文档ID的方法_MongoDB

在IBM我的一份新工作是一名开发的后勤人员.那意味着我的大部分时间是在和数据库打交道.在我的工作流程中,我花了一些时间在MongoDB上面--这是一个文档数据库.但是在通过ID来检索记录这个操作上面我碰到了一些问题.下面的代码是最终版本,以后碰到类似的问题我可以直接引用它.如果大家也需要,希望下面对大家有所帮助. MongoDB 和 IDs 当我向一个集合中插入数据的时候,我并没有设置_id字段:如果这个字段是空的话,那么MongoDB将要自动生成一个ID来使用,这对我来说是非常不错的.然而,当

浅析Mongodb性能优化的相关问题_MongoDB

前言 如何能让软件拥有更高的性能?我想这是一个大部分开发者都思考过的问题.性能往往决定了一个软件的质量,如果你开发的是一个互联网产品,那么你的产品性能将更加受到考验,因为你面对的是广大的互联网用户,他们可不是那么有耐心的.严重点说,页面的加载速度每增加一秒也许都会使你失去一部分用户,也就是说,加载速度和用户量是成反比的.那么用户能够接受的加载速度到底是多少呢? 如图,如果页面加载时间超过10s那么用户就会离开,如果1s–10s的话就需要有提示,但如果我们的页面没有提示的话需要多快的加载速度呢?是

MongoDB快速翻页的方法_MongoDB

翻阅数据是MongoDB最常见的操作之一.一个典型的场景是需要在你的用户界面中显示你的结果.如果你是批量处理的数据,同样重要的是要让你的分页策略正确,以便你的数据处理可以规模化. 接下来,让我们通过一个例子来看在MongoDB中翻阅数据的不同方式.在这个例子中,我们有一个CRM数据库的用户数据,我们需要通过翻阅浏览和在同一时间显示10个用户.所以实际上,我们的页面大小是10.下方是我们的用户文档的结构: { _id, name, company, state } 方法一:Using skip()

学习Mongodb(一)

图片摘录自陈彦铭出品2012.5的<10天掌握MongDB> MongoDB的特点--->面向集合存储,易于存储对象类型的数据--->模式自由--->支持动态查询--->支持完全索引,包含内部对象--->支持查询--->支持复制和故障恢复--->使用高效的二进制数据存储,包括大型对象(如视频等)--->自动处理碎片,以支持云计算层次的扩展性--->支持 Python,PHP,Ruby,Java,C,C#,Javascript,Perl 及

分布式ID生成器

最近会写一篇分布式的ID生成器的文章,先占位.借鉴Mongodb的ObjectId的生成: 4byte时间戳 + 3byte机器标识 + 2byte PID + 3byte自增id 简单代码: import com.google.common.base.Objects; import java.net.NetworkInterface; import java.nio.ByteBuffer; import java.util.Date; import java.util.Enumeration;

分片(Sharding)的全局ID生成

 这里最后redis生成ID的文章已经过时,新的请参考: http://blog.csdn.net/hengyunabc/article/details/44244951 前言 数据在分片时,典型的是分库分表,就有一个全局ID生成的问题.单纯的生成全局ID并不是什么难题,但是生成的ID通常要满足分片的一些要求: 不能有单点故障. 以时间为序,或者ID里包含时间.这样一是可以少一个索引,二是冷热数据容易分离. 可以控制ShardingId.比如某一个用户的文章要放在同一个分片内,这样查询效率高,修

高性能可定制化分布式发号器

玄靖 刘兵,花名玄靖,目前供职于阿里巴巴,开源技术爱好者,高性能Redis中间件NRedis-Proxy作者,目前研究方向为java中间件,微服务等技术. (一) 什么是分布式发号器     说起分布式发号器的前生今世,咱们应该感恩这个时代:随着互联网在中国越来越普及化,单机系统或者一个小系统已经无法满足需要,随着用户逐渐增多,数据量越来越大,单个应用或者单个数据库已经无法满足需求,在应用以至于微服务来临,在数据库存储方面分库分表来临,可以解决问题:但是新的问题产生,怎么样做到多个应用可以有唯一