Automatically configuring machine numbers with SnowFlake in a cluster or development environment using Redis

Posted by joel24 on Fri, 03 Jan 2020 06:30:40 +0100

Automatically configuring machine numbers with SnowFlake in a cluster or development environment using Redis
Preface:
SnowFlake Snowflake ID algorithm is a well-known distributed ID generation algorithm introduced by Telco.With pre-allocated machine IDs, workspace IDs, machine time can generate globally unique Long Type IDs that increase over time. Length is 17-19 bits.As time progresses, the InnoDB storage engine can insert incremental primary keys more quickly in MySQL databases.Unlike UUID s, InnoDB had to do page splitting frequently, which was time consuming and fragmented.

For an introduction to the principles of SnowFlake, refer to this article: Understanding the distributed id generation algorithm SnowFlake

After understanding the basic principles of snowflakes, let's imagine how different services / different machines with the same service should differ in a distributed cluster or development environment.There are several options:

By configuring different parameters in the yml file, the spring container is started by reading the parameters to make different services different from the workerId of different machines.But it's not convenient to automatically configure new machines/colleagues here
Register IDs with third-party applications such as zookeeper and Redis to obtain unique IDs.
For development environments, you can take the last three IP positions of the machine.Because everyone in an office must not repeat the last three IP values before 0-255.However, such a machine ID requires eight Bit s, leaving only four digits for the data center.
This scheme combines the advantages of the above scheme and adjusts the number of digits occupied by the data center and machine ID in the snowflake according to the actual situation of the business: data center occupies 4Bit, ranging from 0-15.Machine ID accounts for 6Bit, ranging from 0-63
.Configure service names in YML for different services, using service numbers as data center ID s.Five different services can be deployed if differentiated by development + test + production environments.The following parameters are configured in application.yml

Distributed Snowflake ID Automatic Configuration of Different Machine IDs

snowFlake:
dataCenter: 1 #id of the data center
appName: test #business type name
Machine ID s are implemented using the following strategies:

Get the IP address localIp of the current machine, module 32, get the integer machineId of 0-31
Register with Redis using appName + dataCenter + machineId as key and native IP localIp as value.
After successful registration, set the key expiration time to 24 h ours and turn on a timer to update the registered key after 23 hours
If registration fails, there are two possible reasons:
The last time the service was abnormally interrupted, there was no time to delete the key.The solution here is to get the value through the key, and if the value and localIp are identical, the registration is still considered successful
IP has the same result as someone else's IP module 32, causing machine ID conflicts.This is to traverse 0-31 to get the number registered as the machine number of this machine
If the Redis connection fails unfortunately, the system will randomly get IDs from 32 to 63 and use log.error() to print striking prompt messages It is recommended that IDEA + Grep Console display different foreground colors for different levels of logs to facilitate timely error information retrieval.
Send a request to Redis to remove the hold of the Key before the service stops.
The code is as follows:
Automatically configure machine ID and place SnowFlake instance object at container startup
package cn.keats.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

