前言
首先说下我们为什么需要分布式 ID,以及分布式 ID 是用来解决什么问题的。当我们的项目还处于单体架构的时候,我们使用数据库的自增 ID 就可以解决很多数据标识问题。但是随着我们的业务发展我们的架构就会逐渐演变成分布式架构,那么这个时候再使用数据的自增 ID 就不行了,因为一个业务的数据可能会放在好几个数据库里面,此时我们就需要一个分布式 ID 用来标识一条数据,因此我们需要一个分布式 ID 的生成服务。那么分布式 ID 的服务有什么要求和挑战呢?
在当下的互联网当中,根据业务场景以及需求的不同,对于分布式 ID 的实现有如下几种实现方式:
写 Java 的朋友对 UUID 肯定不陌生,7dbb9f04-d15e-4c88-b74b-72a35e0d7580
这是一个标准的 UUID,虽然都说 UUID 是全球唯一,具备我们前面提到的要求中的第一点,但是很显然不具备全局递增,这种分布式 ID 可读性很差,如果说只是用来记录日志或者不需要人去理解的场景是可以用,但是不适合我们这里说的业务数据的唯一标识。而且这种无序的 UUID 如果作为主键会很严重影响性能。
Redis 有个 incr 的命令,这个命令是能保证原子递增的,在某种程度上也是可以生成全局 ID,不过使用 Redis 有两个问题:
前面我们提到单个数据库在分布式环境下已经没办法使用自增 ID 了,因为每个 MySQL 的实例自增 ID 都是从 1 开始,并且步长都按照 1依次递增,这种情况下我们很容易想到是不是可以考虑给每个数据库设置不同的步长。如果我们设置了不同的步长,这样就可以保证每个数据库实例都可以生成 ID,并且不会重复。虽然简单的系统可以这样用,但是也有几个问题:
snowflake
算法是推特开源的分布式 ID 生成算法,这个算法提供了一个标准的思路,很多公司都参考这个算法做了自己的实现,比较有名的是美团的 Leaf
。这里我们就着重看下雪花算法是怎么实现的。
感兴趣的可以去参考文章 https://tech.meituan.com/2017/04/21/mt-leaf.html 看下美团的 leaf 的实现原理。
雪花算法的思想是化整为零,将分布式 ID 的生成分散到每个机房和机器上,采用一个 64 位 long 类型的的结构来表示一个 ID,64 的结构如下所示,第一位符号位 0,然后是 41 位的时间戳,接下来的 10 位是机房加机器,最后的 12 位是序列号。
上面这个结构是雪花算法的基本结构,不同公司根据自身的业务会进行相应的调整,有的可以采用 32 位或者其他位数,而且时间戳的位数也可以根据实际情况进行调整,10 位的 workerID 有机房的公司可以用机房加机器组成,没有机房的公司可以直接用机器来组成,序列位也可以根据情况适当调整。
我们可以简单算一下,41 位的时间位是2 ^ 41 / (365 * 24 * 3600 * 1000) = 69 年,每个机器每毫秒可以生成 2 ^ 12 = 4096 个 ID。
那是不是说我们这个代码只能运行 69 年呢?其实不是的,这里服务在启动的时候会设置一个初始值,这里的时间戳是用机器的时间减去初始值的差值。那 SnowFlake 算法有什么优缺点呢?
结合上面的原理,我们可以通过 Java 代码来具体实现,代码如下:
public class SnowFlakeUtil {
//初始时间戳
private final static long START_TIMESTAMP = 1624796691000L;
//数据中心占用的位数
private final static long DATA_CENTER_BIT = 5;
//机器标识占用的位数
private final static long MACHINE_BIT = 5;
//序列号占用的位数
private final static long SEQUENCE_BIT = 12;
/**
* 每一部分的最大值
*/
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
private final static long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private final long idc;
private final long serverId;
private long sequence = 0L;
private long lastTimeStamp = -1L;
private long getNextMill() {
long mill = System.currentTimeMillis();
while (mill <= lastTimeStamp) {
mill = System.currentTimeMillis();
}
return mill;
}
/**
* 根据指定的数据中心ID和机器标志ID生成指定的序列号
*
* @param idc 数据中心ID
* @param serverId 机器标志ID
*/
public SnowFlakeUtil(long idc, long serverId) {
if (idc > MAX_DATA_CENTER_NUM || idc < 0) {
throw new IllegalArgumentException("IDC 数据中心编号非法!");
}
if (serverId > MAX_MACHINE_NUM || serverId < 0) {
throw new IllegalArgumentException("serverId 机器编号非法!");
}
this.idc = idc;
this.serverId = serverId;
}
/**
* 生成下一个 ID
*
* @return
*/
public synchronized long genNextId() {
long currTimeStamp = System.currentTimeMillis();
if (currTimeStamp < lastTimeStamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currTimeStamp == lastTimeStamp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastTimeStamp = currTimeStamp;
return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | idc << DATA_CENTER_LEFT | serverId << MACHINE_LEFT | sequence;
}
public static void main(String[] args) {
SnowFlakeUtil snowFlake = new SnowFlakeUtil(4, 3);
for (int i = 0; i < 100; i++) {
System.out.println(snowFlake.genNextId());
}
}
}