雪花演算法及微服務叢集唯一ID解決方案

胡小華發表於2022-06-22

雪花演算法(SnowFlake)

簡介
現在的服務基本是分散式、微服務形式的,而且大資料量也導致分庫分表的產生,對於水平分表就需要保證表中 id 的全域性唯一性。

對於 MySQL 而言,一個表中的主鍵 id 一般使用自增的方式,但是如果進行水平分表之後,多個表中會生成重複的 id 值。那麼如何保證水平分表後的多張表中的 id 是全域性唯一性的呢?

如果還是藉助資料庫主鍵自增的形式,那麼可以讓不同表初始化一個不同的初始值,然後按指定的步長進行自增。例如有3張拆分表,初始主鍵值為1,2,3,自增步長為3。

當然也有人使用 UUID 來作為主鍵,但是 UUID 生成的是一個無序的字串,對於 MySQL 推薦使用增長的數值型別值作為主鍵來說不適合。

也可以使用 Redis 的自增原子性來生成唯一 id,但是這種方式業內比較少用。

當然還有其他解決方案,不同網際網路公司也有自己內部的實現方案。雪花演算法是其中一個用於解決分散式 id 的高效方案,也是許多網際網路公司在推薦使用的。

SnowFlake 雪花演算法
SnowFlake 中文意思為雪花,故稱為雪花演算法。最早是 Twitter 公司在其內部用於分散式環境下生成唯一 ID。在2014年開源 scala 語言版本。
雪花演算法的原理就是生成一個的 64 位位元位的 long 型別的唯一 id。
image

最高 1 位固定值 0,因為生成的 id 是正整數,如果是 1 就是負數了。
接下來 41 位儲存毫秒級時間戳,2^41/(1000606024365)=69,大概可以使用 69 年。
再接下 10 位儲存機器碼,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 臺機器。
最後 12 位儲存序列號。同一毫秒時間戳時,通過這個遞增的序列號來區分。即對於同一臺機器而言,同一毫秒時間戳下,可以生成 2^12=4096 個不重複 id。
可以將雪花演算法作為一個單獨的服務進行部署,然後需要全域性唯一 id 的系統,請求雪花演算法服務獲取 id 即可。

對於每一個雪花演算法服務,需要先指定 10 位的機器碼,這個根據自身業務進行設定即可。例如機房號+機器號,機器號+服務號,或者是其他可區別標識的 10 位位元位的整數值都行。

實際應用

MybatisPlus實現

依賴:

     	<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>

yml配置:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    worker-id: ${random.int(1,31)}
    datacenter-id: ${random.int(1,31)}

測試實體:

@Data
@TableName("test_content")
public class TestContent {
    /**
     * ID
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;


    /**
     * 資料內容
     */
    private String content;

    /**
     * 部門id
     */
    private Integer deptId;
}

測試控制層:

  @GetMapping("/test2")
    public String add() {
        TestContent testContent = new TestContent();
        testContent.setContent(new Random().nextInt() + "自定義新增內容");
        testContent.setDeptId(1);
        int insert = testContentService.getBaseMapper().insert(testContent);
        log.info("插入成功:{}", testContent.getId());
        return "插入成功";
    }

插入測試:

非ID欄位需要id時可使用Idwork

        testContent.setId(IdWorker.getId());

原始碼解析:

IdWorker提供獲取id的基本方法,底層通過DefaultIdentifierGenerator生成序列Sequence類用於生成雪花id


public class IdWorker {
    private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator();
    public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    public IdWorker() {
    }

    public static long getId() {
        return getId(new Object());
    }

    public static long getId(Object entity) {
        return IDENTIFIER_GENERATOR.nextId(entity).longValue();
    }

    public static String getIdStr() {
        return getIdStr(new Object());
    }

    public static String getIdStr(Object entity) {
        return IDENTIFIER_GENERATOR.nextId(entity).toString();
    }

    public static String getMillisecond() {
        return LocalDateTime.now().format(MILLISECOND);
    }

    public static String getTimeId() {
        return getMillisecond() + getIdStr();
    }

    public static void initSequence(long workerId, long dataCenterId) {
        IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId);
    }

    public static void setIdentifierGenerator(IdentifierGenerator identifierGenerator) {
        IDENTIFIER_GENERATOR = identifierGenerator;
    }

    public static String get32UUID() {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return (new UUID(random.nextLong(), random.nextLong())).toString().replace("-", "");
    }
}

Sequence類:主要構造方法包含兩個引數 類比雪花演算法的機器ID和服務ID,叢集模式下最好不要重複,否則可能會造成生成的Id重複,兩個引數可在YML檔案中配置

public class Sequence {
    private static final Log logger = LogFactory.getLog(Sequence.class);
    private final long twepoch = 1288834974657L;
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long maxWorkerId = 31L;
    private final long maxDatacenterId = 31L;
    private final long sequenceBits = 12L;
    private final long workerIdShift = 12L;
    private final long datacenterIdShift = 17L;
    private final long timestampLeftShift = 22L;
    private final long sequenceMask = 4095L;
    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public Sequence() {
        this.datacenterId = getDatacenterId(31L);
        this.workerId = getMaxWorkerId(this.datacenterId, 31L);
    }

    public Sequence(long workerId, long datacenterId) {
        Assert.isFalse(workerId > 31L || workerId < 0L, String.format("worker Id can't be greater than %d or less than 0", 31L), new Object[0]);
        Assert.isFalse(datacenterId > 31L || datacenterId < 0L, String.format("datacenter Id can't be greater than %d or less than 0", 31L), new Object[0]);
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (StringUtils.isNotBlank(name)) {
            mpid.append(name.split("@")[0]);
        }

        return (long)(mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
    }

    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;

        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                if (null != mac) {
                    id = (255L & (long)mac[mac.length - 1] | 65280L & (long)mac[mac.length - 2] << 8) >> 6;
                    id %= maxDatacenterId + 1L;
                }
            }
        } catch (Exception var7) {
            logger.warn(" getDatacenterId: " + var7.getMessage());
        }

        return id;
    }

    public synchronized long nextId() {
        long timestamp = this.timeGen();
        if (timestamp < this.lastTimestamp) {
            long offset = this.lastTimestamp - timestamp;
            if (offset > 5L) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }

            try {
                this.wait(offset << 1);
                timestamp = this.timeGen();
                if (timestamp < this.lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception var6) {
                throw new RuntimeException(var6);
            }
        }

        if (this.lastTimestamp == timestamp) {
            this.sequence = this.sequence + 1L & 4095L;
            if (this.sequence == 0L) {
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
        } else {
            this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L);
        }

        this.lastTimestamp = timestamp;
        return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp;
        for(timestamp = this.timeGen(); timestamp <= lastTimestamp; timestamp = this.timeGen()) {
        }

        return timestamp;
    }

    protected long timeGen() {
        return SystemClock.now();
    }
}

相關文章