隐匿之符:一次关于负号的雪花追溯

在 64 位的世界里,最左边的那一位,决定了一切。

一、背景

在测试环境中,发现数据库中的 ID 值出现负数现象,第一反应是 ID 超过 long 类型能表示的最大值,导致数据转成负值。

Java 中 long 类型的最大值为 2^63 - 1 = 9223372036854775807,超过该值后会转成负值。

二、实际环境分析数据

生产环境数据分析:

  • 按最近3个月 ID 增长趋势预估:预测 5.5 个月后会增长至 long 最大值
  • 按最近16个月 ID 增长趋势预估:预测仅2+个月后会增长至 long 最大值

可见 ID 增长速度有加快趋势,如果未采取处理举措,将很快遇到全量 ID 转成负值问题。

三、负 ID 对系统的影响

1. 数据库影响

  • 性能问题:ID 递增类值用作主键时,如果达到负值,会导致类 B+ 树结构非最后端插入,触发频繁的页分裂,降低性能
  • 排序问题:负 ID 会导致排序结果异常,尤其是用 ID 进行排序的场景

2. 业务影响

  • 分页查询崩溃:一些场景依赖 ID 递增,实现分页(如每次按 ID 条件查询大于上次最大 ID ),不支持负值时会导致查询失效或重复数据

四、解决方案

业务层面(代码侵入)

id 转成BigInteger 然后数据库用无符号bigint

百度id uid generator 方向

默认配置

protected int timeBits = 28;
protected int workerBits = 22;
protected int seqBits = 13;

配置解析

timeBits (28)workerBits (22)seqBits (13)
时间戳(秒)机器号(固定为29)每秒序列号

UID = (deltaSeconds << (22 + 13)) | (workerId << 13) | sequence;

  1. 序列号(sequence)

    2^13 - 1 = 8191

    • 最多能表示:8192 个唯一值
    • 范围是:0 ~ 8191(共 8192 个)
    • 所以在十进制里:最后最多就是 8191
  2. 机器号(workerId)

    2^22 - 1 = 4,194,304

    • 最多能表示:4,194,303个唯一值
    • 范围是:0 ~ 4,194,303**(共** 4,194,304个)
    • 所以在十进制里:最后最多就是 4,194,304
  3. 时间(deltaSeconds )

    2^28 - 1 = 268,435,455 秒

    • 秒:268,435,455 秒
    • 分钟:≈ 4,473,924 分钟
    • 小时:≈ 74,565 小时
    • 天数:≈ 3,107 天
    • 年数:≈ 8.5 年

    你从设置的起始时间(Epoch)开始,最多能支撑 8.5 年内的 UID 生成

问题原因

Epoch默认时间 :2016-05-20

2016+8.5年 2025年之后会超变成负数

处理方案

目标 :

  1. 保证id 不重复

  2. 尽量保证id 递增趋势

    当前id :9090949948782127403L

    0111111000101001100010101101000000000000000000111010100100101011(64位)

    0 标志位

    1111110001010011000101011010 → 264581466 (时间差秒)

    0000000000000000011101 → 29 (机器号)

    0100100101011 → 2347 (序列号)

处理方法 : 调整uid 各组成位置位数

  1. 首先保证id 不重复可以通过修改机器号,不使用之前的机器id 生成uid 可以保证id 不与之前重复,即使时间戳一样

  2. 将时间差位置扩充以便能使用的时间更长,减少机器号位置

    30位 ≈ 31, 557,600 秒 ≈ 34.0年 机器号:20位=1,048,576个

    protected int timeBits = 30;
    protected int workerBits = 20;
    protected int seqBits = 13;
    

    UID = (deltaSeconds << (20 + 13)) | (workerId << 13) | sequence;

  3. 只将时间戳位置 扩充30位,如果时间差还是当前时间差,那时间戳位置向后移动两位,生成的id 必然小于之前id,观察目前2进制时间差前6位都是1 如果想生成的id大于之前id,需要保证 标识位0 后 7位都是1 ,这样 时间戳位置就需要扩充至37位 ,前 7位为1 保证大于之前id 但需要每次算时间差 + 上前7为是1后30位为0的37位2进制数(1111111000000000000000000000000000000 =34091302913),但同时机器位缩小至13位 只能表示8192台机器号 (如果机器号不够用可以折中,适当缩小时间差位数,但可能生成id 会小于之前id 位数缩短越多交叉越多)

    protected int timeBits = 37;
    protected int workerBits = 13;
    protected int seqBits = 13;
    

    UID = (mask << 30+13 + 13) | (deltaSeconds << (13 + 13)) | (workerId << 13) | sequence;

处理流程及源码修改

公式可能不好理,解通俗解释:

28位二进制时间差 + 22位二进制机器号 + 13位 二进制序列号

