于吉吉的技術(shù)博客

          建造高性能門戶網(wǎng)

            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理 ::
            65 隨筆 :: 6 文章 :: 149 評論 :: 0 Trackbacks
          前段時間有個朋友問我,分布式主鍵生成策略在我們這邊是怎么實(shí)現(xiàn)的,當(dāng)時我給的答案是sequence,當(dāng)然這在不高并發(fā)的情況下是沒有任何問題,實(shí)際上,我們的主鍵生成是可控的,但如果是在分布式高并發(fā)的情況下,那肯定是有問題的。

          突然想起mongodb的objectid,記得以前看過文檔,objectid是一種輕量型的,不同的機(jī)器都能用全局唯一的同種方法輕量的生成它,而不是采用傳統(tǒng)的自增的主鍵策略,因?yàn)樵诙嗯_服務(wù)器上同步自動增加主鍵既費(fèi)力又費(fèi)時,不得不佩服,mongodb從開始設(shè)計(jì)就被定義為分布式數(shù)據(jù)庫。
          下面深入一點(diǎn)來翻翻這個Objectid的底細(xì),在mongodb集合中的每個document中都必須有一個"_id"建,這個鍵的值可以是任何類型的,在默認(rèn)的情況下是個Objectid對象。
          當(dāng)我們讓一個collection中插入一條不帶_id的記錄,系統(tǒng)會自動地生成一個_id的key

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

          可以發(fā)現(xiàn)這里多出一個Objectid類型的_id,當(dāng)然了,這個_id是系統(tǒng)默認(rèn)生成的,你也可以為其指定一個值,不過在同一collections中該值必須是唯一的

          把 ObjectId("4df2dcec2cdcd20936a8b817")這串值拿出來并對照官網(wǎng)的解析來深入分析。

          "4df2dcec2cdcd20936a8b817" 以這段字符串為例來進(jìn)行解析,這是一個24位的字符串,看起來很長,很難理解,實(shí)際上它是由ObjectId(string)所創(chuàng)建的一組十六進(jìn)制的字符,每個字節(jié)兩位的十六進(jìn)制數(shù)字,總共使用了12字節(jié)的存儲空間,可能有些朋友會感到很奇怪,居然是用了12個字節(jié),而mysql的INT類型也只有4個字節(jié),不過按照現(xiàn)在的存儲設(shè)備,多出來的這點(diǎn)字節(jié)也應(yīng)該不會成為什么瓶頸,實(shí)際上,mongodb在設(shè)計(jì)上無處不在的體現(xiàn)著用空間換時間的思想,接下看吧

          下面是官網(wǎng)指定Bson中ObjectId的詳細(xì)規(guī)范



          TimeStamp
          前4位是一個unix的時間戳,是一個int類別,我們將上面的例子中的objectid的前4位進(jìn)行提取“4df2dcec”,然后再將他們安裝十六進(jìn)制專為十進(jìn)制:“1307761900”,這個數(shù)字就是一個時間戳,為了讓效果更佳明顯,我們將這個時間戳轉(zhuǎn)換成我們習(xí)慣的時間格式

          $ date -d '1970-01-01 UTC 1307761900  sec'  -u
          2011年 06月 11日 星期六 03:11:40 UTC

          前4個字節(jié)其實(shí)隱藏了文檔創(chuàng)建的時間,并且時間戳處在于字符的最前面,這就意味著ObjectId大致會按照插入進(jìn)行排序,這對于某些方面起到很大作用,如作為索引提高搜索效率等等。使用時間戳還有一個好處是,某些客戶端驅(qū)動可以通過ObjectId解析出該記錄是何時插入的,這也解答了我們平時快速連續(xù)創(chuàng)建多個Objectid時,會發(fā)現(xiàn)前幾位數(shù)字很少發(fā)現(xiàn)變化的現(xiàn)實(shí),因?yàn)槭褂玫氖钱?dāng)前時間,很多用戶擔(dān)心要對服務(wù)器進(jìn)行時間同步,其實(shí)這個時間戳的真實(shí)值并不重要,只要其總不停增加就好。

          Machine
          接下來的三個字節(jié),就是 2cdcd2 ,這三個字節(jié)是所在主機(jī)的唯一標(biāo)識符,一般是機(jī)器主機(jī)名的散列值,這樣就確保了不同主機(jī)生成不同的機(jī)器hash值,確保在分布式中不造成沖突,這也就是在同一臺機(jī)器生成的objectid中間的字符串都是一模一樣的原因。

          pid
          上面的Machine是為了確保在不同機(jī)器產(chǎn)生的objectid不沖突,而pid就是為了在同一臺機(jī)器不同的mongodb進(jìn)程產(chǎn)生了objectid不沖突,接下來的0936兩位就是產(chǎn)生objectid的進(jìn)程標(biāo)識符。

          increment
          前面的九個字節(jié)是保證了一秒內(nèi)不同機(jī)器不同進(jìn)程生成objectid不沖突,這后面的三個字節(jié)a8b817,是一個自動增加的計(jì)數(shù)器,用來確保在同一秒內(nèi)產(chǎn)生的objectid也不會發(fā)現(xiàn)沖突,允許256的3次方等于16777216條記錄的唯一性。

          客戶端生成
          mongodb產(chǎn)生objectid還有一個更大的優(yōu)勢,就是mongodb可以通過自身的服務(wù)來產(chǎn)生objectid,也可以通過客戶端的驅(qū)動程序來產(chǎn)生,如果你仔細(xì)看文檔你會感嘆,mongodb的設(shè)計(jì)無處不在的使

          用空間換時間的思想,比較objectid是輕量級,但服務(wù)端產(chǎn)生也必須開銷時間,所以能從服務(wù)器轉(zhuǎn)移到客戶端驅(qū)動程序完成的就盡量的轉(zhuǎn)移,必須將事務(wù)扔給客戶端來完成,減低服務(wù)端的開銷,另還有一點(diǎn)原因就是擴(kuò)展應(yīng)用層比擴(kuò)展數(shù)據(jù)庫層要變量得多。

          好吧,既然我們了解到我們的程序產(chǎn)生objectid是在客戶端完成,那再繼續(xù),進(jìn)一步了解,打開mongodb java driver源碼,無源碼可以到mongodb官網(wǎng)進(jìn)行下載,下面摘錄部分代碼

          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;
              }


          (完整代碼請查看源碼)

          這里可以發(fā)現(xiàn)ObjectId的構(gòu)建可以有多種方式,可以由自己制定字節(jié),也可以指定時間,機(jī)器碼和自增值,這里重點(diǎn)看看驅(qū)動程序默認(rèn)的構(gòu)建,也就是public ObjectId()
          可以看到objectid主要由_time _machine _inc 所組成,其中 _time直接由(System.currentTimeMillis() / 1000)計(jì)算出所謂的時間戳,這里很簡單,接下來是重點(diǎn),主要看看機(jī)器碼和進(jìn)程碼的構(gòu)建

           private static final int _genmachine;
              
          static {

                  
          try {
                      final int machinePiece;//機(jī)器碼塊
                      {
                          StringBuilder sb 
          = new StringBuilder();
                          Enumeration
          <NetworkInterface> e = NetworkInterface.getNetworkInterfaces();//NetworkInterface此類表示一個由名稱和分配給此接口的 IP 地址列表組成的網(wǎng)絡(luò)接口,它用于標(biāo)識將多播組加入的本地接口,這里通過NetworkInterface此機(jī)器上所有的接口
                          while ( e.hasMoreElements() ){
                              NetworkInterface ni 
          = e.nextElement();
                              sb.append( ni.toString() );
                          }
                          machinePiece 
          = sb.toString().hashCode() << 16//將得到所有接口的字符串進(jìn)行取散列值
                          LOGGER.fine( "machine piece post: " + Integer.toHexString( machinePiece ) );
                      }

                      final int processPiece;//進(jìn)程塊
                      {
                          
          int processId = new java.util.Random().nextInt();
                          
          try {
                              processId 
          = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().hashCode();//RuntimeMXBean是Java虛擬機(jī)的運(yùn)行時系統(tǒng)的管理接口,這里是返回表示正在運(yùn)行的 Java 虛擬機(jī)的名稱,并進(jìn)行取散列值。
                          }
                          
          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; //最后將機(jī)器碼塊的散列值與進(jìn)程塊的散列值進(jìn)行位或運(yùn)算,得到 _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")區(qū)分的,大約是下面的類型

          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:

          這里為什么要采取這樣方面進(jìn)行取散列值,感覺有些不太理解,應(yīng)該網(wǎng)絡(luò)接口本身相對而言是并不穩(wěn)定的

          int processId = new java.util.Random().nextInt();
           try {
                  processId = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().hashCode();
           }
           catch ( Throwable t ){
          }

          RuntimeMXBean是Java虛擬機(jī)的運(yùn)行時系統(tǒng)的管理接口,這里是返回表示正在運(yùn)行的 Java 虛擬機(jī)的名稱,并進(jìn)行取散列值,如果在這過程中出現(xiàn)異常,processId 將以隨機(jī)數(shù)的方式繼續(xù)計(jì)算

          _genmachine = machinePiece | processPiece;
          最后將機(jī)器碼塊的散列值與進(jìn)程塊的散列值進(jìn)行位或運(yùn)算,當(dāng)然這里是十進(jìn)制,你把這里的十進(jìn)制專為十六進(jìn)制,就會發(fā)現(xiàn)這塊的值就是生產(chǎn)objectid中間部分的值,這里的構(gòu)建跟服務(wù)端的構(gòu)建是有些不一樣的,不過最基本的構(gòu)建元素還是一致的,就是TimeStamp,Machine ,pid,increment

          mongodb的ObejctId生產(chǎn)思想在很多方面挺值得我們借鑒的,特別是在大型分布式的開發(fā),如何構(gòu)建輕量級的生產(chǎn),如何將生產(chǎn)的負(fù)載進(jìn)行轉(zhuǎn)移,如何以空間換取時間提高生產(chǎn)的最大優(yōu)化等等。
          ----------------------------------------

          by 陳于喆
          QQ:34174409
          Mail: dongbule@163.com
          posted on 2011-06-12 18:30 陳于喆 閱讀(15260) 評論(8)  編輯  收藏 所屬分類: javaMongoDB

          評論

          # re: 說說MongoDB的ObjectId 2011-06-13 09:23 @joe
          guid足以,就是解決多節(jié)點(diǎn)問題的。  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2011-06-13 10:14 陳于喆
          @@joe
          guid雖然好處多多,但存儲大,索引也慢,還有不美觀  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2011-06-15 13:13 懶人助手
          不錯,不錯,http://www.lrtool.net  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2011-06-15 23:24 RunCode
          分析得很不錯  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId[未登錄] 2012-03-22 11:40 小刀
          講的非常詳細(xì),蠻不錯  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2012-03-29 12:24 習(xí)慣性路過
          @陳于喆
          ObjectId也有"存儲大 索引慢 不美觀"這些缺點(diǎn)啊 它就是一簡化版GUID
          GUID是全球唯一的 ObjectID是特定DB集群內(nèi)唯一的  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2012-03-29 16:39 陳于喆
          @習(xí)慣性路過
          恩,有道理,那你說說為什么mongodb不直接使用GUID  回復(fù)  更多評論
            

          # re: 說說MongoDB的ObjectId 2014-01-07 22:37 Redbull
          @陳于喆
          一個可以逆向推算出數(shù)據(jù)對應(yīng)的( 插入時間,插入的機(jī)器),而GUID一個不能  回復(fù)  更多評論
            

          主站蜘蛛池模板: 固始县| 乡宁县| 资兴市| 宁乡县| 鸡东县| 当阳市| 榆中县| 石泉县| 郴州市| 梁河县| 遂溪县| 平阴县| 长丰县| 太仓市| 吐鲁番市| 黎川县| 根河市| 修文县| 吉安县| 松桃| 胶州市| 固原市| 枣庄市| 岳普湖县| 尚志市| 江津市| 格尔木市| 望奎县| 古丈县| 赞皇县| 铁岭市| 濮阳市| 泸定县| 台北县| 北安市| 阜城县| 崇文区| 金塔县| 内江市| 远安县| 博野县|