最近在做分布式任务调度系统,遇到分布式id的问题,我们需要一个全局唯一的id,但是服务又部署在多台服务器上面。因为之前没有什么分布式系统的经验,想当然的就是用了全局分布式锁来保证id的唯一性。后来小组周会,经领导一点拨,突然想起之前看过的一些分布式id解决方案(所以说知识需要不断巩固实践以及复习,不然全忘光了- -)。其实网上的分布式id的文章也很多了,但是为了让自己理解更加深刻,决定专门写一篇博客来记录一下这些分布式id的解决方案。
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,在一个机器上生成的UUID,可以保证在同一个时空下都是唯一的。UUID的生成主要和以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字有关,可以保证绝对唯一,完全满足分布式id的唯一性原则。
String id = UUID.randomUUID().toString();
先给某个字段设置为自增长,然后每次插入一条记录并获取最新的id。
先创建一张表 test
create table test(
id int auto_increment,
name varchar(8),
PRIMARY KEY (`id`)
)
每次要获取id使用以下语句
begin;
REPLACE INTO test (NAME) VALUES ('a');
SELECT LAST_INSERT_ID();
commit;
第二种方案每次都要去数据库获取id,这就意味着频繁的IO操作造成了性能的瓶颈。所以就有人想出了优化方案,也就是提前把一个号码段从数据库中取出来放到内存中,之后拿到号码段的进程再慢慢消耗这些号码段,再去数据库中获取新的号码段,这样就大大的减少了IO次数。
create table test2(
id int auto_increment,
tag varchar(255) comment '用来表示业务',
max_id int comment '表示当前已取走的最大id',
step int comment '表示一次取多长的号段',
PRIMARY KEY (`id`)
);
之后用以下sql语句获取号段
Begin;
UPDATE test2 SET max_id=max_id+step WHERE tag='test';
SELECT tag,max_id,step from test2 WHERE tag='test';
Commit;
这样进程就拿到了step个号在进程中使用。
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
/**
* @author yangjb
* @since 2018-07-26 20:22
*
* snowflake分布式id生成器
* 最高位在long中表示符号位,id一般都是正数,所以第一位默认都用0表示
* 接下来的41位表示时间戳,注意,这里的时间戳不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序类的TWEPOCH属性)。41位的时间截,可以使用69年
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
* 12位序列号,在同一个毫秒同一个机器内,可以产生4096个ID序号使用
* 1+41+10+12 = 64,也就是一个long类型的长度。当然,不一定要严格使用这样的分配来计算id,比如比如机器较少,可以把机器的10位切割出几位出来给序列号使用等等
*/
public class IdGenerator {
//id生成器的开始使用时间
private final static long TWEPOCH = 1288834974657L;
// 机器标识位数
private final static long WORKER_ID_BITS = 5L;
// 数据中心标识位数
private final static long DATA_CENTER_ID_BITS = 5L;
// 机器ID最大值 31
private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
// 数据中心ID最大值 31
private final static long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
// 毫秒内自增位
private final static long SEQUENCE_BITS = 12L;
// 机器ID偏左移12位
private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
private final static long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
// 时间毫秒左移22位
private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
private final static long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
private long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long dataCenterId;
IdGenerator(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("%s must range from %d to %d", workerId, 0,
MAX_WORKER_ID));
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("%s must range from %d to %d", dataCenterId, 0,
MAX_DATA_CENTER_ID));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
synchronized long nextValue() {
long timestamp = time();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards, refuse to generate id for "
+ (lastTimestamp - timestamp) + " milliseconds");
}
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = untilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence;
}
private long untilNextMillis(final long lastTimestamp) {
long timestamp = this.time();
while (timestamp <= lastTimestamp) {
timestamp = this.time();
}
return timestamp;
}
private long time() {
return System.currentTimeMillis();
}
public static void main(String[] args) throws ParseException {
long id = new IdGenerator(1, 1).nextValue();
System.out.println(Long.toBinaryString(id));
}
}
使用snowflake最需要防止由于系统时钟回拨导致的id重复问题。上面的java实现方案中有做一些简单的时间戳检查来防止大幅度的系统时钟回拨,但是也没有彻底的解决时钟回拨问题。由于lastTimestamp变量是存储在内存的,一旦程序关闭,就失去了这个值,所以最好的方案还是需要定时的把lastTimestamp持久化起来。可以考虑利用zk的节点时间来记录这个lastTimestamp。
另外提一句,MongoDB的objectId也是用类snowflake的方案来生成id的。objectId由12个字节组成,4个字节表示秒级时间戳,3个字节表示机器id,2个字节表示pid,最后3个字节表示自增序列。也就是表示,在同一秒同一台机器的同一个进程内,可以产生4096个id。
objectId最终标识成一个24长度的十六进制字符。
使用redis的incr命令可以很简单的实现id的自增,并保证全局id唯一。
每次要获取id,只要执行以下语句
incr test
可以说除了第二种方案,其他各个分布式id解决方案都各自有各自的使用场景,也不能一概而论的说哪种方案是最好的。没有最好的方案,只有最适合的方案。当我们需要用到分布式id的时候,应该要先考虑好使用场景,之后分析各个分布式id解决方案各自的优缺点,综合考虑后再决定使用哪种方案。
美团的leaf解决方案:
https://tech.meituan.com/MT_Leaf.html?utm_source=tool.lu