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

2022-06-22 18:08:07

雪花演演算法(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。

最高 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();
    }
}