Implementation of distributed globally unique ID generator (supporting multiple registries)

Keywords: Zookeeper Apache Lombok Java

In the business scenarios of order and payment, the generation rules and methods of DOC No. are very important. There are many kinds of implementation. The simplest one is based on mysql auto add primary key. The advantages and disadvantages of the scheme are not much discussed, and everyone knows. Today, we implement a distributed, scalable and high-performance global unique ID generation scheme (adapted and extended based on the snowflake principle of twitter). Don't talk about code directly.

package com.zxm.adapter;

import org.apache.zookeeper.KeeperException;

/**
 * @Author zxm
 * @Description Register adapter
 * @Date Create in 9:45 am April 12, 2019 0012
 */
public interface RegistryAdapter {
    long getWorkerId() throws KeeperException, InterruptedException;
}
package com.zxm.adapter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.*;

import java.io.IOException;

/**
 * @Author zxm
 * @Description Registration adapter based on zk
 * @Date Create in 9:44 am April 12, 2019 0012
 */
@Slf4j
public class ZkRegistryAdapter implements RegistryAdapter {
    private static final int DEFAULT_SESSION_TIMEOUT = 3000;
    private static final String ROOT_NODE = "/idWorker";

    private static final int DEFAULT_MOD_VALUE = 1024;
    private ZooKeeper zkClient;

    public ZkRegistryAdapter(String connectString) throws Exception {
        this(connectString, DEFAULT_SESSION_TIMEOUT);
    }

    public ZkRegistryAdapter(String connectString, int sessionTimeOut) throws Exception {
        try {
            zkClient = new ZooKeeper(connectString, sessionTimeOut, watchedEvent -> log.info("path:{}, state:{}", watchedEvent.getPath(), watchedEvent.getState()));
            initRootNode(zkClient);
        } catch (IOException e) {
            log.error("zookeeper connect error,url:{},errorMsg:{}", connectString, e.getMessage());
            throw e;
        }
    }

