文章内容输出来源:拉勾教育Java高薪训练营
在分布式系统中,往往需要对大量的数据和消息进行唯一标识。单单靠数据库的自增ID显然不能满足需求,此时一个能够生成全局唯一ID的系统是非常必要的。所以对于分布式ID必须全局唯一性即不能出现重复的ID号,保证生成的 ID 全局唯一;另外对于生成全局唯一ID的系统必须保证高可用性。
分布式生成全局ID解决方案有:
(1)UUID
(2)SnowFlake雪花算法
(3)借助redis的Incr命令获取全局唯一ID
(4)利用MongoDB文档(Document)生成全局唯一ID
(5)独立数据库的自增ID
UUID为通用唯一识别码,算法的核心思想是结合机器的网卡、当地时间、一个随即数来生成UUID。可以通过java.util工具包获得:
public class TestUUID {
public static void main(String[] args) {
System.out.println(java.util.UUID.randomUUID().toString());
}
}
输出结果:3fb05807-2f30-40f2-80ff-5cef513c3e26
雪花算法生成的id为long型,由8个字节构成,即64bit,分别由符号位、时间戳、机器id、序列号组成。
雪花算法原理示意图如下:
说明:
(1)符号位:1位,固定为0,二进制表示最高位是符号位,0代表正数,1代表负数;
(2)时间戳:41位,即用41个二进制数来记录时间戳,表示某一个毫秒(毫秒级);
(3)机器id:10位,代表当前算法运行机器的id;
(4)序列号:12位,用来记录某个机器同一个毫秒内产生的不同序列号,代表同一个机器同一个毫秒可以产生的ID序号。
雪花算法源码如下:
/**
* Twitter_Snowflake
* SnowFlake的结构如下(每部分用-分开):
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截-开始时间截)
* 这里的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。
* 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 总共加起来刚好64位,为一个Long型。
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,
* 经测试,SnowFlake每秒能够产生26万个ID左右。
*/
public class SnowflakeIdWorker {
/** 开始时间截 (2020-09-07) */
private final long twepoch = 1599436800000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
public class TestSnowFlake {
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
测试输出结果:
110001011000011010110000010000000000000000000000
217182273273856
110001011000011010110000010000000000000000000001
217182273273857
110001011000011010110000010000000000000000000010
217182273273858
110001011000011010110000010000000000000000000011
217182273273859
110001011000011010110000010000000000000000000100
217182273273860
110001011000011010110000100000000000000000000000
217182277468160
110001011000011010110000100000000000000000000001
217182277468161
110001011000011010110000100000000000000000000010
217182277468162
110001011000011010110000100000000000000000000011
217182277468163
110001011000011010110000100000000000000000000100
217182277468164
Redis Incr命令是将key中存储的数值增1,如果key不存在,那么key的值初始化为0,然后执行incr命令操作。
java代码中利用jedis客户端调用redis的incr命名获得全局唯一id:
1.引入jedis客户端jar
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2.java代码实现
Jedis jedis = new Jedis("127.0.0.1",6379);
try {
long id = jedis.incr("id");
System.out.println("从redis中获取分布式id为:" + id);
} finally {
if (null != jedis) {
jedis.close();
}
}
MongoDB中的"_id" 类型是ObjectId 类型是一个12字节BSON类型数据,其生成方式如下:
|0|1|2|3|4|5|6 |7|8|9|10|11|
|时间戳 |机器ID|PID|计数器 |
时间戳:即前四个字节表示从标准纪元开始的时间戳,单位为秒,有如下特性:
1.时间戳与后边5个字节一块,保证秒级别的唯一性;
2.保证插入顺序大致按时间排序;
3.隐含了文档创建时间;
4.时间戳的实际值并不重要,不需要对服务器之间的时间进行同步(因为加上机器ID和进程ID已保证此值唯一,唯一性是ObjectId的最终诉求)。
机器ID:是服务器主机标识,通常是机器主机名的散列值。
PID:同一台机器上可以运行多个mongod实例,因此也需要加入进程标识符PID。
计数器:前9个字节保证了同一秒钟不同机器不同进程产生的ObjectId的唯一性。后三个字节是一个自动增加的计数器(一个mongod进程需要一个全局的计数器),保证同一秒的ObjectId是唯一的。同一秒钟最多允许每个进程拥有(256^3 = 16777216)个不同的ObjectId。
所以要生成全局唯一ID即生成ObjectId即可,实际应用中,"_id"既可以在服务器端生成也可以在客户端生成,在客户端生成可以降低服务器端的压力。
单独创建一张表,每次获取id时往表里插入一条数据,然后通mysql操作获取此条记录的id作为全局唯一id。
根据业务场景的不同,推荐使用UUID、SnowFlake雪花算法、利用redis的incr命令和MongoDB的文档_id获取全局唯一ID;
不推荐使用独立数据库的自增ID,因为此方法性能和可靠性都不好,需要在代码中连接数据库操作,如果存储此表的服务实例挂了,就无法获取分布式全局id了,就要考虑mysql集群了,这样复杂性大大提高了。
文章内容输出来源:拉勾教育Java高薪训练营
若有错误之处,欢迎留言指正~~~