1.有mybatis-plus 3.5.0。Sequence类还是构造雪花算法的实现类:其函数和下面这篇博客写的功能完全一致:https://www.modb.pro/db/150947 为了放置该博客失效:我还是简单介绍下:
mybitas-plus Sequence源码:
public synchronized long nextId() {
long timestamp = timeGen();
//闰秒
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
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 {
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;
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
//该SystemClock.now就是使用一个守护线程实时更新当前的时间戳
return SystemClock.now();
}
如果本次获取的时间戳比上次小,说明发生了时间回退,可能是全球闰秒调整造成或者你主动执行了系统时钟同步的命令。
如果时间戳差值小于5毫秒,系统会自动等待,然后继续生成。
如果是闰秒问题,按目前调整记录都是每次调整1秒,显然超过了5毫秒,那么此时系统会抛出异常。也就是如果您的业务正好发生在闰秒调整后的那一秒之内则无法完成分表的写操作。当然这种几率较小,发生后还可以在业务层面延迟重试,基本上可以解决此问题。(上边博客的讲解)
重头戏来了:
MybatisConfiguration 类中的 GlobalConfig的id生成器,你自己不配置的话,会自动使用DefaultIdentifierGenerator作为该实现,然而默认的DefaultIdentifierGenerator 有如下4中构造器。默认会使用第一种,即InetAddress 为空作为参数传递给Sequenc
其中的getDatacenterId(maxDatacenterId)会在inetAddress为null时,使用
InetAddress.getLocalHost();
但是java这个api使用该方法:得到的却是localhost.localdomain/127.0.0.1,windows可能会取到正确的值.this.inetAddreess里面是localhost.localdomain/127.0.0.1,获得的mac也会是null,网卡的mac地址(物理地址)压根取不到
至于原因:建议看看这个博客 Java中InetAddress的使用(二):获取本机IP地址的正确姿势这个就很致命,mybatis-plus默认获取的ip地址都是一定的,雪花算法(时间戳+机器号+序列号)的序列号每次都是一定的。机器号又是根据(序列号+进程id)%固定的maxWorkerId(32)。
只要集群中某两台机器的时间戳和进程号相同,那么生成的主键就一定相同,这就违背了数据库的主键唯一的原则。所以我们需要手动获得本地的ip地址,将其传递给Sequence才能保证雪花算法的唯一性
获得本机的ip地址
private static InetAddress getLocalHostExactAddress() {
try {
InetAddress candidateAddress = null;
// 获得该主机下的所有网卡信息:包括名称,ipv4地址和ipv6地址
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface iface = networkInterfaces.nextElement();
// iface.isUp Returns whether a network interface is up and running.
//iface.isLoopback Returns whether a network interface is loopback.回环地址
//只有正在运行且地址不为回环地址的网卡才能找到本机的ip
if (iface.isLoopback() || !iface.isUp()) {
continue;
}
// 该网卡接口下的ip会有多个,也需要一个个的遍历,找到自己所需要的
for (Enumeration<InetAddress> inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements(); ) {
InetAddress inetAddr = inetAddrs.nextElement();
if (inetAddr.isSiteLocalAddress()) {
// 如果是site-local地址,就是它了 就是我们要找的
// ~~~~~~~~~~~~~绝大部分情况下都会在此处返回你的ip地址值~~~~~~~~~~~~~
return inetAddr;
}
// 若不是site-local地址 那就记录下该地址当作候选
if (candidateAddress == null) {
candidateAddress = inetAddr;
}
}
}
// 如果出去loopback回环地之外无其它地址了,那就回退到原始方案吧
return candidateAddress == null ? InetAddress.getLocalHost() : candidateAddress;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}