举例:

  • 时间 2025-04-16 00:00:00 时间戳 1744732800 秒
  • Epoch默认时间 :2016-05-20 00:00:00 时间戳 1463673600秒
  • 时间差 281059200 = 1744732800 - 1463673600
  • 机器号比如 现在是 29 序列号1024
  • id = 二进制(时间差 281059200) + 二进制(29)+ 二进制(1024) 不够需要补全对应位置的位数
  • 10000110000001001111110000000 + 0000000000000000011101 + 0010000000000
  • 整体转long 无符号 9657120577919624088 有符号 -8789623495789927528、
  • 为啥有符号是负的 ,这是因为 28位时间差 最多表示 268,435,455秒 但 281,059,200 时间差明显超过了 ,再转二进制 就是1开头的29位二进制数 10000110000001001111110000000 这样 开头的标志位 0 就被时间差的第29位占用了,标志位是1 转 long 就是负数
  • 通过调整时间位,让时间位够用 ,不占用标志位 就能 使long值为正数
  • 调整后 30位二进制时间差 + 20位二进制机器号 + 13位 二进制序列号
  • 二进制(时间差 281059200) + 二进制(29)+ 二进制(1024) 不够需要补全对应位置的位
  • 010000110000001001111110000000 + 00000000000000011101 + 0010000000000
  • 整体转long 值为 2414280144480085222 比目前id 小可能会有一系列问题
  • 那 如何保证生成的id 大呢解析目前id 时间戳位置 前6位都是1 保证比之前id大 需要二进制时间戳前7位都是1 此时生成的id 比 之前的大 暂时称 1 的这几位数是 mask 位 减少 机器号位置增加mark位
  • 此时 id = 7位二进制 (mask )+ 30位二进制时间差 + 13位二进制机器号 + 13位 二进制序列号
  • 1111111 + 010000110000001001111110000000 + 0000000011101 + 0010000000000
  • 整体转long 值为 9170176006445836224
  • 达到不重复,不为负数,比之前id大的目的

代码调整后增加mask 位(标红位置为修改源码)

/*
 * Copyright (c) 2017 Baidu, Inc. All Rights Reserve.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     <http://www.apache.org/licenses/LICENSE-2.0>
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.baidu.fsg.uid;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.springframework.util.Assert;

/**
 * Allocate 64 bits for the UID(long)<br>
 * sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
 * 
 * @author yutianbao
 */
public class BitsAllocator {
    /**
     * Total 64 bits
     */
    public static final int TOTAL_BITS = 1 << 6;

    /**
     * Bits for [sign-> mark -> second-> workId-> sequence]
     */
    private int signBits = 1;
    private final int timestampBits;
    private final int workerIdBits;
    private final int sequenceBits;

    /**
     * Max value for workId & sequence
     */
    private final long maxDeltaSeconds;
    private final long maxWorkerId;
    private final long maxSequence;

    /**
     * Shift for timestamp & workerId
     */
    private final int timestampShift;
    private final int workerIdShift;

    //面具位 7位为1 保证大于之前id  但需要每次算时间差 + 上前7为是1后30位为0的37位2进制数(1111111000000000000000000000000000000 =136365211648
    // mask = (1L << maskLen) -1 面具值 = 1111111
    //mask << maskShift = 1111111000000000000000000000000000000
    private final int maskLen = 7;
    private final long mask ;
    private final int maskShift;

    /**
     * Constructor with timestampBits, workerIdBits, sequenceBits<br>
     * The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
     */
    public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
        // make sure allocated 64 bits
        int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
        Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");

        // initialize bits
        this.timestampBits = timestampBits;
        this.workerIdBits = workerIdBits;
        this.sequenceBits = sequenceBits;

        // initialize max value
        this.maxDeltaSeconds = ~(-1L << timestampBits);
        this.maxWorkerId = ~(-1L << workerIdBits);
        this.maxSequence = ~(-1L << sequenceBits);

        // initialize shift
        this.timestampShift = workerIdBits + sequenceBits;
        this.workerIdShift = sequenceBits;
        //面具参数相关值
        this.mask = (1L << maskLen) -1 ;
        this.maskShift = timestampShift + timestampBits - maskLen;
    }

    /**
     * Allocate bits for UID according to delta seconds & workerId & sequence<br>
     * <b>Note that: </b>The highest bit will always be 0 for sign
     * 
     * @param deltaSeconds
     * @param workerId
     * @param sequence
     * @return
     */
    public long allocate(long deltaSeconds, long workerId, long sequence) {
        return  (mask << maskShift) | (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
    }
    
    /**
     * Getters
     */
    public int getSignBits() {
        return signBits;
    }

    public int getTimestampBits() {
        return timestampBits;
    }

    public int getWorkerIdBits() {
        return workerIdBits;
    }

    public int getSequenceBits() {
        return sequenceBits;
    }

    public long getMaxDeltaSeconds() {
        return maxDeltaSeconds;
    }

    public long getMaxWorkerId() {
        return maxWorkerId;
    }

    public long getMaxSequence() {
        return maxSequence;
    }

    public int getTimestampShift() {
        return timestampShift;
    }

    public int getWorkerIdShift() {
        return workerIdShift;
    }
    
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
    
}

备注:

  1. 修改之后如果时间位不够用生成 的id 可能重复,修改之前位数不够用先是负数后可能重复
  2. 修改之后如果代码涉及到id 解析的 需要修改对应解析方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值