前段時間有個朋友問我,分布式主鍵生成策略在我們這邊是怎么實(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
突然想起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" }
> 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ī)范

把 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í)慣的時間格式
前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
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條記錄的唯一性。
接下來的三個字節(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)行下載,下面摘錄部分代碼
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;
}

(完整代碼請查看源碼)

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)建
可以看到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 );
}
}
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ū)分的,大約是下面的類型
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:
/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)定的
by 陳于喆
QQ:34174409
Mail: dongbule@163.com
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;
----------------------------------------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)化等等。
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