Twitter的雪花算法(snowflake)自增ID
前言
这个问题源自于,我想找一个分布式下的ID生成器。
这个最简单的方案是,数据库自增ID。为啥不用咧?有这么几点原因,一是,会依赖于数据库的具体实现,比如,mysql有自增,oracle没有,得用序列,mongo似乎也没有他自己有个什么ID,sqlserver貌似有自增等等,有些不稳定因素,因为ID生成是业务的核心基础。当然,还有就是性能,自增ID是连续的,它就依赖于数据库自身的锁,所以数据库就有瓶颈。当然了,多台数据库加某种间隔也是可用的,但是,运维维护会很复杂,因为它不是内聚的解决方案。而且,很难提前获得下一个ID。
后来,我用过一段时间在数据库表里进行记录来进行自增。这个的优势是,我可以提前获得下一个ID,而且,某个进程里可以一次获取一批,减少锁的依赖,虽然进程间的不重复依然是基于数据库事务隔离的,但是,依赖小了,瓶颈小了。这个方案其实挺好的,我依然也会继续用,主要是,它可以生成数字字母混合的编剧号,而且基本可控。但是,我数据库主键为了效率和空间成本,基本会选用long,基本顺序生成就可以了,所以,使用这种带持久化的方案,会显得很重。起项目的时候,也是,需要先建立对应的表,然后再把代码或者jar包引进去,然后再用,比较重。最好就是能够直接生成,没有那么多依赖。
然后,我从我上司那里听到了twitter的这个算法。其实,我上司有个实现,我这个就是基于他的改的,但是,他的有两个值是配置的,我还是嫌麻烦,于是就动手把那两个值变成了从机器与进程获取,就有了这个版本。
思路
说实话,我也就听了这么个算法的名字,没正经看过原算法,但是,我上司说他代码是网上抄的,所以,这个算法名字我还是不敢丢,下面我们说说整体的思路。
整个ID的构成大概分为这么几个部分,时间戳差值,机器编码,进程编码,序列号。java的long是64位的从左向右依次介绍是:时间戳差值,在我们这里占了42位;机器编码5位;进程编码5位;序列号12位。所有的拼接用位运算拼接起来,于是就基本做到了每个进程中不会重复了。
代码
package nature.framework.core.common; import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration; /**
* 主键生成器
*
* @author nature
* @create 2017-12-22 10:58
*/
public class KeyWorker {
private final static long twepoch = 12888349746579L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L; // 毫秒内自增位数
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//sequence掩码,确保sequnce不会超出上限
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
//上次时间戳
private static long lastTimestamp = -1L;
//序列
private long sequence = 0L;
//服务器ID
private long workerId = 1L;
private static long workerMask= -1L ^ (-1L << workerIdBits);
//进程编码
private long processId = 1L;
private static long processMask=-1L ^ (-1L << datacenterIdBits);
private static KeyWorker keyWorker = null; static{
keyWorker=new KeyWorker();
}
public static synchronized long nextId(){
return keyWorker.getNextId();
} private KeyWorker() { //获取机器编码
this.workerId=this.getMachineNum();
//获取进程编码
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue(); //避免编码超出最大值
this.workerId=workerId & workerMask;
this.processId=processId & processMask;
} public synchronized long getNextId() {
//获取时间戳
long timestamp = timeGen();
//如果时间戳小于上次时间戳则报错
if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
}
//如果时间戳与上次时间戳相同
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
return nextId;
} /**
* 再次获取时间戳直到获取的时间戳与现有的不同
* @param lastTimestamp
* @return 下一个时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
} private long timeGen() {
return System.currentTimeMillis();
} /**
* 获取机器编码
* @return
*/
private long getMachineNum(){
long machinePiece;
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> e = null;
try {
e = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
e1.printStackTrace();
}
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
sb.append(ni.toString());
}
machinePiece = sb.toString().hashCode();
return machinePiece;
}
}
代码解读
整体设计
为了最大程度的减少配置,方便实用,这个模块,我设计成了单例模式。之所以没有直接使用static方法,还是希望可以控制整个模块的生命周期,但是,模块的初始化,我使用了static块,因为它没有任何依赖。
有个static的nextId方法,可以直接获得下一个ID,这个方法是线程安全的。同时这个模块的使用就是这么简单粗暴,也不用配置bean。
ID生成逻辑
我们先看最后一步:long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
这句话什么意思呢?
timestamp - twepoch:时间戳减去一个时间戳,获得一个差值。
((timestamp - twepoch) << timestampLeftShift):timestampLeftShift是22,这个操作是将这个差值向左移22位,左移空出来的会自动补0,我们就有了22位的空间了。
后面可以看到三个|符号,与操作会把1都加进来,而我们后面的数也都在各自的位上才有1,那么|操作就把这些数合进来了。
(processId << datacenterIdShift):进程编码左移datacenterIdShift,这个是17位,而processId最多是5位,于是刚好填满空位
(workerId << workerIdShift):与进程编码类似,机器编码也是5位,左移12位
sequence最大12位。
如何确保不超出位数限制
前面的逻辑中,我们说了很多不超出位数限制啥的内容,那么,具体是怎么做到的呢?我们拿workerId举个例子:
this.workerId=workerId & workerMask;
这是我们确保workerId不超过5位的语句,什么意思呢?不经常操作位运算真看不懂。我们先看看workerMask是啥。
private static long workerMask= -1L ^ (-1L << workerIdBits);
。。。什么意思呀?它先执行的是-1L << workerIdBits,workerIdBits是5。这又是什么意思呢?注意,这是位运算,long用的是补码,-1L,就是64个1,这里使用-1是为了格式化所有位数,<<是左移运算,-1L左移五位,低位补零,也就是左移空出来的会自动补0,于是就低位五位是0,其余是1。然后^这个符号,是异或,也是位运算,位上相同则为0,不通则为1,和-1做异或,则把所有的0和1颠倒了一下。这时候,我们再看,workerId & workerMask,与操作,两个位上都为1的才能唯一,否则为零,workerMask高位都是0,所以,不管workerId高位是什么,都是0,;而workerMask低位都是1,所以,不管workerId低位是什么,都会被保留,于是,我们就控制了workerId的范围。
最后的异常
这里,时间戳,保证了不通毫秒不同,然后机器编码进程编码保证了不同进程不通,再然后,序列,在统一毫秒内,如果获取第二个ID,则序列号+1,到下一毫秒后重置。至此,唯一性ok。但是,还有问题,序列号用完了怎么办?代码里的解决方案是,等到下一毫秒。
补充
其实,这个方案中,机器码和进程编码是可能相同的,只是概率比较小,我们就凑合着用吧。如果有更好地获取这两位的方式,欢迎沟通。
Twitter的雪花算法(snowflake)自增ID的更多相关文章
- 一个类似 Twitter 雪花算法 的 连续序号 ID 产生器 SeqIDGenerator
项目地址 : https://github.com/kelin-xycs/SeqIDGenerator 今天 QQ 群 里有网友问起产生唯一 ID 的方法 有哪些, 讨论了各种方法 . 有网 ...
- 雪花算法-snowflake
雪花算法-snowflake 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有 ...
- 基于雪花算法的增强版ID生成器
sequence 基于雪花算法的增强版ID生成器 解决了时间回拨的问题 无需手动指定workId, 微服务环境自适应 可配置化 快速开始 依赖引入 <dependency> <gro ...
- 【Java】分布式自增ID算法---雪花算法 (snowflake,Java版)
一般情况,实现全局唯一ID,有三种方案,分别是通过中间件方式.UUID.雪花算法. 方案一,通过中间件方式,可以是把数据库或者redis缓存作为媒介,从中间件获取ID.这种呢,优点是可以体现全局的递增 ...
- 一秒可生成500万ID的分布式自增ID算法—雪花算法 (Snowflake,Delphi 版)
概述 分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有些时候我们希望能使用一种 ...
- Twitter雪花算法 SnowFlake算法 的java实现
概述 SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序. 原理 Sn ...
- 分布式系统-主键唯一id,订单编号生成-雪花算法-SnowFlake
分布式系统下 我们每台设备(分布式系统-独立的应用空间-或者docker环境) * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作 ...
- 分布式唯一ID生成方案选型!详细解析雪花算法Snowflake
分布式唯一ID 使用RocketMQ时,需要使用到分布式唯一ID 消息可能会发生重复,所以要在消费端做幂等性,为了达到业务的幂等性,生产者必须要有一个唯一ID, 需要满足以下条件: 同一业务场景要全局 ...
- 雪花算法生成全局唯一ID
系统中某些场景少不了全局唯一ID的使用,来保证数据的唯一性.除了通过数据库自带的自增id来保证 id 的唯一性,通常为了保证的数据的可移植性会选择通过程序生成全局唯一 id.百度了不少php相关的生成 ...
随机推荐
- 第 8 章 容器网络 - 065 - 容器在 Weave 中如何通信和隔离?
首先在host2 执行如下命令: weave launch 10.12.31.22 这里必须指定 host1 的 IP 10.12.31.22,这样 host1 和 host2 才能加入到同一个 we ...
- [JavaScript] 设置函数同名变量为false会导致函数无法执行
var findEmail=false; function findEmail(){ alert("findEmail");} 这样函数不会运行. 为了保证函数可以运行,修改为: ...
- 《Practical Vim》第十章:复制和粘贴
第十章和第十一章讲了 Vim 的寄存器功能, 寄存器,是用于保存文本的特定的容器.它的内容: 既可以是类似于系统剪切板功能的,用于粘贴的文本 也可以是录制成的宏的命令. 第十章讲使用寄存器使用复制与粘 ...
- Lab 6-3
In this lab, we'll analyze the malware found in the file Lab06-03.exe. Questions and Short Answers C ...
- 解决安卓UI刷新卡屏,只显示最后一处刷新的问题
---恢复内容开始--- 错误1 安卓的机制决定了只有UI线程(也就是主线程)才能更新UI界面 否则会导致UI界面混乱的问题 这就说明了在new Thread中直接showImage是会报“出现非主线 ...
- mongoose手动生成ObjectId
用mongoose驱动保存数据,如果_id没有定义,那么在save的时候,mongoose驱动会自己生成一个_id.那么如果需要手动生成可以用mongoose.Types.ObjectId()方法. ...
- 洛谷P1030求先序排列
题目描述 给出一棵二叉树的中序与后序排列.求出它的先序排列.(约定树结点用不同的大写字母表示,长度≤8. 输入输出格式 输入格式: 2行,均为大写字母组成的字符串,表示一棵二叉树的中序与后序排列. 输 ...
- kafka分区及副本在broker的分配
kafka分区及副本在broker的分配 部分内容參考自:http://blog.csdn.net/lizhitao/article/details/41778193 以下以一个Kafka集群中4个B ...
- 【IDEA&&Eclipse】1、为何 IntelliJ IDEA 比 Eclipse 更适合于专业java开发者
圣战 有一些没有唯一正确答案的“永恒”的问题,例如哪个更好:是Windows还是Linux,Java还是C#:谁更强壮:Chuck Norris还是Van Damme. 其中的一个圣战便是Java I ...
- Linux:Ubuntu系统的安装
好久没更了,今天就更完这一期的Linux系统吧,这次主要安装的是常用Linux系统的之一:Ubuntu(乌班图)系统,这个系统和CentOS 7的安装步骤也是类似的,(我不采取用虚拟机的方法来安装,当 ...