    private void initRootNode(ZooKeeper zkClient) throws KeeperException, InterruptedException {
        if (zkClient.exists(ROOT_NODE, false) == null) {
            String path = zkClient.create(ROOT_NODE, "idWorker".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            if (StringUtils.isNotBlank(path)) {
                log.info("root node init success,path:{}", path);
            }
        }
    }

    @Override
    public long getWorkerId() throws KeeperException, InterruptedException {
        String path = zkClient.create(ROOT_NODE + "/_", "idWorker".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        if (StringUtils.isNotBlank(path)) {
            log.info("node create success,path:{}", path);
            return Long.valueOf(path.substring(ROOT_NODE.length() + 2, path.length())) % DEFAULT_MOD_VALUE;
        }
        return -1;
    }
}
package com.zxm.core;

import com.zxm.adapter.RegistryAdapter;
import lombok.extern.slf4j.Slf4j;

/**
 * Id_Worker<br>
 * Id_Worker Its structure is as follows (each part is separated by using): < br >
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 0000000000 - 0000000 <br>
 * 1 Bit id: since the long basic type is signed in Java, the highest bit is the sign bit, the positive number is 0, and the negative number is 1, so the id is generally a positive number, and the highest bit is 0 < br >
 * 41 Bit time section (millisecond level), note that 41 bit time section is not the time section of the current time, but the difference of the storage time section (current time section start time section)
 * The start time section here is generally the start time of our id generator, which is specified by our program (as follows: the startTime property of IdWorker class of the program below).
 * 41 It can be used for 69 years. Year t = (1L < 41) / (1000L * 60 * 60 * 24 * 365) = 69 < br >
 * 10 Machine bit of bit (up to 1023 machines supported) < br >
 * 12 Bit sequence, counting within milliseconds, 7-bit counting sequence number supports each node to generate 4096 ID sequence numbers per millisecond (the same machine, the same time cut) < br >
 * Add up to just 64 bits, a Long type. <br>
 */
@Slf4j
public class IdWorker {

    /**
     * Start time (2018-01-01 00:00:00)
     */
    private static final long twepoch = 1514736000000L;

    /**
     * Number of digits occupied by machine id
     */
    private static final long workerIdBits = 10L;

    /**
     * Number of digits of sequence in id
     */
    private static final long sequenceBits = 12L;


    private static final long workerIdShift = sequenceBits;

    /**
     * Time cut shift 22 bits to the left (12 + 10)
     */
    private static final long timestampLeftShift = sequenceBits + workerIdBits;

    /**
     * Mask to generate sequence, 4095 here
     */
    private static final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * Working machine ID(0~1023)
     */
    private static long workerId;

    /**
     * Sequence in milliseconds (0-4095)
     */
    private static long sequence = 0L;

    /**
     * Last ID generated by
     */
    private static long lastTimestamp = -1L;

    public IdWorker(RegistryAdapter registryAdapter) throws Exception {
        if (null == registryAdapter) {
            throw new Exception("registryAdapter init fail");
        }
        workerId = registryAdapter.getWorkerId();
        log.info("GLOBAL_WORkER_ID INIT:" + workerId);
    }

    /**
     * Get the next ID (this method is thread safe)
     *
     * @return id
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //If the current time is less than the time stamp generated by the last ID, an exception should be thrown when the system clock goes back
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //If generated at the same time, sequence in milliseconds
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //Sequence overflow in MS
            if (sequence == 0) {
                //Block to next MS, get new timestamp
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //Timestamp change, sequence reset in MS
        else {
            sequence = 0L;
        }

        //Last ID generated by
        lastTimestamp = timestamp;
        //Shift and combine by or operation to form a 64 bit ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift)
                | exchangeSequence(sequence);
    }

    /**
     * Blocks to the next millisecond until a new timestamp is obtained
     *
     * @param lastTimestamp Last ID generated by
     * @return Current timestamp
     */
    protected static long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * Returns the current time in milliseconds
     *
     * @return Current time (MS)
     */
    protected static long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * Scrambling auto increasing sequence
     *
     * @param sequence
     * @return
     * @since
     */
    protected static long exchangeSequence(long sequence) {
        String tmp = Long.toBinaryString(sequence | (1 << 12));
        StringBuffer sb = new StringBuffer(tmp.substring(1));
        long sqr = Long.parseLong(sb.reverse().toString(), 2) & sequenceMask;
        return sqr;
    }
}

Test generation results:

package com.zxm;

import com.zxm.adapter.RegistryAdapter;
import com.zxm.adapter.ZkRegistryAdapter;
import com.zxm.core.IdWorker;

/**
 * Hello world!
 */
public class App {
    public static void main(String[] args) throws Exception {
        RegistryAdapter registryAdapter = new ZkRegistryAdapter("10.10.4.17:2181", 3000);
        Thread.sleep(10 * 1000);
        IdWorker idWorker = new IdWorker(registryAdapter);

        for (int i = 0; i < 10; i++) {
            System.out.println(idWorker.nextId());
        }
    }
}

When used in the spring project, the configuration is as follows:

@Configuration  
 public class IdWorkerConfig {  
     @Bean
     public RegistryAdapter registryAdapter() throws Exception{
         return new ZkRegistryAdapter("10.10.4.4:2181", 3000);
     }
 
     @Bean
     public IdWorker idWorker() throws Exception{
         return new IdWorker(registryAdapter());
     }
 }  

Test case:

public class IdWorkerTest{
    @Autowire
    private IdWorker idWorker;
    
    @Test
    public void test(){
        for (int i = 0; i < 10; i++) {
            System.out.println(idWorker.nextId());
        }
    }
}

The results of implementation are the same as above.

Project address: https://github.com/zhangxiaomin1993/id-worker

The project will gradually improve, the code is only for your reference!

Posted by lentin64 on Fri, 29 Nov 2019 12:02:28 -0800