@Configuration
@Slf4j
public class MachineIdConfig {

@Resource
private JedisPool jedisPool;

@Value("${snowFlake.dataCenter}")
private Integer dataCenterId;

@Value("${snowFlake.appName}")
private String APP_NAME;

/**
 * Machine id
 */
public static Integer machineId;
/**
 * local ip address
 */
private static String localIp;

/**
 * Get ip address
 *
 * @return
 * @throws UnknownHostException
 */
private String getIPAddress() throws UnknownHostException {
    InetAddress address = InetAddress.getLocalHost();
    return address.getHostAddress();
}

/**
 * hash Machine IP initializes a machine ID
 */
@Bean
public SnowFlake initMachineId() throws Exception {
    localIp = getIPAddress(); // 192.168.0.233

    Long ip_ = Long.parseLong(localIp.replaceAll("\\.", ""));// 1921680233
    //
    machineId = ip_.hashCode() % 32;// 0-31
    // Create a machine ID
    createMachineId();

    log.info("Initialization machine_id :{}", machineId);
    return new SnowFlake(machineId, dataCenterId);
}

/**
 * Clear registry before container is destroyed
 */
@PreDestroy
public void destroyMachineId() {
    try (Jedis jedis = jedisPool.getResource()) {
        jedis.del(APP_NAME + dataCenterId + machineId);
    }
}
/**
 * Main method: Get machine IP first and%32 get 0-31
 * Use Business Name + Group Name + IP as Redis key and Machine IP as value to store in Redis
 *
 * @return
 */
public Integer createMachineId() {
    try {
        // Register with redis and set timeout
        log.info("Register a machine ID reach Redis " + machineId + " IP:" + localIp);
        Boolean flag = registerMachine(machineId, localIp);
        // login was successful
        if (flag) {
            // Start a thread update timeout
            updateExpTimeThread();
            // Return to Machine Id
            log.info("Redis Medium Port No Conflict " + machineId + " IP:" + localIp);
            return machineId;
        }
        // Registration failed, possibly due to conflicting results from Hash%32
        if (!checkIfCanRegister()) {
            // If 0-31 is used up, use a random ID between 32-64
            getRandomMachineId();
            createMachineId();
        } else {
            // If there are remaining ID s
            log.warn("Redis Medium port conflict, use 0-31 Unoccupied between Id " + machineId + " IP:" + localIp);
            createMachineId();
        }
    } catch (Exception e) {
        // Get a random Id between 32 and 63
        // Return to Machine Id
        log.error("Redis Connection exception,Failed to register snowflake machine number correctly " + machineId + " IP:" + localIp, e);
        log.warn("Using the temporary scenario, get 32 - 63 Random number between as machine number, please check in time Redis Connect");
        getRandomMachineId();
        return machineId;
    }
    return machineId;
}

/**
 * Check to see if the registration is full
 *
 * @return
 */
private Boolean checkIfCanRegister() {
    // Determine if machine IP is full in the range 0-31
    try (Jedis jedis = jedisPool.getResource()) {
        Boolean flag = true;
        for (int i = 0; i < 32; i++) {
            flag = jedis.exists(APP_NAME + dataCenterId + i);
            // If it does not exist.Set Machine Id to this non-existent number
            if (!flag) {
                machineId = i;
                break;
            }
        }
        return !flag;
    }
}

/**
 * 1.Update timeout
 * Note, check for machine ip usage before updating
 */
private void updateExpTimeThread() {
    // Open a thread to perform a timed task:
    // Update timeout every 23 hours
    new Timer(localIp).schedule(new TimerTask() {
        @Override
        public void run() {
            // Check if the IP in the cache is consistent with the native ip, update time if consistent, and retrieve a machine id if inconsistent
            Boolean b = checkIsLocalIp(String.valueOf(machineId));
            if (b) {
                log.info("IP Consistent, update timeout ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                try (Jedis jedis = jedisPool.getResource()) {
                    jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24 );
                }
            } else {
                // IP conflict
                log.info("Rebuild Machine ID ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                // Regenerate the machine ID and change the machine ID in the snowflakes
                getRandomMachineId();
                // Regenerate and register machine id
                createMachineId();
                // Change the machine ID in the snowflakes
                SnowFlake.setWorkerId(machineId);
                // End current task
                log.info("Timer->thread->name:{}", Thread.currentThread().getName());
                this.cancel();
            }
        }
    }, 10 * 1000, 1000 * 60 * 60 * 23);
}

/**
 * Get 32-63 random numbers
 */
public void getRandomMachineId() {
    machineId = (int) (Math.random() * 31) + 31;
}
/**
 * Check if the Value of the corresponding Key in Redis is a native IP
 *
 * @param mechineId
 * @return
 */
private Boolean checkIsLocalIp(String mechineId) {
    try (Jedis jedis = jedisPool.getResource()) {
        String ip = jedis.get(APP_NAME + dataCenterId + mechineId);
        log.info("checkIsLocalIp->ip:{}", ip);
        return localIp.equals(ip);
    }
}

/**
 * 1.Register Machine
 * 2.Set timeout
 *
 * @param machineId Values from 0 to 31
 * @return
 */
private Boolean registerMachine(Integer machineId, String localIp) throws Exception {
    // try with resources, an exception releases the Java7 feature of the resource in parentheses
    try (Jedis jedis = jedisPool.getResource()) {
        // key Business Number + Data Center ID + Machine ID value Machine IP
        Long result = jedis.setnx(APP_NAME + dataCenterId + machineId, localIp);
        if(result == 1){
            // Expiration time 1 day
            jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24);
            return true;
        } else {
            // If Key exists, returns True if Value and current IP are consistent
            String value = jedis.get(APP_NAME + dataCenterId + machineId);
            if(localIp.equals(value)){
                // IP Consistent, Register Machine ID Successful
                jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24);
                return true;
            }
            return false;
        }
    }
}

}
Snowflake ID:
import org.springframework.context.annotation.Configuration;

