1.背景:
公司的分布式应用部署了多个pod, 利用雪花算法来生成id, 然后用来保存数据, 但是生产上跑久了之后,偶尔间就会出现id碰撞的事情, 出现的概率非常小,但是一出现就会导致该笔业务处理失败。
关于雪花算法的64位的知识可以参考:
2.研究风险:
雪花算法10位标识符使用的是 dataCenterId 和 workerId
- public static long getDataCenterId(long maxDatacenterId) {
- long id = 1L;
- final byte[] mac = NetUtil.getLocalHardwareAddress();
- if (null != mac) {
- id = ((0x000000FF & (long) mac[mac.length - 2])
- | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
- id = id % (maxDatacenterId + 1);
- }
- return id;
- }
获取 dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L;另一种不为空,通过 Mac 地址获取 dataCenterId
可以得知,dataCenterId 的取值与 Mac 地址有关
接下来再看看 workerId
- public static long getWorkerId(long datacenterId, long maxWorkerId) {
- final StringBuilder mpid = new StringBuilder();
- mpid.append(datacenterId);
- try {
- mpid.append(RuntimeUtil.getPid());
- } catch (UtilException igonre) {
- //ignore
- }
- return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
- }
入参 maxWorkderId 也是一个固定值,代表工作机器 ID 最大值,默认值 31;datacenterId 取自上述的 getDatacenterId 方法
name 变量值为 PID@IP,所以 name 需要根据 @ 分割并获取下标 0,得到 PID
通过 MAC + PID 的 hashcode 获取16个低位,进行运算,最终得到 workerId.
标识位的获取依赖 Mac 地址和进程 PID,
经过研究发现部署在docker上的同应用的多个pod的pid一致, 如果dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L ,或者去mac地址与32取余后又存在与其他pod的算出来的datacenterId一致;
那么10位标志位就不是唯一的就有可能存在, 如果在碰上时间戳和自增序列号一直的情况,那么就有可能出现id冲突的情况。
3.解决方案:
常规的id冲突的解决方案: http://www.zzvips.com/article/191716.html
结合实际分析:
- 预分配标识符: 无法解决动态扩容问题,放弃
2 )标识位存放在 Redis,因公司redis保存key不可以是永久的, 那么就会出现key过期后,扩容的时候 拿到的标识位冲突的情况,割弃。
3) 通过将标识位存放在Zookeeper、MySQL 等中间件,得割弃掉现有得算法,得重新考虑生成的id和已经生成得id不能重复得问题,舍弃掉。
4.最终解决方案:
使用podip地址的后四位,不进行32取余运算,充当雪花算法中的10位机器码 参与运算
4.1)可行性分析
Ip地址得后四位是分为是0001-9999
最大得9999的二进制位数是14位 比原来得10机器多了四位, 即前4位二进制和42位时间戳的最后4位参与或运算。
影响:相差一毫秒换算成二进制之后的后11位是不一样得,就算最后四位和9999生成得二进制的前四位参与了与运算,也是可以确保同一个毫秒内是唯一的,故无影响
5.完整代码:
package com.hxp.testdemo;
import org.springframework.util.StringUtils;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.time.LocalDateTime;
/**
* 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左右。
*/
public class SnowflakeIdWorker2 {
// ==============================Fields===========================================
/**
* 开始时间截 (2015-01-01)
*/
private final long twepoch = 1288834974657L;
/**
* 机器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);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long dataCenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器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 SnowflakeIdWorker2 idWorker;
static {
idWorker = new SnowflakeIdWorker2(getDataCenterAndWorkerId());
}
//==============================Constructors=====================================
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
*/
public SnowflakeIdWorker2(long workerId) {
this.workerId = workerId;
}
// ==============================Methods==========================================
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
LocalDateTime now = LocalDateTime.now().plusMonths(6);
//return now.toInstant(ZoneOffset.of("+8")).toEpochMilli();
return System.currentTimeMillis();
}
private static long getDatecenterId() {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (32);
}
} catch (Exception e) {
e.printStackTrace();
}
return id;
}
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
System.out.println("sequence :" + sequence);
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
System.out.println("pod地址后四位:"+workerId);
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (workerId << workerIdShift)
| sequence;
}
//==============================Test=============================================
private static long getDataCenterAndWorkerId() {
String hostAddress = null;
try {
InetAddress address = InetAddress.getLocalHost();
if (null != address) {
hostAddress = address.getHostAddress();
if (StringUtils.isEmpty(hostAddress) || "127.0.0.1".equals(hostAddress)) {
hostAddress = null;
} else {
hostAddress=hostAddress.replace(".", "");
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
if (StringUtils.isEmpty(hostAddress)) {
String random = "" + Math.random();
hostAddress = random.substring(random.length() - 4);
}
return Long.parseLong(hostAddress.substring(hostAddress.length()-4));
}
/**
* 测试
*/
public static void main(String[] args) throws Exception {
long datecenterId = getDatecenterId();
SnowflakeIdWorker2 idWorker = new SnowflakeIdWorker2(getDataCenterAndWorkerId());
// 生成4位的验证码
String random = "" + Math.random();
String substring = random.substring(random.length() - 4);
System.out.println(getDataCenterAndWorkerId());
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(id);
System.out.println("二进制位:"+Long.toBinaryString(id));
System.out.println(id);
}
}}