Mongo中ObjectId是一个12字节的BSON类型字符串。ObjectId是小的,接近唯一且快速生成的有序的id
* 4 byte timestamp 5 byte process unique 3 byte counter
* |<----------------->|<---------------------->|<------------->
* OID layout: [----|----|----|----|----|----|----|----|----|----|----|----]
* 0 4 8 12
3.2版本之前包括3.2
- 4字节 UNIX时间戳
- 3字节 机器识别码
- 2字节 进程id
- 3字节 随机数开始的计数器
3.4版本之后包含3.4
- 4字节 UNIX时间戳
- 5字节 随机数
- 3字节 随机数开始的计数器
为什么会更新
引用自Mongo ObjectId 早就不用机器标识和进程号了 | Wolfogre's Blog
mongo官方没有给出详细解释,从网上看到有人的主观猜想。mongo 的 C++ 源码中,设置 ObjectId 中间 5 个字节的函数叫 setInstanceUnique
,而在官方 golang 驱动中叫 processUnique
。字面意思相近,都是说明这个值的作用是“区分不同进程实例”。而这个值具体怎么实现并没有什么要求,所以,使用“机器标识+进程号”来拿区分不同进程实例是可以的,使用互无关联的随机数来拿区分不同进程实例也是可以的。
可想而知,“在同一秒内,两个进程实例产生了相同的 5 字节随机数,且刚巧这时候两个进程的自增计数器的值也是相同的”——这种情况发生的概率实在太低了,完全可以认为不可能发生,所以使用互无关联的随机数来拿区分不同进程实例是完全合乎需求的。
为什么不继续使用“机器标识+进程号”呢?
- 机器标识码,ObjectId 的机器标识码是取系统 hostname 哈希值的前几位,问题来了,想必在座的各位都有干过吧:准备了几台虚拟机,hostname 都是默认的 localhost,谁都想着这玩意儿能有什么用,还得刻意给不同机器起不同的 hostname?此外,hostname 在容器、云主机里一般默认就是随机数,也不会检查同一集群里是否有 hostname 重名。
- 进程号,这个问题就更大了,要知道,容器内的进程拥有自己独立的进程空间,在这个空间里只用它自己这一个进程(以及它的子进程),所以它的进程号永远都是 1。也就是说,如果某个服务(既可以是 mongo 实例也可以是 mongo 客户端)是使用容器部署的,无论部署多少个实例,在这个服务上生成的 ObjectId,第八第九个字节恒为
0000 0001
,相当于说这两个字节废了。
mongo server实现
#### mongo 源码中src\mongo\bson\oid.h
mongo::OID __oid() const {
return OID::from(value());
}
class OID {
public:
/**
* Functor compatible with std::hash for std::unordered_{map,set}
* Warning: The hash function is subject to change. Do not use in cases where hashes need
* to be consistent across versions.
*/
struct Hasher {
size_t operator()(const OID& oid) const;
};
OID() : _data() {}
enum { kOIDSize = 12, kTimestampSize = 4, kInstanceUniqueSize = 5, kIncrementSize = 3 };
explicit OID(const std::string& s) {
init(s);
}
}
void OID::init() {
// each set* method handles endianness
setTimestamp(time(0)); // time(0)当前时间 精确到s
setInstanceUnique(_instanceUnique);
setIncrement(Increment::next());
}
void OID::setTimestamp(const OID::Timestamp timestamp) {
_view().write<BigEndian<Timestamp>>(timestamp, kTimestampOffset);
}
void OID::setInstanceUnique(const OID::InstanceUnique unique) {
// Byte order doesn't matter here
_view().write<InstanceUnique>(unique, kInstanceUniqueOffset);
}
void OID::setIncrement(const OID::Increment inc) {
_view().write<Increment>(inc, kIncrementOffset);
}
void OID::regenMachineId() {
std::unique_ptr<SecureRandom> entropy(SecureRandom::create());
_instanceUnique = InstanceUnique::generate(*entropy);
}
c实现
// c driver
//bson-context.c
struct _bson_context_t {
/* flags are defined in bson_context_flags_t */
int flags;
int32_t seq32; // 4字节 取后三字节作为递增的随机数
int64_t seq64; // 8字节 后面的8字节,随机数5字节+3字节递增书
uint8_t rand[5]; // 5字节随机值
uint16_t pid; // 2字节进程id
void (*oid_set_seq32) (bson_context_t *context, bson_oid_t *oid); // 函数
void (*oid_set_seq64) (bson_context_t *context, bson_oid_t *oid); // 函数
/* this function pointer allows us to mock gethostname for testing. */
void (*gethostname) (char *out);
};
void
_bson_context_set_oid_rand (bson_context_t *context, bson_oid_t *oid)
{
BSON_ASSERT (context);
BSON_ASSERT (oid);
if (context->flags & BSON_CONTEXT_DISABLE_PID_CACHE) {
uint16_t pid = _bson_getpid ();
if (pid != context->pid) {
context->pid = pid;
/* randomize the random bytes, not the sequence. */
_bson_context_init_random (context, false);
}
}
memcpy (&oid->bytes[4], &context->rand, sizeof (context->rand)); // 设置4字节后的5个随机数字节
}
static void
_bson_context_set_oid_seq32_threadsafe (bson_context_t *context, /* IN */
bson_oid_t *oid) /* OUT */
{
int32_t seq = bson_atomic_int_add (&context->seq32, 1);
seq = BSON_UINT32_TO_BE (seq);
memcpy (&oid->bytes[9], ((uint8_t *) &seq) + 1, 3);
}
static void
_bson_context_set_oid_seq64_threadsafe (bson_context_t *context, /* IN */
bson_oid_t *oid) /* OUT */
{
int64_t seq = bson_atomic_int64_add (&context->seq64, 1); // 原子操作
seq = BSON_UINT64_TO_BE (seq);
memcpy (&oid->bytes[4], &seq, sizeof (seq));
}
static void
_bson_context_init (bson_context_t *context, bson_context_flags_t flags)
{
context->flags = (int) flags;
context->oid_set_seq32 = _bson_context_set_oid_seq32;
context->oid_set_seq64 = _bson_context_set_oid_seq64;
context->gethostname = _bson_context_get_hostname;
if ((flags & BSON_CONTEXT_THREAD_SAFE)) {
context->oid_set_seq32 = _bson_context_set_oid_seq32_threadsafe; // 线程安全的设置函数
context->oid_set_seq64 = _bson_context_set_oid_seq64_threadsafe;
}
context->pid = _bson_getpid ();
_bson_context_init_random (context, true);
}
static BSON_ONCE_FUN (_bson_context_init_default)
{
_bson_context_init (
&gContextDefault,
(BSON_CONTEXT_THREAD_SAFE | BSON_CONTEXT_DISABLE_PID_CACHE));
BSON_ONCE_RETURN;
}
bson_context_t *
bson_context_get_default (void)
{
// 构造默认的context
static bson_once_t once = BSON_ONCE_INIT;
bson_once (&once, _bson_context_init_default);
return &gContextDefault;
}
# oid初始化
void
bson_oid_init (bson_oid_t *oid, /* OUT */
bson_context_t *context) /* IN */
{
uint32_t now = (uint32_t) (time (NULL)); // 4字节的时间戳
BSON_ASSERT (oid);
if (!context) {
// 使用默认的context
context = bson_context_get_default ();
}
now = BSON_UINT32_TO_BE (now);
memcpy (&oid->bytes[0], &now, sizeof (now)); // 前四个自己赋值在这
_bson_context_set_oid_rand (context, oid); // 中间5字节的随机数赋值
context->oid_set_seq32 (context, oid); // 后3个字节赋值
}
cxx driver实现
// C++的实现依赖C的实现,依赖C实现的库在基础上进行了C++的封装
///
/// A BSON ObjectId value.
///
struct BSONCXX_API b_oid {
static constexpr auto type_id = type::k_oid;
oid value;
};
oid::oid() {
bson_oid_t oid;
bson_oid_init(&oid, nullptr);
std::memcpy(_bytes.data(), oid.bytes, sizeof(oid.bytes));
}
go driver 实现
//mongo-tools\vendor\gopkg.in\mgo.v2\bson\bson.go
// 暂未更新,比较旧的实现
// NewObjectId returns a new unique ObjectId.
func NewObjectId() ObjectId {
var b [12]byte
// Timestamp, 4 bytes, big endian # 时间戳
binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))
// Machine, first 3 bytes of md5(hostname)
// 机器码
b[4] = machineId[0]
b[5] = machineId[1]
b[6] = machineId[2]
// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
// 进程id
b[7] = byte(processId >> 8)
b[8] = byte(processId)
// Increment, 3 bytes, big endian
// 自增数
i := atomic.AddUint32(&objectIdCounter, 1)
b[9] = byte(i >> 16)
b[10] = byte(i >> 8)
b[11] = byte(i)
return ObjectId(b[:])
}
// 新版本
var objectIDCounter = readRandomUint32()
var processUnique = processUniqueBytes()
// New generates a new ObjectID.
func New() ObjectID {
var b [12]byte
binary.BigEndian.PutUint32(b[0:4], uint32(time.Now().Unix()))
copy(b[4:9], processUnique[:])
putUint24(b[9:12], atomic.AddUint32(&objectIDCounter, 1))
return b
}
// ……
func processUniqueBytes() [5]byte {
var b [5]byte
_, err := io.ReadFull(rand.Reader, b[:])
if err != nil {
panic(fmt.Errorf("cannot initialize objectid package with crypto.rand.Reader: %v", err))
}
return b
}
java driver实现
// mongo-java-driver-r4.1.0
// ObjectId.java
"objectId" -> new BsonObjectId(new ObjectId());
// Use primitives to represent the 5-byte random value.
private static final int RANDOM_VALUE1;
private static final short RANDOM_VALUE2;
private final int timestamp;
private final int counter;
private final int randomValue1;
private final short randomValue2;
public ObjectId() {
this(new Date());
}
public ObjectId(final Date date) {
this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false);
}
private ObjectId(final int timestamp, final int counter, final boolean checkCounter) {
this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter);
}
private ObjectId(final int timestamp, final int randomValue1, final short randomValue2, final int counter,
final boolean checkCounter) {
if ((randomValue1 & 0xff000000) != 0) {
throw new IllegalArgumentException("The machine identifier must be between 0 and 16777215 (it must fit in three bytes).");
}
if (checkCounter && ((counter & 0xff000000) != 0)) {
throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes).");
}
this.timestamp = timestamp;
this.counter = counter & LOW_ORDER_THREE_BYTES;
this.randomValue1 = randomValue1;
this.randomValue2 = randomValue2;
}
// 初始化RANDOM_VALUE1 这里不再采用机器编号+进程编号的方式了
static {
try {
SecureRandom secureRandom = new SecureRandom();
RANDOM_VALUE1 = secureRandom.nextInt(0x01000000); // 随机产生一个大于等于0不等于0x01000000(不包含0x01000000)的正整数
RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 可以看到Java对ObjectId的封装后数据所占用的空间14个字节
// 对齐后占用16字节
uuid简单实现版
import java.util.Date;
class MyUUID
{
private static int _machineID=0;
private static int _incID=0;
private static int getMachineID() {
//复杂些的可以通过主机主板序列号、IP、硬盘序列号等生成机器码,简单起见默认为1
_machineID+=1;
return _machineID%127;
}
private static int getInc()
{
//一般使用Memcached统一维护一个全局自增ID,简单起见默认为1
_incID+=1;
return _incID;
}
public static long ObjectID() {
long v_time = (int) (System.currentTimeMillis() / 1000);
int v_machine = getMachineID();
int v_inc = getInc();
long myUUID=0;
myUUID=myUUID | v_inc;
myUUID=myUUID | (v_machine<<24);
myUUID=myUUID | (v_time<<32);
return myUUID;
}
public static void reverseObjectID(long myUUID)
{
//反向解析myUUID
int v_inc =(int) myUUID & 0xFFFFFF;//取出最右边3字节(即24bit)的值,每个16进制数存储4bit
int v_machine=(int) (myUUID >>> 24) & 0xFF;//无符号右移24位,然后取最右边1字节的值
long v_time=(myUUID >>>32) & 0xFFFFFFFF;//无符号右移32位,然后取四字节的秒数
System.out.println("v_time="+Long.toHexString(v_time));
System.out.println("时间为:"+new java.util.Date(v_time*1000));
System.out.println("v_machine="+v_machine);
System.out.println("inc="+v_inc);
}
public static void main(String[] args)
{
long objectid;
long id1=0,id2=0;
//生成100000个MyUUID
for (int i=0; i<100000; i++)
{
objectid=ObjectID();
System.out.println("MyUUID="+Long.toHexString(objectid));
if (i==0)
id1=objectid;
else if (i==99999)
id2=objectid;
else;
}
System.out.println("反向解析MyUUID...................");
reverseObjectID(id1);
reverseObjectID(id2);
}
}
# -*- coding: utf-8 -*-
import os
import hashlib
import socket
import random
import threading
import struct
import time
import binascii
def _machine_bytes():
"""Get the machine portion of an ObjectId.
"""
machine_hash = hashlib.md5()
machine_hash.update(socket.gethostname())
return machine_hash.digest()[0:3]
_machine_bytes = _machine_bytes()
_inc_lock = threading.Lock()
_inc = random.randint(0, 0xFFFFFF)
oid = struct.pack(">i", int(time.time())) # 4字节当前时间
oid += _machine_bytes # 3字节机器码
oid += struct.pack(">H", os.getpid() % 0xFFFF) # 2字节pid
with _inc_lock:
oid += struct.pack(">i", _inc)[1:4] # 自增数加锁保证线程安全
_inc = (_inc + 1) % 0xFFFFFF
print binascii.hexlify(oid)
转换
mongodb的每一个document都有一个_id字段,是Mongo的ObjectId对象。ObjectId是一个12字节的BSON对象,但是在mongoshell中显示的时候字符串长度为24,这个字符串是一个16进制的数字,做所周知长度24的16进制的数字是12个字节。
在一般游戏开发中不会直接用ObjectId的16进制值来表示玩家的id,类似XbZceckLMEuRO3Tx,entity_id是一个长度为16字节的字符串。
entity_id -> base64 decode -> hex encode -> ObjectId
ObjectId -> hex decode -> base64 encode -> entity_id
# 转换代码
def entity_id2objectid(entity_id):
return base64.b64decode(entity_id).encode("hex")
def objectid2entity_id(oid):
return base64.b64encode(oid.decode("hex"))