/**

  • Function: Distributed ID Generation Tool Class
    *

*/
@Configuration
public class SnowFlake {

/**
 * Start time cut (2019-09-08) services cannot be modified once they have been run.Causes ID generation to duplicate
 */
private final long twepoch = 1567872000000L;

/**
 * Number of digits occupied by machine Id 0 - 64
 */
private final long workerIdBits = 6L;

/**
 * Number of digits occupied by workgroup Id 0 - 16
 */
private final long dataCenterIdBits = 4L;

/**
 * Maximum machine id supported, result 63 (this shift algorithm can quickly calculate the maximum decimal number represented by several binary digits)
 */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

/**
 * Maximum supported data identification id, result 15
 */
private final long maxDatacenterId = -1L ^ (-1L << dataCenterIdBits);

/**
 * The number of digits the sequence occupies in the id
 */
private final long sequenceBits = 12L;

/**
 * Machine ID moved 12 bits to the left
 */
private final long workerIdShift = sequenceBits;

/**
 * Data id moved 17 bits to the left (12+5)
 */
private final long datacenterIdShift = sequenceBits + workerIdBits;

/**
 * Time Truncation Left 22 Bits (5+5+12)
 */
private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;

/**
 * Generate sequence mask, 4095 (0b111111111111=0xfff=4095)
 */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);

/**
 * Work machine ID(0~63)
 */
private static long workerId;

/**
 * Data Center ID(0~16)
 */
private long datacenterId;

/**
 * Sequences in milliseconds (0~4095)
 */
private long sequence = 0L;

/**
 * Time Intercept of Last ID Generation
 */
private long lastTimestamp = -1L;

//==============================Constructors=====================================

/**
 * Constructor
 *
 * @param workerId     Work ID (0~63)
 * @param datacenterId Data Center ID (0~15)
 */
public SnowFlake(long workerId, long datacenterId) {
    if (workerId > maxWorkerId || workerId < 0) {
        throw new IllegalArgumentException(String.format("machine ID Must be less than %d And greater than 0", maxWorkerId));
    }
    if (datacenterId > maxDatacenterId || datacenterId < 0) {
        throw new IllegalArgumentException(String.format("Working Group ID Must be less than %d And greater than 0", maxDatacenterId));
    }
    this.workerId = workerId;
    this.datacenterId = datacenterId;
}

/**
 * Constructor
 *
 */
public SnowFlake() {
    this.workerId = 0;
    this.datacenterId = 0;
}

/**
 * Get the next ID (this method is thread safe)
 *
 * @return SnowFlakeId
 */
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 falls 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, the sequence is in milliseconds
    if (lastTimestamp == timestamp) {
        sequence = (sequence + 1) & sequenceMask;
        // Sequence overflow in milliseconds
        if (sequence == 0) {
            // Block to the next millisecond to get a new timestamp
            timestamp = tilNextMillis(lastTimestamp);
        }
    }
    //Timestamp change, sequence reset in milliseconds
    else {
        sequence = 0L;
    }

    // Time Intercept of Last ID Generation
    lastTimestamp = timestamp;

    // Shift and assemble 64-bit ID s together by or operation
    return ((timestamp - twepoch) << timestampLeftShift) //
            | (datacenterId << datacenterIdShift) //
            | (workerId << workerIdShift) //
            | sequence;
}

/**
 * Block until next millisecond until new timestamp is obtained
 *
 * @param lastTimestamp Time Intercept of Last ID Generation
 * @return Current timestamp
 */
protected long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();
    while (timestamp <= lastTimestamp) {
        timestamp = timeGen();
    }
    return timestamp;
}

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

public long getWorkerId() {
    return workerId;
}

public static void setWorkerId(long workerId) {
    SnowFlake.workerId = workerId;
}

public long getDatacenterId() {
    return datacenterId;
}

public void setDatacenterId(long datacenterId) {
    this.datacenterId = datacenterId;
}

}
Redis Configuration
public class RedisConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port:6379}")
private Integer port;

@Value("${spring.redis.password:-1}")
private String password;

@Bean
public JedisPool jedisPool() {
    // 1. Set the configuration object for the connection pool
    JedisPoolConfig config = new JedisPoolConfig();
    // Set the maximum number of connections in the pool
    config.setMaxTotal(50);
    // Set the maximum number of connections to keep in the pool when idle
    config.setMaxIdle(10);
    config.setMaxWaitMillis(3000L);
    config.setTestOnBorrow(true);
    log.info(password);
    // 2. Set up connection pool objects
    if("-1".equals(password)){
        log.info("Redis Connect without a password");
        return new JedisPool(config, host, port,0);
    } else {
        log.info("Redis Connect by password" + password);
        return new JedisPool(config, host, port,0, password);
    }
}

}
Usage method
Introducing Redis, Jedis dependencies into your project
Copy the above two classes under the project until package
application.yml Configuration Service Name, Machine Number, Redis Account, Password
Configure Jedis to have Redis connection objects in the pool at project startup
Start Project
Inject into a class that needs to generate an ID

@Autowired
private SnowFlake snowFlake;
// Production ID
snowFlake.nextId(); Method production ID

Original Address https://www.cnblogs.com/keatsCoder/p/12129279.html

Topics: Database Redis Jedis Java Spring