工作时无意间发现sahrding-jdbc使用雪花算法生成的id 在某一业务分库分表 永远在那两个库表里面,排查后这里做下分享
环境、配置、问题介绍
- 16库16表
- 使用的是org.apache.shardingsphere.core.strategy.keygen下面generateKey生成id
- 分库表算法是对16取模
- 生成数据永远在0库0表 0库1表 1库0表 1库1表
雪花算法构成部分
雪花算法一共由64个bit组成 也就是我们常说的64位,换算下来就是8个字节
- 第一位是付号位 也就是代表正负 0正 1负数
- 后面的41位是时间戳
- 在后面的10位又可以细分成5+5,代表机房id和机器id也可以直接使用机器id表示
- 最后12位就是最小颗粒度的序号,也就是同一毫秒值内同一机房同一机器可以生成多少个不同的序号,12位最大也就是4096(包含0就是4095)
源码分析
下面是sahrding-jdbc生成id的源码跟读一下
@Override
public synchronized Comparable<?> generateKey() {
// 获取当前时间戳
long currentMilliseconds = timeService.getCurrentMillis();
// 这里面判断当前时间戳是否在允许的浮动范围内
if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) {
// 不允许的范围会重新获取时间戳
currentMilliseconds = timeService.getCurrentMillis();
}
// 如果本次获取id的时间戳和上次相同则对sequence进行+1 &我们后面细说
if (lastMilliseconds == currentMilliseconds) {
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
// 自旋至到当前时间大于上次时间
currentMilliseconds = waitUntilNextTime(currentMilliseconds);
}
} else {
// 这里是获取当前sequence的值
vibrateSequenceOffset();
sequence = sequenceOffset;
}
lastMilliseconds = currentMilliseconds;
// 时间减去初始的时间左移22位 或 workId左移12位 或 获取到的sequence
return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS)
| (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
详解
先了解一下二进制部分运算付号 和源码 反码 补码
- ‘&’ 记住一点就是遇到0就是0即可 如:0000 0001 & 1111 1110 = 0000 0000
- ‘|’ 和’&'类似遇到1就是1即可 如: 0000 0001 | 1111 1110 = 1111 1111
- ‘~’ 所有二进制全部取反 如: ~ 0011 0011 = 1100 1100
- ‘<<’ 左移符号就是二进制向左边移动多少位,之后高位补0 如: 1100 << 2 = 11 0000 高位补0 -> 0011 0000
- ‘>>’ 同理左移 这个是右移符不同的是低位会直接舍弃如: 0010 1111 >> 4 = 0010 高位补0 -> 0000 0010
- ‘反码’ 正数来说反码就是原码 负数就是二进制进行取反也就是上面说的’~’ 唯一不同的是负数反码不针对符号位
- ‘补码’ 正数补码就是原码 负数就是反码最低位+1 如: 0001 0000 的补码就是 0001 0001
通过上面的了解再来看下面这段代码
这里的SEQUENCE_MASK是1 左移12位减1等于2的22次方减一也就是4095
(sequence + 1) & SEQUENCE_MASK) 通过上面的二进制运算我们知道SEQUENCE_MASK 4095的二进制是从低位到高位一共12个1 再往高位去全是0而&付号的运算是遇到0就是0所以我们可以得知这里最大值一定是4095 如:
4096 -> 0001 0000 0000 0000 & 4095 -> 0000 1111 1111 1111 1111 = 0000 0000 0000 0000 -> 0
4097 -> 0001 0000 0000 0001 & 4095 -> 0000 1111 1111 1111 1111 = 0000 0000 0000 0001 -> 1
private static final long SEQUENCE_BITS = 12L;
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
0L == (sequence = (sequence + 1) & SEQUENCE_MASK)
if (lastMilliseconds == currentMilliseconds) {
if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
// 自旋至到当前时间大于上次时间
currentMilliseconds = waitUntilNextTime(currentMilliseconds);
}
}
sequenceOffset默认值是0 这就是运算 0取反 0000 0000 & 1 -> 0000 0001 = 0000 0001 = 1
private byte sequenceOffset;
else {
vibrateSequenceOffset();
sequence = sequenceOffset;
}
private void vibrateSequenceOffset() {
sequenceOffset = (byte) (~sequenceOffset & 1);
}
一部分是毫秒值左偏移22位
第二部分是workId左偏移12位
第三部分是sequence
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_BITS = 10L;
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS)
| (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
问题原因
通过上面的解析我们发现最终雪花算法生成的id由三个部分生成,
第一部分的值左偏移了22位也就是二进制22位到低位全是0
第二部分左偏移12位 12位到低位全是0
第三部分的值由时间戳决定同一毫秒值内能出现的值最大有4095 不同毫秒值内能出现的值只有0和1
我们这里分库分表的算法是%16而我们发现第一部分和第二部分进行|运算后12位到最低位的值是0
12位往高位有值这最终运算后得出来的值一定是2的12次方以上数字相加这样的数字由于一定是16的整数倍所以取模一定是0而最终落日库表就取决于sequence我们的并发又没有高到一毫秒出现很多次请求进来导致生成的sequence不是0就是1所以最终取模会在0和1上面
这里解释一下为什么毫秒值左偏移 | workId左偏移一定可以被16整除
如:
0001 >> 22 = 0100 0000 0000 0000 0000 = 02(n) + 12(23) + 02(22) …+ 02(0)
0001 >> 12 = 0000 0001 0000 0000 0000 = 02(n) + 12(13) + 02(12)…+ 02(0)
最终的值一定是2*2(4) = 16 的整数倍