雪花算法

7/6/2024
(adsbygoogle = window.adsbygoogle || []).push({});

# 简介

雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。 1:最高 1 位是符号位,固定值 0,表示id 是正整数 41:接下来 41 位存储毫秒级时间戳,2^41/(1000606024365)=69,大概可以使用 69 年。 10:再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。 12:最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。 2^63=9,223,372,036,854,775,808 十进制下就是19位数

# 具体实现

常用的有mybatis-plus的,hutool等,主要介绍下mp的

# mp默认的

com.baomidou.mybatisplus.core.toolkit.Sequence

/**
  * 数据标识id部分
  */
protected long getDatacenterId(long maxDatacenterId) {
    long id = 0L;
    try {
        if (null == this.inetAddress) {
            if (logger.isDebugEnabled()) {
                logger.debug("Use localhost address ");
            }
            this.inetAddress = InetAddress.getLocalHost();
        }
        if (logger.isDebugEnabled()) {
            logger.debug("Get " + inetAddress + " network interface ");
        }
        NetworkInterface network = NetworkInterface.getByInetAddress(this.inetAddress);
        if (logger.isDebugEnabled()) {
            logger.debug("Get network interface info: " + network);
        }
        if (null == network) {
            logger.warn("Unable to get network interface for " + inetAddress);
            id = 1L;
        } else {
            byte[] mac = network.getHardwareAddress();
            if (null != mac) {
                id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        }
    } catch (Exception e) {
        logger.warn(" getDatacenterId: " + e.getMessage());
    }
    return id;
}


/**
 * 获取 maxWorkerId
 */
protected long getMaxWorkerId(long datacenterId, long maxWorkerId) {
    StringBuilder mpid = new StringBuilder();
    mpid.append(datacenterId);
    String name = ManagementFactory.getRuntimeMXBean().getName();
    if (StringUtils.isNotBlank(name)) {
        /*
          * GET jvmPid
          */
        mpid.append(name.split(StringPool.AT)[0]);
    }
    /*
      * MAC + PID 的 hashcode 获取16个低位
      */
    return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

/**
 * 获取下一个 ID
 *
 * @return 下一个 ID
 */
public synchronized long nextId() {
    long timestamp = timeGen();
    //闰秒
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        if (offset <= 5) {
            try {
                wait(offset << 1);
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
        }
    }

    if (lastTimestamp == timestamp) {
        // 相同毫秒内,序列号自增
        sequence = (sequence + 1) & sequenceMask;
        if (sequence == 0) {
            // 同一毫秒的序列数已经达到最大
            timestamp = tilNextMillis(lastTimestamp);
        }
    } else {
        // 不同毫秒内,序列号置为 1 - 2 随机数
        sequence = ThreadLocalRandom.current().nextLong(1, 3);
    }

    lastTimestamp = timestamp;

    // 时间戳部分 | 数据中心部分 | 机器标识部分 | 序列号部分
    return ((timestamp - twepoch) << timestampLeftShift)
        | (datacenterId << datacenterIdShift)
        | (workerId << workerIdShift)
        | sequence;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# 问题

# 前端接收丢失末尾两位

现象: 1810559288063254529前端接收的为1810559288063254500 原因: 前端Number的最大整数是2 ** 53 -1 解决方案:

  1. 改为string类型和前端传输
  2. 并发量少的情况可以减少最后序列号的位数
  3. 可以通过添加拦截器统一修改

参考: https://segmentfault.com/q/1010000038232026 https://blog.csdn.net/qq_42672839/article/details/130562170

# 重复ID,改进版(版本 >= 3.4.0)

k8s集群下,进程号pid为1导致生成重复ID的可能性大大增 具体示例

MAC地址 后两个转二进制 右移6位 十进制 对32取余
06:d0:6f:1f:96:f8 1001 0110 1111 1000 1001 0110 11 603 27
8e:a5:66:95:e6:c5 1110 0110 1100 0101 1110 0110 11 923 27

验证代码

package me.panxin.snowflake;

public class DatecenterId {

    public static final long generate(String macAddress) {
        long id = 0;
        String[] split = macAddress.split(":");
        byte[] mac = new byte[6];
        for (int i = 0; i < split.length; i++) {
            mac[i] = (byte) Integer.parseInt(split[i], 16);
        }
        if (null != mac) {
            id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
            id = id % (32);
        }
        return id;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.panxin.snowflake;

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class DatacenterIdTest {

    @Test
    public void testDatacenterId() {
        List<String> list = Arrays.asList(
                "82:11:F3:FC:7B:7F",
                "AA:D7:3C:04:49:EE",
                "86:38:F4:94:AE:60",
                "5A:18:2C:B5:7D:E7",
                "AE:03:49:4F:B3:DF",
                "1E:EC:62:6C:91:2A",
                "A2:C4:DE:C4:27:F0",
                "36:6D:53:FF:6A:13"
        );
        for (String macAddress : list) {
            long id = DatecenterId.generate(macAddress);
            System.out.println(id);
        }
        /**
         * 输出结果
         * 29
         * 25
         * 2
         * 29
         * 30
         * 10
         * 0
         * 13
         */
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

主要是pid相同,导致有一定概率中间的10位机器码相同 com.baomidou.mybatisplus.core.incrementer.ImadcnIdentifierGenerator 个人理解用redis生成中间10位置机器码应该也可以解决这个问题。

# 参考

https://github.com/baomidou/mybatis-plus/pull/6247 https://github.com/baomidou/mybatis-plus/issues/6177 https://blog.csdn.net/w1014074794/article/details/125607205