对于分布式id,有很多方案,现在大多数用的是基于雪花算法Snowflake的实现,美团有Leaf,百度有Uidgenerator,我这里记录下苞米豆在MybatisPlus3中的分布式id实现
简单介绍下雪花算法
雪花算法也叫雪花id,是一个64bit的整型数据,原生的Snowflake是这样的:
最高位不用,41bit保存时间戳,单位是毫秒,10bit的机器位,12bit的唯一序列号,可以理解是某一毫秒内,某台机器生成了不重复的序列号
10bit 一般一会分为5bit的datacenterId存储位和5bit的workerId存储位
从mybatisplus3.X开始,苞米豆已经把雪花算法的java实现放在了mybatisplus中,并且提供了默认实现类
public interface IdentifierGenerator {
/**
* 生成Id
*
* @param entity 实体
* @return id
*/
Number nextId(Object entity);
/**
* 生成uuid
*
* @param entity 实体
* @return uuid
*/
default String nextUUID(Object entity) {
return IdWorker.get32UUID();
}
}
这个接口就是mybatisplus的id生成接口
public class DefaultIdentifierGenerator implements IdentifierGenerator {
private final Sequence sequence;
//无参数构造
public DefaultIdentifierGenerator() {
this.sequence = new Sequence();
}
//workerId和dataCenterId
public DefaultIdentifierGenerator(long workerId, long dataCenterId) {
this.sequence = new Sequence(workerId, dataCenterId);
}
public DefaultIdentifierGenerator(Sequence sequence) {
this.sequence = sequence;
}
@Override
public Long nextId(Object entity) {
return sequence.nextId();
}
}
DefaultIdentifierGenerator是默认实现类,当然我们也可以自己实现IdentifierGenerator自定义生成id,这里有两个构造参数,一个是数据中心id一个是workerId用来区分服务区域,
这两个是雪花算法里必要的。互联网模式下,这两者也会用不同的方案去做区分,比如用zk的顺序节点id,这里不展开讲,只说跟id生成相关的逻辑
Sequence 类是处理id的核心
/**
* 获取下一个 ID
*
* @return 下一个 ID
*/
public synchronized long nextId() {
//获取当前时间戳
long timestamp = timeGen();
//闰秒,处理时针回拨,很多时候因为时间同步产生时间点不一致,当前时间比之前的时间戳小,这个时候需要回拨时间
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
//左移一位,等待2倍时间差,消除时间回拨
wait(offset << 1);
//重新获取时间戳
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
// 时间差超过5ms,直接抛异常
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}
if (lastTimestamp == timestamp) {
// 相同毫秒内,序列号自增
// 这里有个序列面罩,按位与拿到加一后的结果
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 同一毫秒的序列数已经达到最大,切换到下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒内,序列号置为 1 - 3 随机数
sequence = ThreadLocalRandom.current().nextLong(1, 3);
}
lastTimestamp = timestamp;
// 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
这段代码里有几个点:
一是时钟回拨问题的解决,baomidou是直接等待;时钟回拨还有一些解决方案,比如从机器位拿两位做回拨计数位,我个人觉得等待就够用了;
二是同时刻序列号的自增和占满修改时间戳;
三是不同毫秒的random,我个人觉得没什么必要,直接给1其实也没问题
四是把这些拼接成64bit的结果
雪花算法的实现部分到这里就结束了,上面说的无参构造也不是真的没有数据中心id和workid,是mybatisplus根据48位MAC地址推演出来的两个参数
展示下代码
public Sequence() {
this.datacenterId = getDatacenterId(maxDatacenterId);
this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
}
/**
* 数据标识id部分
*/
protected static long getDatacenterId(long maxDatacenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
if (null != mac) {
id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
id = id % (maxDatacenterId + 1);
}
}
} catch (Exception e) {
logger.warn(" getDatacenterId: " + e.getMessage());
}
return id;
}
/**
* 获取 maxWorkerId
*/
protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
StringBuilder mpid = new StringBuilder();
mpid.append(datacenterId);
String name = ManagementFactory.getRuntimeMXBean().getName();
if (StringUtils.isNotBlank(name)) {
/*
* GET jvmPid
*/
mpid.append(name.split(StringPool.AT)[0]);
}
/*
* MAC + PID 的 hashcode 获取16个低位
*/
return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}
这里看着唬人,大多是一些补码运算,移位运算,逻辑运算,用手算一算就清楚了,把大学课堂再拿出来捋一捋
Time