使用PHP、NodeJS、Swift 实现雪花算法

现有的MySql 数据库在实现大数据的分库分表时,会碰上分表时主键自增ID重复问题。虽然在分表时可以利用规分ID值区间规则的方式规避问题。但很明显不可能会有程序员这样玩。


自增ID不可避免以面对多表ID重复问题

过去一些案例有使用 UUID 来作为主键,不过UUID 生成的是一个无序的字符串,对于 MySQL 推荐使用增长的数值类型值作为主键来说不适合,同时以字符串方式存入到MySql 的聚簇索引中,既浪费空间也不利于查询性能优化。

也有在插入数据表前,利用 Redis 的自增原子性来建立唯一 ID再存入的做法,当然也是考虑到性能问题,在业内是非常少用。

比起MongoDB,天生带有处理这种分库分布能力的ObjectID特性。MySql在这方面确实是挺颓势。还好外面还是有不少优秀的解决方法,其中由Twitter 的SnowFlake(雪花) 算法就是能够实现这样的一种分布式 id 的生成算法。其核心思想就是:使用一个 64 位 的 long 型的数字作为全局唯一 id。通过对64位中每个区间位置的引入特定特性的做法,有效利用一个Long型来满足自增并且唯一的能力。
简单来介绍就是如图:


雪花算法对Long型ID值的结构说明

1bit-不用: 第一位为0,没有意义。因为MySql的Long类型属于无符号正整数,如果是 1 就成负数了。

41bit-时间戳:表示的是时间戳,2^41/(1000606024365)=69,大概可以使用 69 年。

5bit-工作机器id + 5bit-服务码id:这两个数合计可以表达服务器+分区名,正常情况下同一个表允许分出 1024 个区。

12bit-序列号:表示1毫秒内产生的不同 id,12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。

PHP的实现算法如下:

function snowFlake_1($mach_id,$server_id,$seq){
    
    //由于41位容量只够玩69年,所以时间戳应该由系统发布的时间开始进行减掉。69年后这个时间就得变更了。
    $sys_public_time=1676731103000;//2023-02-18 22:38:23
    $time=intval(microtime(true) * 1000);
    $offset_time=$time-$sys_public_time;
    $time_22=($offset_time) << 22;
    $mach_id_17=$mach_id << 17;
    $server_id_12=$server_id << 12;
    $combine=$time_22|$mach_id_17|$server_id_12 | $seq;
    
    var_dump("当前时间:".$time);
    var_dump("扣减后的时间差:".$offset_time);

    var_dump("把时间差转为二进制的值 :".decbin($offset_time));
    var_dump("时间差左移22位后的二进制的值:".decbin($time_22));
    var_dump("机器码二进制的值:".decbin($mach_id_17));
    var_dump("服务码二进制的值:".decbin($server_id_12));
    var_dump("序列码:".decbin($seq));
    var_dump("合并后的二进制:".decbin($combine));
    var_dump("应该要返回给数据库的雪花值:".$combine);

    return $combine;
}

#执行
$seq=mt_rand(0, 4095);
snowFlake_1(1,3,$seq);

输出结果:

string(26) "当前时间:1676731793789"
string(28) "扣减后的时间差:690789"
string(55) "把时间差转为二进制的值 :10101000101001100101"
string(84) "时间差左移22位后的二进制的值:101010001010011001010000000000000000000000"
string(43) "机器码二进制的值:100000000000000000"
string(39) "服务码二进制的值:11000000000000"
string(22) "序列码:110000010011"
string(66) "合并后的二进制:101010001010011001010000100011110000010011"
string(55) "应该要返回给数据库的雪花值:2897379212307"

换成NodeJS来实现,会出现一些小小的麻烦,两个类型不一样的变量发生位运算等操作时,只会转为32位。所以在发生计算时,每一个环节的变量都必须是BigInt类型:

const snowFlake = (mach_id,server_id,seq)=>{
     //由于41位容量只够玩69年,所以时间戳应该由系统发布的时间开始进行减掉。69年后这个时间就得变更了。
    const sys_public_time=BigInt(1676731103000);//2023-02-18 22:38:23

    const timestamp=BigInt(new Date().getTime());
    const offset_time=timestamp-sys_public_time;

    let timestamp_22=offset_time <

输出结果:

当前时间:1676734225158
扣减后的时间差:3122158
把时间差转为二进制的值 :1011111010001111101110
时间差左移22位后的二进制的值:10111110100011111011100000000000000000000000
机器码二进制的值:1111100000000000000000
服务码二进制的值:11111000000000000
序列码:111111111111
合并后的二进制:10111110100011111011101111111111111111111111
应该要返回给数据库的雪花值:13095283982335

Swift的代码:

func snowFlake(mach_id: Int32, server_id: Int32, seq:Int32) -> Int64{
    
    let sys_public_time: Int = 1676731103000;//2023-02-18 22:38:23
    let time: Int = Int(Date().timeIntervalSince1970 * 1000);
    
    let offset_time: Int = time - sys_public_time;
    
    let time_22: Int64 = Int64(offset_time) << 22;
    let mach_id_17: Int64 = Int64(mach_id) << 17;
    let server_id_12: Int64 = Int64(server_id) << 12;
    let combine:Int64 = time_22 | mach_id_17 | server_id_12 | Int64(seq);

    
    print(sys_public_time)
    print("当前时间:\(time)");
    print("扣减后的时间差:\(offset_time)");

    print("把时间差转为二进制的值 :\(String(offset_time,radix:2))");
    print("时间差左移22位后的二进制的值:\(String(time_22,radix:2))");
    print("机器码二进制的值:\(String(mach_id_17,radix:2))");
    print("服务码二进制的值:\(String(server_id_12,radix:2))");
    print("序列码:\(String(seq,radix:2))");
    print("合并后的二进制:\(String(combine,radix:2))");
    print("应该要返回给数据库的雪花值:\(combine)");

    
    return combine;
}
//执行:
snowFlake(mach_id: 31,server_id: 31,seq:4095);

输出结果:

1676731103000
当前时间:1679300780126
扣减后的时间差:2569677126
把时间差转为二进制的值 :10011001001010100010100101000110
时间差左移22位后的二进制的值:100110010010101000101001010001100000000000000000000000
机器码二进制的值:1111100000000000000000
服务码二进制的值:11111000000000000
序列码:111111111111
合并后的二进制:100110010010101000101001010001101111111111111111111111
应该要返回给数据库的雪花值:10778007052484607

你可能感兴趣的:(使用PHP、NodeJS、Swift 实现雪花算法)