1.为什么需要分布式全局唯一ID
分布式系统下唯一ID生成。
2.ID生成规则部分硬性要求
- 全局唯一
不能出现重复ID号 - 趋势递增
在MySQL的INODB引擎中使用的是聚簇索引,由于多数RDBMS使用的Btree的数据结构来存储索引数据,在主键的选择上应尽量使用有序的主键保证写入性能 - 单调递增
尽量保证下一个的ID尽量大于下一个,例如事务版本号、IM增量消息、排序等特殊需求 - 信息安全
如果ID是连续的,恶意用户爬取数据比较方便。所以在一些场景下需要ID无规则,让竞争对手不好猜 - 含时间戳
最好包含一个时间戳,方便获知ID生成时间
3.ID号生成系统的可用性要求
- 高可用
服务器保证99.999%的情况下创建一个唯一的分布式ID - 低延迟
服务要快 - 高QPS
服务器要顶住且一下创建10万个分布式ID
4.分布式ID通用生成方案
(1).UUID
如果只考虑唯一性是OK的,但是由于无序导致入库性能差。
注意:
1.分布式Id一般用来作为主键,但是MySQL官网推荐主键尽量越短越好,36位的UUID不太合适
2.分布式Id作为主键,主键是包含索引的,MySQL通过B+树来生成索引,每次新的UUID数据插入,为了查询优化,都会对索引底层的B+数进行修改,因为UUID是无序的,所以每次UUID数据的插入都会对B+树进行很大的修改,这一点很不友好,插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和节点,这样大大降低了数据库插入性能。
结论:只能保证唯一性
(2).数据库自增主键
在分布式里,数据库自增ID的机制主要原理是:数据库自增ID和MySQL数据库的replace into实现。
replace into 首先尝试插入数据,如果发现有此行数据(根据主键或唯一索引判断)则先删除再插入。
注意:
1.系统水平扩展比较困难,通过设置初始值和步长勉强可以满足
2.数据库压力大,每次获取ID都读写一遍数据库,非常影响性能
结论:唯一性、自增满足,但是使用mysql在高QPS情况下性能较差
(3).基于Redis生成全局id策略
因为Redis是单线程的天生保证原子性,可以使用原子操作INCR和INCRBY来实现
注意:
1.Redis集群情况下需要设置步长,同时key一定要设置有效期
假设一个集群5台Redis服务器,可以初始化Redis值为1,2,3,4,5,然后步长是5,各RedisID生成:
A:1,6,11
B:2,7,12
C:3,8,13
D:4,9,14
E:5,10,15
2.横向扩展差,Redis集群维护复杂
5.雪花算法snowflake(推荐使用)
(1).概设
Twitter的分布式自增主键Id算法snowflake:
1.SnowFlake生成的Id能够按照时间顺序生成
2.SnowFlake算法生成id的结果是一个64bit大小的整数,为一个Long型(转换成String后长度最多19位)
3.分布式系统不会产生Id碰撞(有datacenter-数据中心和workid-机器码做区分),并且效率较高
(2).结构
号段解析:
- 1bit
生成的id一般都是用整数,所以最高位固定为0 - 41bit。时间戳,用来记录时间戳,毫秒级
41位可以表示2^{41}-1个数字
如果用来表示整数(机器中正数包括0)可以表示的数值范围是0-2^{41}-1,减1因为表示的数值范围从0开始,而不是1
41位可表示 2^{41}-1 个毫秒值,转换成单位年则是(2^{41}-1)/(1000606024365)=69年,从1970年开始可用到2039-09-07 - 10bit。工作机器id,用来记录工作机器id
可以部署在2^{10}=1024个节点上,包括5位datacenterid和5位workid
5位(bit)可以表示的最大整数是2^5-1=31个,即可以用0,1,2…31这32个数字来表示不同的datacenterid和workid - 12bit。序列号,用来记录同毫秒内生成的不同id。
12位(bit)可以表示的最大正整数是2^{12}-1=4095,即可以用0,1,2…4094这4095个数字来表示同一机器同一时间戳(毫秒)内产生的4095个id序号
(3).源码
https://github.com/twitter-archive/snowflake
(4).工作落地
1.可使用糊涂工具包:https://github.com/looly/hutool
2.自己封装工具类
public class IdWorker {
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
private static IdWorker idWorker;
static {
idWorker = new IdWorker();
}
/**
* 每次调用产生一个新的ID并返回
*
* @return 最新的ID
*/
public static synchronized long getNextId() {
return idWorker.nextId();
}
private IdWorker() {
this.workerId = getWorkerId();
this.datacenterId = getDatacenterId();
//支持的最大机器id,31
long maxWorkerId = -1L ^ (-1L << workerIdBits);
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
//支持的最大数据标识id,31
long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
}
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
private 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));
}
//序列在id中占的位数
long sequenceBits = 12L;
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
//生成序列的掩码,4095 (0b111111111111=0xfff=4095)
long sequenceMask = -1L ^ (-1L << sequenceBits);
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//开始时间截 (2018-01-01 00:00:00)
long twepoch = 1514736000000L;
//数据标识id向左移17位(12+5)
long datacenterIdShift = sequenceBits + workerIdBits;
// 时间截向左移22位(5+5+12)
long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
return ((timestamp - twepoch) << timestampLeftShift)
//移位并通过或运算拼到一起组成64位的ID
| (datacenterId << datacenterIdShift)
| (workerId << sequenceBits)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
/**
* 获取机器编码
*
* @return workerId
*/
private long getWorkerId() {
long machineId;
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> e = null;
try {
e = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
e1.printStackTrace();
}
while (e != null && e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
Enumeration<InetAddress> addrs;
addrs = ni.getInetAddresses();
if (addrs == null) {
continue;
}
// 获取IP地址(获取不到IP地址时使用网卡名)
String ipStr = "";
InetAddress ip;
while (addrs.hasMoreElements()) {
ip = addrs.nextElement();
if (!ip.isLoopbackAddress() && ip.isSiteLocalAddress() && ip.getHostAddress().indexOf(":") == -1) {
try {
ipStr = ip.toString();
} catch (ArrayIndexOutOfBoundsException aioe) {
ipStr = ni.toString();
}
}
}
sb.append(ipStr);
}
machineId = sb.toString().hashCode();
//工作机器ID掩码
long workerIdMask = -1L ^ (-1L << workerIdBits);
return machineId & workerIdMask;
}
/**
* 获取数据中心Id
*
* @return datacenterId
*/
private long getDatacenterId() {
//获取进程编码
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
long datacenterId = Long.valueOf(runtimeMXBean.getName().split("@")[0]);
//数据中心ID掩码
long datacenterIdMask = -1L ^ (-1L << datacenterIdBits);
return datacenterId & datacenterIdMask;
}
public static String next() {
return String.valueOf(getNextId());
}
/**
* get uuid
*
* @return return id like 'c79454d6-9e0c-4b4a-a2bc-62f559f71570'
* @since v0.2.22
*/
public static String uuid() {
return UUID.randomUUID().toString();
}
/**
* get uuid
*
* @return return id like 'c79454d69e0c4b4aa2bc62f559f71570'
* @since v0.2.22
*/
public static String simpleUUID() {
return uuid().replaceAll("-", "");
}
}
(5).缺点
依赖时钟,如果时钟回拨,会导致生成重复的id
在单机上是递增的,但是到分布式环境,每台机器时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以认为无所谓的,一般分布式id只要求趋势递增,并不会严格要求递增)