全局 & 局部
全局是个什么概念?
我的理解:全局是个相对的概念,相对的全局。
为什么这么说?
全世界所有计算机在一个系统里,这是不是全局的概念?
公司所有计算机在一个系统里,这又是一个全局的概念。
公司中的机器分出了10台给具体某一个独立完整的程序使用,这十台计算机也是个全局的概念。
有全局,那么自然就有局部的概念:我的理解是,每一台计算机都是局部概念的展现。
全局唯一id的设计方案
全局唯一id是指:在某个全局中的所有计算机产生的所有序列是独一无二的,不会产生重复的。
跟着这个概念来设计全局唯一id,那么必然是可行的。
方案一
生成uuid做全局唯一id。uuid的生成原理是跟每台计算机的mac地址相关的,任何两台电脑的网卡地址都是不一样的。 所以这个uuid可以作为全局唯一id的生成方案。
缺点就是uuid没有任何规律可言,同一个节点生成的uuid都是找不到任何规律的,而且是字符串类型。
方案二
单独拿一台计算机出来写一个接口,这个接口的内容很简单返回一个序列号。
伪代码:
private static long seq = 0L;
public static long getSeq(){
return seq++;
}
然后全局的所有计算机都访问这个节点的这个接口,从而获取唯一的id。
当然这样设计在高并发的情况下会出现问题,因为seq++并不是原子操作,通过使用long对应的原子类,或者简单粗暴的给getSeq()方法加上锁即可。
除了并发安全问题,还有单点故障,以及序列没有持久化的问题。
单机故障:部署集群解决。
序列持久化问题:将序列入库 或者 自己通过io流写到物理磁盘上。
为什么序列要持久化?如果这个生成序列的节点重启,那么序列又得从0开始,那么序列就重复了,就不是唯一的了。
方案三
用数据库实现全局唯一id,利用数据库的主键自增,全局所有计算机要生成唯一id都让这台数据库操作。
还是会存在一个问题:单点故障。数据库没有持久化问题,数据库的表文件本来就是持久化在磁盘上的。
解决方案:部署主从数据库解决单点故障。
方案四
利用redis的 incr 这个原子自增命令 来实现。
存在问题:单点故障、持久化。
解决方案:主从redis 解决单点故障;用aof、rdb策略进行持久化。
方案五
主角登场:雪花算法(snow flake)
雪花算法
这种实现方案是最先推出来的是推特。
主要思想:一个long类型的整型占8个字节,一个字节有8个比特位,那么一个long类型的整型总共占有64个比特位。
在java中不存在什么无符号整型,底层二进制的最高位是符号位,所以实际上一个long类型存储数据的部分只有63位。
还剩下63位,这63位得表示四部分信息:
1、拿出41位表示时间戳。41位时间戳可以表示69年。
这里是毫秒级别的时间戳。
计算代码:
public static void main(String[] args) {
long a = 1L;
System.out.println((a << 41) / 1000 / 60 / 60 / 24 / 365);
}
2、拿出5位表示机房号。5位可表示的最大机房数是 (1 << 5)= 32。
3、拿出5位表示计算机节点编号。5位可表示的最大节点编号是 ( 1 << 5) = 32。
4、拿出12位表示序列号。12位序列号可表示的最大编号是 (1 << 12)== 4096。 到此:41 + 5 + 5 + 12 = 63,一位都没浪费。
说下1-4的整体语意:在某个毫秒中,xx号机房中的xx号台计算机,生成了 xx 序列。(同一毫秒最多生成4096个序列)
到这里雪花算法的主要思想已经说完了。下面来贴一段我用java实现的雪花算法,有详细的注释说明。
前置知识:
1、完整的理解雪花算法那63位分别表示什么含义。
2、理解 位运算: & 、 |、<<。
public class SnowFlake {
// 用5位表示机房号
public static final int ROOM_LEN = 5;
// 用5位表示电脑号
public static final int COMPUTER_LEN = 5;
// 用12位表示序列号
public static final int SEQ_LEN = 12;
// 序列最大表示值
public static final int SEQ_MAX_VALUE = (1 << SEQ_LEN) - 1;
// 电脑号最大表示值
public static final int COMPUTER_MAX_VALUE = (1 << COMPUTER_LEN) - 1;
// 机房最大表示值
public static final int ROOM_MAX_VALUE = (1 << ROOM_LEN) - 1;
// 毫秒级别41位表示需要往左移动几位
public static final int MILLIS_LEFT_LEN = SEQ_LEN + COMPUTER_LEN + ROOM_LEN;
// 机房号需要往左移动几位
public static final int ROOM_LEFT_LEN = SEQ_LEN + COMPUTER_LEN;
// 电脑号需要往左移动几位
public static final int COMPUTER_LEFT_LEN = SEQ_LEN;
// 当前的毫秒
private long millis = 0L;
// 每一毫秒都从序列0开始
private int seq = 0;
// 房间号
private long roomId;
// 电脑号
private long computerId;
// 上一秒的时间表示
private long lastMillis = 0;
// 雪花算法的构造函数
public SnowFlake(long roomId,long computerId){
if (roomId > ROOM_MAX_VALUE || roomId < 0){
throw new IllegalArgumentException("机房号不合法");
}
if (computerId > COMPUTER_MAX_VALUE || computerId < 0){
throw new IllegalArgumentException("机器号不合法");
}
this.roomId = roomId;
this.computerId = computerId;
}
// 得到下一毫秒
private long getNextMillis(){
long millis = System.currentTimeMillis();
// 注意这里要强制性获取到下一毫秒
while (millis <= lastMillis){
millis = System.currentTimeMillis();
}
return millis;
}
// 对外调用的方案,核心实现
public long getNextSeqId(){
// 获取当前毫秒级时间戳
long curMillis = System.currentTimeMillis();
// 如果当前进来的时间戳大于之前保留的时间戳,那么在逻辑区里重置序列号为0
if (curMillis > lastMillis){
seq = 0;
millis = curMillis;
}
// 如果当前进来的时间戳等于之前保留的时间戳,那么在逻辑区里是对序列号的操作
else {
// 序列号 + 1
seq = seq + 1;
// 同一毫秒已经生成了4096个序列了
// 强制让其等待下一个毫秒再生成序列
if ((seq & SEQ_MAX_VALUE) == 0){
millis = getNextMillis();
// 下一个毫秒级别的时间戳了,那么序列号重置为0,重新开始
seq = 0;
}
}
lastMillis = millis;
return seq | (computerId << COMPUTER_LEFT_LEN)
| (roomId << ROOM_LEFT_LEN)
| (millis << MILLIS_LEFT_LEN);
}
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(4, 3);
for (int i = 0; i < 16; i++) {
System.out.println(snowFlake.getNextSeqId());
}
}
}
部分运行结果:
这里着重强调一个点,上面代码第79行的else里的逻辑块
// 序列号 + 1
seq = seq + 1;
// 同一毫秒已经生成了4096个序列了
// 强制让其等待下一个毫秒再生成序列
if ((seq & SEQ_MAX_VALUE) == 0){
millis = getNextMillis();
// 下一个毫秒级别的时间戳了,那么序列号重置为0,重新开始
seq = 0;
}
走进else里说明是在同一毫秒级别还要产生序列。
seq & seq_max_value == 0 这个判断为什么表示这一毫秒中已经产生4096个序列了?
(1) seq_max_value 是 4095,和刚刚说的4096为啥相差一个,因为我们是从seq = 0开始表示的。
(2) 4095 的16位二进制表示是:0000 1111 1111 1111
(3) 在最大的基础上加上一就是4096,那4096:0001 0000 0000 0000
(4) 所以 4095 & 4096 表示为如下:
0000 1111 1111 1111
0001 0000 0000 0000
& 结果:
0000 0000 0000 0000
(5) 所以得强制等下一毫秒,让序列重置为0开始计算。
小细节优化:让当前时间戳减去一个固定时间戳,这样可以让41位时间戳多表示几年。
分布式id
分布式id,全局没有提分布式id,在文章的最后我们来提一下。
在分布式的架构中,分库、分表的情况下,我们要生成表的id,就可以用全局唯一id的思想,简单粗暴点就是直接用uuid;要是想稍微体现出一点技术含量的就采用雪花算法来生成分布式id。
全局唯一id和分布式id是啥关系?非等价关系。
个人觉得全局唯一id是思想层面上的东西;而分布式id是业务层面的东西;分布式id可以用全局唯一id来实现。
总结
1、介绍了全局唯一id的概念。
2、介绍了全局唯一id的实现思路。
3、重点介绍了全局唯一id的雪花算法思想 & 实现代码,对核心代码进行了讲解。
4、介绍了分布式id和全局唯一id的关系。