1,原理
雪花算法(SnowFlake) 是主键生成策略中介于自增 ID 和 UUID 之间的一种数据库主键生成策略,生成的ID大致上是按照时间递增的
用在分布式系统中时需要注意数据中心标识和机器标识必须唯一,这样就能保证每个节点生成的 ID 都是唯一的,每秒能够产生 26 万
ID 左右,这些 ID 是唯一的且有大致的递增顺序,并且是一个 64 位整形,即 8 字节,可以展示为一个 Long 类型的整数
2,上代码
package com.hwq.web.server.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Service;
@Service
public class SnowFlakeService {
// 机器标识 和 数据标识
private long machineId = 1;
private long datacenterId = 1;
// 起始的时间戳,设置的尽量大,最好是项目创建的时间,确定之后禁止修改
private final static long START_STMP = 1480166465631L;
// 三个部分中每一部分占用的位数
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 5; // 机器标识占用的位数
private final static long DATACENTER_BIT = 5; // 数据中心占用的位数
// 每一部分最大值
//private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
//private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
// 每一部分向左的位移
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
// 每一毫秒中的序列号,和上一次执行时的时间戳
private long sequence = 0L;
private long lastStmp = -1L;
/**
* 生成主键的方法,请确保分布式不同节点中,方法两个参数的唯一性
*/
public synchronized String nextId() {
long currStmp = System.currentTimeMillis();
if (currStmp == lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L){
long mill = System.currentTimeMillis();
while (mill <= lastStmp) {
mill = System.currentTimeMillis();
}
currStmp = mill;
}
} else {
sequence = 0L;
}
lastStmp = currStmp;
currStmp = (currStmp - START_STMP) << TIMESTMP_LEFT; // 时间戳部分
currStmp = currStmp | (datacenterId << DATACENTER_LEFT); // 数据中心部分
currStmp = currStmp | (machineId << MACHINE_LEFT); // 机器标识部分
currStmp = currStmp | sequence; // 序列号部分
return String.valueOf(currStmp); // 转化为字符串
}
}
3,缺陷
上面的写法虽然能生成 唯一 id,但是在 分布式部署时,就要求我们需要配置不同的 机器标识 和 数据标识,要不然高并发的分布式系统还是会出现重复的情况,
但是每次部署项目的时候,不管是通过修改代码还是修改配置文件从而保证 机器标识 和 数据标识 的唯一性,都不是很友好,尤其是采用 docker 部署时,每次
还需要重新制作镜像,基于此,笔者这里打算通过项目部署时不然是唯一的 ip地址 和 端口 来生成唯一的 机器标识 和 数据标识,其中端口可以直接使用,ip
地址则需要一些算法进行转化,来确保不同的 ip 生成一个 不同的 唯一数字
4,修改后的代码
package com.hwq.web.server.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SnowFlakeService implements ApplicationRunner {
@Value("${spring.cloud.client.ip-address}")
private String ip;
@Value("${server.port}")
private Long port;
// 机器标识(用 ip 地址计算而来) 和 数据标识(直接使用端口)
private long machineId;
private long datacenterId;
// 起始的时间戳,设置的尽量大,最好是项目创建的时间,确定之后禁止修改
private final static long START_STMP = 1480166465631L;
// 三个部分中每一部分占用的位数
private final static long SEQUENCE_BIT = 12; // 序列号占用的位数
private final static long MACHINE_BIT = 5; // 机器标识占用的位数
private final static long DATACENTER_BIT = 5; // 数据中心占用的位数
// 每一部分最大值
//private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
//private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
// 每一部分向左的位移
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
// 每一毫秒中的序列号,和上一次执行时的时间戳
private long sequence = 0L;
private long lastStmp = -1L;
/**
* 生成主键的方法,请确保分布式不同节点中,方法两个参数的唯一性
*/
public synchronized String nextId() {
long currStmp = System.currentTimeMillis();
if (currStmp == lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L){
long mill = System.currentTimeMillis();
while (mill <= lastStmp) {
mill = System.currentTimeMillis();
}
currStmp = mill;
}
} else {
sequence = 0L;
}
lastStmp = currStmp;
currStmp = (currStmp - START_STMP) << TIMESTMP_LEFT; // 时间戳部分
currStmp = currStmp | (datacenterId << DATACENTER_LEFT); // 数据中心部分
currStmp = currStmp | (machineId << MACHINE_LEFT); // 机器标识部分
currStmp = currStmp | sequence; // 序列号部分
return String.valueOf(currStmp); // 转化为字符串
}
/**
* 当项目启动时,从 redis 获取数据中心标识 和 机器标识
*/
@Override
public void run(ApplicationArguments args) {
machineId = ipToNum(ip);
datacenterId = port;
log.info("获得雪花算法的机器码:machineId = " + machineId + "、数据码:datacenterId = " + datacenterId);
}
/**
* 将 ipv4 的地址转化为 唯一数字
* @param ip 地址
*/
private long ipToNum(String ip) {
String[] ipArr = ip.split("\.");
long ipNum = Long.parseLong(ipArr[3]) & 0xFF;
ipNum |= ((Long.parseLong(ipArr[2]) << 8) & 0xFF00);
ipNum |= ((Long.parseLong(ipArr[1]) << 16) & 0xFF0000);
ipNum |= ((Long.parseLong(ipArr[0]) << 24) & 0xFF000000);
return ipNum;
}
}