分布式部署ID全局配置之雪花算法
前言
为什么需要分布式全局唯一ID 以及分布式ID的业务需求?
- 在复杂分布式系统中,往往需要对大量对数据和消息进行标识
- 如在美团、支付、餐饮 中 系统的数据日渐增长,对数据分库分表需要有一个唯一来标识一条数据或消息
- 此时一个能够生成全局唯一ID的系统是非常有必要的
ID生成规则部分硬性要求
- 全局唯一 :不能出现重复的ID,要 唯一标识
- 趋势递增 :在Mysql 的InnoDB引擎使用的是聚集索引,由于多数RDBMS 使用的是Btree数据结构来存储数据,在主键的选择上面我们应该尽量使用有序的主键保证数据写入
- 单调递增 :保证下一个ID一定大于上一个ID,例如事物版本号,增量消息
- 信息安全 :如果ID是连续的,恶意用户的扒取数据就非常容易来,直接按照顺序下载指定的URL,如果是订单号就更危险来,竞争对手可以知道我们一天的单量,所以在一些应用场景下,需要ID不规则
- 含时间戳 :这样就能够在开发中快速了解这个分布式id的生成时间
ID生成系统的可用性要求
- 高可用 :发一个获取分布式ID的请求,服务器就要保证99.99%的情况下给我创建一个唯一分布式ID
- 低延迟 :发一个获取分布式ID的请求,服务器就是要快,极速
- 高QPS :假如并发一口气10万个创建分布式ID请求同时杀过来,服务器要顶的住一下子成功创建10w个分布式ID
我们平时的方案
UUID 、 数据库自增主键 、基于Redis 生成全局ID策略
弊端
UUID 不能生成顺序,递增的数据,并且长,不是很推荐
数据库自增,集群多的情况下,扩容简直就是噩梦
Redis 使用Redis INCR 和 INCRBY 实现
snowflake(雪花算法)
Twitter的分布式自增ID算法:snowflake(雪花算法)
概述
最初 Twitter把存储系统从Mysql 迁移到 Cassandra (由Facebook 开发一套开源分布式Nosql系统) 因为Cassandra没有顺序ID生成机制,所以开发成了这样一套全局唯一 ID生成服务
Twitter 的分布式雪花算法SnowFlake , 经测试 snowflake 每秒能产出26 万个自增可排序的ID
- twitter的SnowFlake生成ID能够按照时间有序生成
- SnowFlake 算法生成id 的结果是一个64 bit 大小的整数,为一个Long 型(转换成字符后长度19位)
- 分布式系统不会产生ID碰撞(由datacenter 和 workerld 区分)并且效率较高
雪花算法原理讲解
- 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
- 41bit-时间戳,用来记录时间戳,毫秒级。
- 41位可以表示[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Oll7dpVz-1605695234754)(https://math.jianshu.com/math?formula=2%5E%7B41%7D-1)]个数字,
- 如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LCzpD94C-1605695234762)(https://math.jianshu.com/math?formula=2%5E%7B41%7D-1)],减1是因为可表示的数值范围是从0开始算的,而不是1。
- 也就是说41位可以表示[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xEQQjn42-1605695234764)(https://math.jianshu.com/math?formula=2%5E%7B41%7D-1)]个毫秒的值,转化成单位年则是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZUNl2pqO-1605695234768)(https://math.jianshu.com/math?formula=(2%5E%7B41%7D-1)]%20%2F%20(1000%20*%2060%20*%2060%20*%2024%20*365)%20%3D%2069)年 - 10bit-工作机器id,用来记录工作机器id。
- 可以部署在[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J7uNLXEq-1605695234771)(https://math.jianshu.com/math?formula=2%5E%7B10%7D%20%3D%201024)]个节点,包括5位datacenterId和5位workerId
- 5位(bit)可以表示的最大正整数是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-egRhWNu3-1605695234774)(https://math.jianshu.com/math?formula=2%5E%7B5%7D-1%20%3D%2031)],即可以用0、1、2、3、…31这32个数字,来表示不同的datecenterId或workerId - 12bit-序列号,序列号,用来记录同毫秒内产生的不同id。
- 12位(bit)可以表示的最大正整数是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0YyvYCu9-1605695234774)(D:%5Ccustersofeware%5Ctypora%5Cimage%5Cmath)],即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。
SnowFlake可以保证:
- 所有生成的id按时间趋势递增
- 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)
twitter的雪花算法:https://github.com/twitter-archive/snowflake
GitHub上java版的雪花算法:
https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java
使用建议
1、改进
其实雪花算法就是把id按位打散,然后再分成上面这几块,用位来表示状态,这其实就是一种思想。
所以咱们实际在用的时候,也不必非得按照上面这种分割,只需保证总位数在64位即可
如果你的业务不需要69年这么长,或者需要更长时间
用42位存储时间戳,(1L << 42) / (1000L * 60 * 60 * 24 * 365) = 139年
用41位存储时间戳,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
用40位存储时间戳,(1L << 40) / (1000L * 60 * 60 * 24 * 365) = 34年
用39位存储时间戳,(1L << 39) / (1000L * 60 * 60 * 24 * 365) = 17年
用38位存储时间戳,(1L << 38) / (1000L * 60 * 60 * 24 * 365) = 8年
用37位存储时间戳,(1L << 37) / (1000L * 60 * 60 * 24 * 365) = 4年
如果你的机器没有那么1024个这么多,或者比1024还多
用7位存储机器id,(1L << 7) = 128
用8位存储机器id,(1L << 8) = 256
用9位存储机器id,(1L << 9) = 512
用10位存储机器id,(1L << 10) = 1024
用11位存储机器id,(1L << 11) = 2048
用12位存储机器id,(1L << 12) = 4096
用13位存储机器id,(1L << 13) = 8192
如果你的业务,每个机器,每毫秒最多也不会4096个id要生成,或者比这个还多
用8位存储随机序列,(1L << 8) = 256
用9位存储随机序列,(1L << 9) = 512
用10位存储随机序列,(1L << 10) = 1024
用11位存储随机序列,(1L << 11) = 2048
用12位存储随机序列,(1L << 12) = 4096
用13位存储随机序列,(1L << 13) = 8192
用14位存储随机序列,(1L << 14) = 16384
用15位存储随机序列,(1L << 15) = 32768
注意,随机序列建议不要太大,一般业务,每毫秒要是能产生这么多id,建议在机器id上增加位
如果你的业务量很小,比如一般情况下每毫秒生成不到1个id,此时可以将随机序列设置成随机开始自增
比如从0到48随机开始自增,算是一种优化建议
如果你有多个业务,也可以拿出来几位来表示业务,比如用最后4位,支持16种业务的区分
如果你的业务特别复杂,可以考虑128位存储,不过这样的话,也可以考虑使用uuid了,但uuid无序,这个有序
如果你的业务很简单,甚至可以考虑32位存储,时间戳改成秒为单位…
2、总结:
合理的根据自己的实际情况去设计各个唯一条件的组合,雪花算法只是提供了一种相对合理的方式。
雪花算法这种用位来表示状态的,我们还可以用在其他方面,比如数据库存储,可以用更小的空间去表示不同的状态位
包括各种底层的比如序列化,也是有用到拆解位,充分利用存储
算法实现
经过学习,本人写了两种方式去获取全局ID。
方式1:使用工具类进行获取ID
package com.lyj.demo.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.concurrent.Executors.newFixedThreadPool;
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
/**
* @author
* @date 2020/11/18 10:14
* 雪花算法分布式唯一ID生成工具
*/
public class SnowflakeIdUtil {
private static final Logger logger = LoggerFactory.getLogger(SnowflakeIdUtil.class);
/** 开始时间戳 */
private final long startTimeStamp = 1605665795726L;
/** 机器ID所占位数 */
private final long workIdBits = 5L;
/** 数据标志Id所占位数 */
private final long dataCenterIdBits = 5L;
/** 支持的机器最大ID,结果是31,这里受机器设置的ID所占位数大小变化 */
private final long maxSupportWorkId = -1L ^ (-1L << workIdBits);
/** 支持的最大数据标识ID,结果是31,这里受数据标识ID所占位数大小变化 */
private final long maxSupportDataCenterId = -1L ^ (-1L << dataCenterIdBits);
/** 序列号ID所占位数 */
private final long sequenceBits = 12L;
/** 工作厂房ID向左移12位 */
private final long workIdLeftShift = sequenceBits;
/** 数据标识ID向左移17位(12+5) */
private final long dataCenterIdLeftShift