sahrding-jdbc的雪花算法取模为0或1的问题

工作时无意间发现sahrding-jdbc使用雪花算法生成的id 在某一业务分库分表 永远在那两个库表里面,排查后这里做下分享

环境、配置、问题介绍

  1. 16库16表
  2. 使用的是org.apache.shardingsphere.core.strategy.keygen下面generateKey生成id
  3. 分库表算法是对16取模
  4. 生成数据永远在0库0表 0库1表 1库0表 1库1表

雪花算法构成部分

雪花算法一共由64个bit组成 也就是我们常说的64位,换算下来就是8个字节

  1. 第一位是付号位 也就是代表正负 0正 1负数
  2. 后面的41位是时间戳
  3. 在后面的10位又可以细分成5+5,代表机房id和机器id也可以直接使用机器id表示
  4. 最后12位就是最小颗粒度的序号,也就是同一毫秒值内同一机房同一机器可以生成多少个不同的序号,12位最大也就是4096(包含0就是4095)

sahrding-jdbc的雪花算法取模为0或1的问题_第1张图片

源码分析

下面是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;
    }

详解
先了解一下二进制部分运算付号 和源码 反码 补码

  1. ‘&’ 记住一点就是遇到0就是0即可 如:0000 0001 & 1111 1110 = 0000 0000
  2. ‘|’ 和’&'类似遇到1就是1即可 如: 0000 0001 | 1111 1110 = 1111 1111
  3. ‘~’ 所有二进制全部取反 如: ~ 0011 0011 = 1100 1100
  4. ‘<<’ 左移符号就是二进制向左边移动多少位,之后高位补0 如: 1100 << 2 = 11 0000 高位补0 -> 0011 0000
  5. ‘>>’ 同理左移 这个是右移符不同的是低位会直接舍弃如: 0010 1111 >> 4 = 0010 高位补0 -> 0000 0010
  6. ‘反码’ 正数来说反码就是原码 负数就是二进制进行取反也就是上面说的’~’ 唯一不同的是负数反码不针对符号位
  7. ‘补码’ 正数补码就是原码 负数就是反码最低位+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
不同毫秒值再进来 sequenceOffset经过上次值是1 之后运算 1取反 1111…0 & 1 = 0000 0000 = 0

	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 = 0x2(n) + 1x2(23) + 0x2(22) …+ 0x2(0)
0001 >> 12 = 0000 0001 0000 0000 0000 = 0x2(n) + 1x2(13) + 0x2(12)…+ 0x2(0)
最终的值一定是2x2(4) = 16 的整数倍

你可能感兴趣的:(java,sharding-jdbc,java,开发语言)