基于SnowFlake算法如何让分库分表中不同的ID落在同一个库的算法的实现
SnowFlake算法
SnowFlake算法是由Twitter锁分享出来的一种生成不重复的分布式ID的一种算法,在复杂的分布式系统中,我们通常需要使用分库分表来实现我们的系统,在分库分表的过程中将会涉及到一个ID重复的问题,数据库的自增ID很明显不会满足要求,此时拥有一个可以生成全局唯一的ID的算法是非常有必要的的。
1、生成唯一ID具备的条件
-
全局唯一性:在数据库分库分表以后,某张表的流水ID必须是唯一的。
-
趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
-
单调递增:保证下一个ID一定大于上一个ID,如果下个ID有可能小于上一个ID那就有可能导致ID的全局不唯一性。
-
信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可。
2、生成全局唯一ID的三种方式
2.1、UUID
uuid可以保证我们的全局ID的唯一,如果是在单体的系统使用UUID和递增ID实际上对性能影响不大,因为毕竟单表的数据量要超过百万在传统的单体系统中基本上是很难的,但是在互联网企业中,单表超过百万可能就几个小时的事情,这时候使用UUID在插入数据的时候会比递增ID效率上差很多。
优点
- 性能非常高:本地生成,没有网络消耗
缺点
- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病xxxx者位置。
- ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。(MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。)
2.2、基于分布式锁的ID
基于分布式锁的方式的ID,就是采用分布式锁(redis可实现,java采用关键字Sychronization也可以实现)的机制来实现,生成某张表ID的时候,采用分布式锁直接加锁,生成流水ID以后直接解锁即可,然后业务上可以直接使用当前ID来生成想要的数据,这时候生成的ID是唯一的。
优点
- 性能很好:基于redis的性能可以很快的生成的自己想要的流水ID。
缺点
- 增加成本:需要专门的运维人员去维护我们的redis数据库。
- 网络消耗:在与redis交互的过程中会增加网络的消耗。
- 复杂度:提高了开发的复杂度,首先开发人员需要引入redis的库,同时要理解分布式锁,这样才可以正确的使用分布式锁。
2.3、SnowFlake
SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序。
2.3.1、SnowFlake算法原理
-
因为最高位是标识位,为1表示为负数,所以最高位不使用。
-
41bit 保存时间戳,精确到毫秒。也就是说最大可使用的年限是69年。
-
10bit 的机器位,能部属在1024台机器节点来生成ID。
-
12bit 的***,一毫秒最大生成唯一ID的数量为4096个。
2.3.2、41位时间戳
这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年,例如我们可以设置我们当前的开始时间为2020-12-01这天开始,那么我们的***就可以使用到2079-12-01这一天,这已经完全满足我们的需求了。
2.3.3、10位机器位
可以同时部署到1024台机器中,在微服务中我们可以为每一个实例分配一个唯一ID。
2.3.4、12位***
可以在一个毫秒内生成4096个ID,4096个ID完全已经满足百分99以上的场景,若有需要可以自己再去扩展。
2.4、雪花算法变种实现
在我们的数据库中存在分库和分表的业务场景,例如订单表,上面存着订单ID和用户ID,这时候我们是根据订单ID来进行分库分表,那我们希望我们的用户ID可以和订单ID落在同一个库上,那么这时候我们该如何生成我们的雪花ID呢?
需要保证我们的用户ID和订单ID同时落在一个库上,那么就是在生成订单ID的时候,订单ID取余的值必须要等于用户ID取余的值,这样才会使得用户ID和订单ID落在同一个库上,那么需要订单ID取余的值落在同一个库上,那么我们可以改造我们原先的12位的***,将其分为7位的***和5位的取余值,只要保证我们5位的取余值就是当前用户ID取余的值,就可以保证我们生成的订单ID和用户ID会落在通一个库上。
2.4.1、java代码实现
/**
* @author linzef
* @since 2020-11-29
* 类描述: 生成雪花ID的工具类
*/
public class SnowflakeIdWorker {
/**
* 构造函数
*
* @param modCoefficient 当前取余的系数,例如你设置为32,则生成的id % 32 则会等于你当前设置的modVal
* @param modVal 当前生成的需要取余的值,假设你希望生成的雪花ID需要取余为8,则设置为8,这个值必须要小于31,且要小于modCoefficient的值
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(int modCoefficient, int modVal, long twepoch, long workerId, long dataCenterId) {
this.modCoefficient = modCoefficient;
this.modVal = modVal;
this.twepoch = twepoch;
if (modVal > modCoefficient || modCoefficient < 0) {
throw new IllegalArgumentException(String.format("modVal的值不能大于modCoefficient取余的系数的值,或者modCoefficient的值不能小于0", modCoefficient));
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId不能大于" + maxWorkerId + "的值或者小于0", maxWorkerId));
}
if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("dataCenterId不能大于" + maxDataCenterId + "的值或者小于0", maxDataCenterId));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
/**
* 当前取余的系数,例如你设置为32,则生成的id % 32 则会等于你当前设置的modVal
*/
private final int modCoefficient;
/**
* 当前生成的需要取余的值,假设你希望生成的雪花ID需要取余为8,则设置为8,这个值必须要小于31,且要小于modCoefficient的值
*/
private final int modVal;
/**
* 开始时间截
*/
private final long twepoch;
/**
* 机器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);
/**
* 取余的值所占用的位数
*/
private final long modBits = 5L;
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 7L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = 12L;
/**
* 数据标识id向左移17位(7+5+5)
*/
private final long dataCenterIdShift = sequenceBits + workerIdBits + modBits;
/**
* 时间截向左移22位(7+5+5+5)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits + modBits;
/**
* 生成序列的掩码,这里为128(2^7)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long dataCenterId;
/**
* 毫秒内序列(0~128)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("时钟异常,请重试生成雪花ID", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1 * modCoefficient) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (dataCenterId << dataCenterIdShift)
| (workerId << workerIdShift)
| sequence | modVal;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @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 static void main(String[] args) {
/**
* 取余的系数设置为16,取余的值设置为13,这样你会发现生成的雪花ID的值被16取余以后为13
*/
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(16, 13, 28778794000L, 0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println((Long.toBinaryString(id)));
System.out.println(id);
System.out.println(id % 16);
}
}
}