当你有空闲时间的时候,看看 Redis、Mongo 等系统的设计,收获会颇丰。
我在前面的一章讲解了,MongoDB 中 ObjectId 的生成原理。Mongo 中的 ObjectId 设计的很精妙,长度比雪花算法还长,还能不完全依赖于时钟(比如时钟回拨,我们可以通过调整 pid 的形式保障 ObjcetId 不重复)。但是它也有缺点,就是长度太长,占用空间比雪花算法还大,在 Java 中我们必须通过字符串来标识它。
我猜有部分网友可能会说,雪花算法我通过 Java 就可以实现哈,单独为了一个分布式 ID,我放弃雪花,引入 MongoDB 并不明智。
确实是这样的,但是技术架构师在技术选型时,往往考虑的是 Mongo 的 NoSQL 特性。
Mongo 在设计之初,就考虑了分布式,高性能的插入等需求场景,所以在 ObjectId 的设计上,它还支持让客户端生成 ObjectId。
具体的生成方法,我们通过 Mongo 的 Java 驱动程序来一探究竟。
访问网址https://github.com/mongodb/mongo-java-driver/downloads
,下载 mongodb 的 java driver 源码(本地反编译驱动程序也是可以的,或者直接在 github 上看对应类的源码)。
驱动源码打开后,我们找到 org.bson 包下的 ObjectId.java 文件,针对性的看看这个类,进行分析。
默认构建的 objectId 代码主要由 _time,_machine 和 _inc 组成。为了方便讲解,我把部分源码摘录如下:
public class ObjectId implements Comparable<ObjectId>, Serializable {
final int _time;
final int _machine;
final int _inc;
boolean _new;
public ObjectId(){
_time = (int) (System.currentTimeMillis() / 1000);
_machine = _genmachine;
_inc = _nextInc.getAndIncrement();
_new = true;
}
}
需要说明的是,这里的 _machine 变量,实际上包括了机器码 machinePiece 和进程码 processPiece 两个部分。
_machine 变量的算法过程大致是这样的:先通过 NetworkInterface 这个类,获取机器的所有网络接口信息,然后将得到的字符串取散列值,就得到了机器码;再然后通过 RuntimeMXBean.getName() 方法获取 pid,接着再拼装 classloaderid,得到进程码;最后将机器码和进程码进行位或运算得到 _machine。当然这里生成的 _machine 是十进制的,需转成十六进制。
_time 就是我们的时间戳变量,直接由 System.currentTimeMillis() / 1000 计算得出的时间戳,当然这里生成的也是 10 进制的。
_inc 变量就是我们的自增变量,它的自增数是通过 Java 中的 AtomicInteger 类的 getAndIncrement() 方法获取,它能保证每次得到的值是一个递增并不重复的值。
需要注意的是,不同的版本,ObjectId 源码可能不一样,比如我又看了一个 mongo-java-driver-3.8.2.jar,它其中的变量分布如下,但是核心逻辑都是一样的。
public final class ObjectId implements Comparable<ObjectId>, Serializable {
private static final long serialVersionUID = 3670079982654483072L;
// ...
private static final AtomicInteger NEXT_COUNTER = new AtomicInteger((new SecureRandom()).nextInt());
private final int timestamp;
private final int machineIdentifier;
private final short processIdentifier;
private final int counter;
// ...
}
下面看看机器码的生成代码。
private static int createMachineIdentifier() {
int machinePiece;
try {
StringBuilder sb = new StringBuilder();
Enumeration e = NetworkInterface.getNetworkInterfaces();
while(e.hasMoreElements()) {
NetworkInterface ni = (NetworkInterface)e.nextElement();
sb.append(ni.toString());
byte[] mac = ni.getHardwareAddress();
if (mac != null) {
ByteBuffer bb = ByteBuffer.wrap(mac);
try {
sb.append(bb.getChar());
sb.append(bb.getChar());
sb.append(bb.getChar());
} catch (BufferUnderflowException var7) {
;
}
}
}
machinePiece = sb.toString().hashCode();
} catch (Throwable var8) {
machinePiece = (new SecureRandom()).nextInt();
LOGGER.debug("Failed to get machine identifier from network interface, using SecureRandom instead");
}
machinePiece &= 16777215;
return machinePiece;
}
机器码生成的逻辑和我上面说的 _machine 类似,下面再看看进程 pid 的获取方式。
private static short createProcessIdentifier() {
short processId;
try {
String processName = ManagementFactory.getRuntimeMXBean().getName();
if (processName.contains("@")) {
processId = (short)Integer.parseInt(processName.substring(0, processName.indexOf(64)));
} else {
processId = (short)ManagementFactory.getRuntimeMXBean().getName().hashCode();
}
} catch (Throwable var2) {
processId = (short)(new SecureRandom()).nextInt();
LOGGER.debug("Failed to get process identifier from JMX, using SecureRandom instead");
}
return processId;
}
nodejs 等其他语言的相关实现也是类似的,我就不在一一列举了。总之 Mongo 的高性能精髓还是很智慧的!
虽然 MongoDB 中 ObjectId 是轻量级的,但是如果全部在服务端生成肯定会花费一点开销,所以 Mongo 团队就想办法让 ObjectId 交给客户端去生成,让后服务端判断,如果插入的数据中已经有了 ObjcetId,我就采用,如果没有 ObjectId,服务端再生成。
Mongo 的高性能保障,ObjectId 的精妙设计只是其中之一。
: » Mongo高性能揭秘之ObjectId解密
原创文章,作者:Carrie001128,如若转载,请注明出处:https://blog.ytso.com/252282.html