KOCA版本 :4.2.0
KOCA模块 :koca-id-generator
模块版本 :4.2.0
场景 :低并发
问题 :生成的ID几乎都是偶数
报错细节 :
尝试解决方案:
在并发比较低的情况下,如以下测试用例,每次获取ID后,休眠1ms后再获取下一个ID
int total = 10000;
// 奇数数量
int oddNum = 0;
// 偶数数量
int evenNum = 0;
for (int i = 0; i < total; i++) {
String id = idGenerator.getId("test");
if (Long.parseLong(id) % 2 == 0) {
evenNum++;
} else {
oddNum++;
}
// 休眠1ms
Thread.sleep(1);
}
oddNum = 0;
evenNum = 0;
for (int i = 0; i < total; i++) {
String id = idGenerator.getId("test");
if (Long.parseLong(id) % 2 == 0) {
evenNum++;
} else {
oddNum++;
}
}
System.out.println("high concurrency, total: " + total + " odd: " + oddNum + " even: " + evenNum);
获取10000个ID,最终在sleep(1)的情况下,获取的全是偶数,不sleep的获取的奇数偶数相差不大
在部分场景下,会产生问题,列如使用ID作为分表键插入数据,会导致数据大量分布在偶数分表键的表里,极端情况下可能所有数据都在偶数分表键的表里
通过源码可发现同一个毫秒时间戳内,最多可以生成4095个ID,序列sequence从0开始,如果获取序列时,已经不是上一个时间戳,sequence会置为0,这样在并发比较低的情况下,sequence大部分都是0,加上最后生成ID时都是左移位运(<<)之后再或运算(|),导致生成的ID二进制下最后一位都是0,也就是十进制都是偶数。
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCEMASK;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
if (littleFlag) {
//移位并通过或运算拼到一起组成63位的ID datacenterId 的最大值小一倍
return ((timestamp - TWEPOCH) << TIMESTAMPLEFTSHIFT - 1) | (datacenterId << DATACENTERIDSHIFT) |
(workerId << WORKERIDSHIFT) | sequence;
} else {
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - TWEPOCH) << TIMESTAMPLEFTSHIFT) | (datacenterId << DATACENTERIDSHIFT) |
(workerId << WORKERIDSHIFT) | sequence;
}
解决方法,如果不在同一个毫秒时间戳内,sequence不置为0,而是使用使用100内随机数,但这样会导致ID数量减少,最大减少99个(随机到了99),但对于总数4095来说能够接受。
sequence = RandomUtils.nextInt(100);
补丁版本4.2.1已使用上述解决方法解决了该问题,后面的主线版本4.7.0会同步修改。