[Distribuido] Varios esquemas de generación y ventajas y desventajas del esquema de optimización de copos de nieve e ID únicos distribuidos

En los sistemas comerciales de Internet, están involucradas varias identificaciones, como las identificaciones de pago y las identificaciones de reembolso en los sistemas de pago. ¿Cuáles son las soluciones generales para generar identificaciones? Especialmente en escenarios de negocios de sistemas distribuidos complejos, es muy importante qué solución debemos adoptar que se adapte a nosotros. Vamos a enumerarlos uno por uno a continuación, no todos son adecuados. Estas soluciones son solo de referencia y pueden serle útiles.

1. Identificación distribuida

1. ¿Qué es la identificación distribuida?

En el desarrollo diario, necesitamos usar identificaciones únicas para varios datos en el sistema. Por ejemplo, una identificación de usuario corresponde y solo corresponde a una persona, una identificación de producto corresponde y solo corresponde a un producto, y una identificación de pedido corresponde a y solo corresponde a un pedido.

Tome la base de datos MySQL como ejemplo:
cuando la cantidad de datos comerciales no es grande, una sola base de datos y una sola tabla pueden admitir completamente el negocio existente, e incluso una información más grande puede ser manejada por una separación de lectura y escritura de sincronización maestro-esclavo de MySQL .
Sin embargo, a medida que los datos crecen día a día, la sincronización maestro-esclavo ya no se admite, por lo que la base de datos debe dividirse en bases de datos y tablas , pero después de dividir la base de datos en tablas, se requiere una identificación única para identificar un pieza de datos. La identificación de incremento automático de la base de datos obviamente no puede cumplir con los requisitos. Requisitos; los especiales, como pedidos y cupones, también deben identificarse mediante una identificación única. En este momento, es muy necesario un sistema que pueda generar una identificación única a nivel mundial. Entonces, esta identificación globalmente única se denomina identificación distribuida.

2. Características de la identificación distribuida

  • Exclusividad: asegúrese de que el ID generado sea único en toda la red.
  • Incremento ordenado: asegúrese de que el ID generado se incremente secuencialmente en un número determinado para un determinado usuario o empresa.
  • Alta disponibilidad: asegúrese de que las identificaciones se puedan generar correctamente en cualquier momento.
  • Con hora: El ID contiene la hora, y puedes saber en qué día es la transacción de un vistazo.

2. Esquema de generación de ID distribuida

1. UUID

La idea central del algoritmo es combinar la tarjeta de red de la máquina, la hora local y un número aleatorio para generar un UUID.

ventaja:

  • Implementación de código simple
  • Generado localmente, sin problemas de rendimiento, sin riesgo de alta disponibilidad
  • Único en el mundo, fácil migración de datos

defecto:

  • La longitud es demasiado larga, el almacenamiento es redundante, está desordenado e ilegible, y la eficiencia de la consulta es baja
  • Los ID generados cada vez están desordenados y no satisfacen la tendencia de aumento
  • UUID es una cadena, y es relativamente larga, ocupa mucho espacio y la eficiencia de la consulta es baja
  • ID no tiene significado, mala legibilidad

2. ID de incremento automático de la base de datos

Utilice la estrategia de incremento automático de id de la base de datos, como auto_increment de MySQL. Y puede usar las dos bases de datos para establecer diferentes longitudes de paso y generar estrategias de identificación no duplicadas para lograr una alta disponibilidad.

  • Ventajas: Los ID generados por la base de datos están absolutamente ordenados y la implementación de alta disponibilidad es simple
  • Desventajas: se requiere una implementación independiente de las instancias de la base de datos, lo cual es costoso y tiene cuellos de botella en el rendimiento.

3. Genera ID en lotes

Se generan múltiples ID en lotes a pedido a la vez. Cada generación necesita acceder a la base de datos, modificar la base de datos al valor máximo de ID y registrar el valor actual y el valor máximo en la memoria.

  • Ventajas: Evite acceder a la base de datos cada vez que se genera una identificación y ejerza presión, mejore el rendimiento
  • Desventajas: Pertenece a la estrategia de generación local, existe un único punto de falla , y el ID no es continuo por reinicio del servicio

4. Redis genera ID

Todas las operaciones de comandos de Redis son de subproceso único y proporciona comandos atómicos de incremento automático, como incr e increby, por lo que puede garantizar que el ID generado debe ser único y ordenado.

  • Ventajas: incremento ordenado, gran legibilidad, alto rendimiento . No depende de la base de datos, es flexible y conveniente, y su rendimiento es mejor que el de la base de datos; la identificación digital se ordena de forma natural, lo que es muy útil para la paginación o los resultados que deben ordenarse.
  • Desventajas: Ocupa ancho de banda y depende de Redis : si no hay Redis en el sistema, se deben introducir nuevos componentes para aumentar la complejidad del sistema ; la carga de trabajo de codificación y configuración es relativamente grande.
    Teniendo en cuenta el cuello de botella de rendimiento de un solo nodo, se puede usar un clúster de Redis para obtener un mayor rendimiento. Supongamos que hay 5 Redis en un clúster. Los valores que se pueden inicializar para cada Redis son 1, 2, 3, 4, 5 respectivamente, y luego el tamaño del paso es 5. El ID generado por cada Redis es:
A:1, 6, 11, 16, 21
B:2, 7, 12, 17, 22
C:3, 8, 13, 18, 23
D:4, 9, 14, 19, 24
E:5, 10, 15, 20, 25

Es bueno decidir en qué máquina cargar, y será difícil modificarlo en el futuro. El tamaño del paso y el valor inicial deben determinarse de antemano. El uso del clúster de Redis también puede evitar el problema del punto único de falla. Además, es más adecuado usar Redis para generar números de serie a partir de 0 todos los días. Por ejemplo, número de pedido = fecha + número de autoincremento del día. Se puede generar una clave en Redis todos los días y acumularse usando INCR.

5. Algoritmo de copo de nieve de Twitter

El copo de nieve de código abierto de Twitter se compone de ​marca de tiempo + máquina + secuencia de incremento​, la tendencia básica es creciente y el rendimiento es muy alto.

Snowflake genera un valor de tipo Long y los datos de tipo Long ocupan 8 bytes, que son 64 bits. SnowFlake divide 64, y cada parte tiene un significado diferente. Por supuesto, la cantidad de dígitos del código de la máquina y el número de serie se pueden personalizar.

Divida los 64 bits en varios segmentos y marque por separado la máquina, la hora, etc.. Por ejemplo, los 64 bits en el copo de nieve se representan respectivamente como se muestra en la siguiente figura (imagen de Internet):

  • Bit de signo (1 bit): El bit de signo reservado, que siempre es cero. (Debido a que el tipo largo está firmado en Java, el bit más alto es el bit de signo, el número positivo es 0, el número negativo es 1 y las ID utilizadas en el sistema real son generalmente números positivos, por lo que el bit más alto es 0)
  • Bit de marca de tiempo (41 bits): la cantidad de milisegundos que puede contener una marca de tiempo de 41 bits es 2 a la 41ª potencia, y la cantidad de milisegundos utilizados en un año es: 365 * 24 * 60 * 60 * 1000. A través del cálculo, se puede ver que Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);el resultado es aproximadamente igual a 69.73 años.

La época de tiempo del algoritmo de copo de nieve de ShardingSphere comienza a la medianoche del 1 de noviembre de 2016 y se puede usar hasta 2086, que se cree que cumple con los requisitos de la mayoría de los sistemas.

  • Bit de proceso de trabajo (10 bits): este indicador es único dentro del proceso de Java. Si se trata de una implementación de aplicación distribuida, se debe asegurar que la identificación de cada proceso de trabajo sea diferente. El valor predeterminado es 0 y se puede configurar a través de propiedades. Las máquinas de 10 bits pueden representar 1024 máquinas respectivamente. Si necesitamos una división IDC, también podemos dividir 10 bits en 5 bits para IDC y 5 bits para máquinas en funcionamiento. De esta forma, se pueden representar 32 IDC, y cada IDC puede tener 32 máquinas, que se pueden definir según sus propias necesidades.
  • Bit de número de secuencia (12 bits): esta secuencia se utiliza para generar diferentes ID en el mismo milisegundo. Si el número generado en este milisegundo supera los 4096 (2 elevado a 12), el generador esperará hasta el siguiente milisegundo para seguir generando. Este conteo de 12 bits admite que cada nodo genere hasta 1 << 12 = 4096 ID por milisegundo (misma máquina, mismo momento)

Ventajas: Generado localmente, no depende de middleware. La identificación distribuida generada es lo suficientemente pequeña, solo 8 bytes, y se incrementa

  • Puede cumplir con el requisito de que los ID no se repitan en un entorno de sistema distribuido de alta concurrencia
  • Alta eficiencia de generación
  • Según la marca de tiempo, se puede garantizar el incremento ordenado básico
  • No depende de bibliotecas o middleware de terceros
  • Generado idcon sincronización y singularidad.

Desventajas: problema de devolución de llamada del reloj , depende en gran medida de la hora del servidor, si se produce una devolución de llamada de tiempo, pueden aparecer identificaciones duplicadas

6. Generador de Uid de Baidu

Para obtener más información, consulte la descripción del sitio web oficial: https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

7. Hoja de Meituán

Leaf es un generador de ID distribuido de código abierto de Meituan, que puede garantizar la unicidad global, la tendencia creciente, el aumento monótono y la seguridad de la información. También menciona la comparación de varios esquemas distribuidos, pero también necesita middleware como la base de datos relacional y ZooKeeper.

https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

Para obtener más información, consulte el sitio web oficial: https://tech.meituan.com/2017/04/21/mt-leaf.html

8. Tinyid

Tinyid está desarrollado por Didi, dirección de Github: https://github.com/didi/tinyid.
Tinyid se implementa según el principio del modo de segmento numérico. Es exactamente igual que Leaf. Cada servicio obtiene un segmento numérico (1000,2000], (2000,3000], (3000,4000)

inserte la descripción de la imagen aquí

Habiendo dicho tanto, hoy hablaremos principalmente sobre el copo de nieve de código abierto de Twitter.

Tres, copo de nieve

1. Proceso

2. implementación del código java

 En el siguiente ejemplo, 41 bits son para la marca de tiempo, 5 bits para IDC, 5 bits para la máquina en funcionamiento y 12 bits para el número de serie. El código está codificado. Si es necesario ajustar dinámicamente algunos bits, se pueden definir en el miembro propiedades. El proceso de cálculo requiere algunos fundamentos de manipulación de bits.

import java.util.Date;

/**
 * @Author allen
 * @Description TODO
 * @Date 2023-07-26 9:51
 * @Version 1.0
 */
public class SnowFlakeUtil {

    private static SnowFlakeUtil snowFlakeUtil;
    static {
        snowFlakeUtil = new SnowFlakeUtil();
    }

    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    // 1650789964886:2022-04-24 16:45:59
    private static final long INIT_EPOCH = 1650789964886L;

    // 时间位取&
    private static final long TIME_BIT = 0b1111111111111111111111111111111111111111110000000000000000000000L;

    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
    private long lastTimeMillis = -1L;

    // dataCenterId占用的位数
    private static final long DATA_CENTER_ID_BITS = 5L;

    // dataCenterId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);

    // dataCenterId
    private long dataCenterId;

    // workId占用的位数
    private static final long WORKER_ID_BITS = 5L;

    // workId占用5个比特位,最大值31
    // 0000000000000000000000000000000000000000000000000000000000011111
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    // workId
    private long workerId;

    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
    private static final long SEQUENCE_BITS = 12L;

    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
    // 0000000000000000000000000000000000000000000000000000111111111111
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
    private long sequence;

    // workId位需要左移的位数 12
    private static final long WORK_ID_SHIFT = SEQUENCE_BITS;

    // dataCenterId位需要左移的位数 12+5
    private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    // 时间戳需要左移的位数 12+5+5
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;

    /**
     * 无参构造
     */
    public SnowFlakeUtil() {
        this(1, 1);
    }

    /**
     * 有参构造
     * @param dataCenterId
     * @param workerId
     */
    public SnowFlakeUtil(long dataCenterId, long workerId) {
        // 检查dataCenterId的合法值
        if (dataCenterId < 0 || dataCenterId > MAX_DATA_CENTER_ID) {
            throw new IllegalArgumentException(
                    String.format("dataCenterId 值必须大于 0 并且小于 %d", MAX_DATA_CENTER_ID));
        }
        // 检查workId的合法值
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(String.format("workId 值必须大于 0 并且小于 %d", MAX_WORKER_ID));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 获取唯一ID
     * @return
     */
    public static Long getSnowFlakeId() {
        return snowFlakeUtil.nextId();
    }

    /**
     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
     * @return 唯一id
     */
    public synchronized long nextId() {
        long currentTimeMillis = System.currentTimeMillis();
        //System.out.println(currentTimeMillis);
        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
        if (currentTimeMillis < lastTimeMillis) {
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }
        if (currentTimeMillis == lastTimeMillis) {
            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
        // 优先级:<< > |
        return
                // 时间戳部分
                ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
                        // 数据中心部分
                        | (dataCenterId << DATA_CENTER_ID_SHIFT)
                        // 机器表示部分
                        | (workerId << WORK_ID_SHIFT)
                        // 序列号部分
                        | sequence;
    }

    /**
     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
     * @param lastTimeMillis 指定毫秒时间戳
     * @return 时间戳
     */
    private long getNextMillis(long lastTimeMillis) {
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis <= lastTimeMillis) {
            currentTimeMillis = System.currentTimeMillis();
        }
        return currentTimeMillis;
    }

    /**
     * 获取随机字符串,length=13
     * @return
     */
    public static String getRandomStr() {
        return Long.toString(getSnowFlakeId(), Character.MAX_RADIX);
    }

    /**
     * 从ID中获取时间
     * @param id 由此类生成的ID
     * @return
     */
    public static Date getTimeBySnowFlakeId(long id) {
        return new Date(((TIME_BIT & id) >> 22) + INIT_EPOCH);
    }

    public static void main(String[] args) {
        SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil();
        long id = snowFlakeUtil.nextId();
        System.out.println(id);
        Date date = SnowFlakeUtil.getTimeBySnowFlakeId(id);
        System.out.println(date);
        long time = date.getTime();
        System.out.println(time);
        System.out.println(getRandomStr());

/*        System.out.println("============================");
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            long id2 = snowFlakeUtil.nextId();
            System.out.println(id2);
        }
        System.out.println(System.currentTimeMillis() - startTime);*/


    }

}

Principalmente esto: long id = snowFlakeUtil.nextId();

 Solución del problema de sincronización del reloj:

Algoritmo de copo de nieve-Implementación de Java-Un método para resolver la devolución de llamada de reloj_Algoritmo de copo de nieve Devolución de llamada de reloj_Blog de fierys-Blog de CSDN

Este código se basa en la solución de otro blogger, puede aprender de él, es solo para referencia

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

/**相较于标准算法,加入了时钟回拨解决方法,仅单机研究,仅个人思考,仅供参考
 */
public class SnowFlow {
    //因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

    //机器ID  2进制5位  32位减掉1位 31个
    private long workerId;
    //机房ID 2进制5位  32位减掉1位 31个
    private long datacenterId;
    //代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
    private long sequence;
    //设置一个时间初始值    2^41 - 1   差不多可以用69年
    private long twepoch = 1420041600000L;
    //5位的机器id
    private long workerIdBits = 5L;
    //5位的机房id;。‘
    private long datacenterIdBits = 5L;
    //每毫秒内产生的id数 2 的 12次方
    private long sequenceBits = 12L;
    // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // -1L 二进制就是1111 1111  为什么?
    // -1 左移12位就是 1111  1111 0000 0000 0000 0000
    // 异或  相同为0 ,不同为1
    // 1111  1111  0000  0000  0000  0000
    // ^
    // 1111  1111  1111  1111  1111  1111
    // 0000 0000 1111 1111 1111 1111 换算成10进制就是4095
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
    //记录产生时间毫秒数,判断是否是同1毫秒
    private long lastTimestamp = -1L;
    public long getWorkerId(){
        return workerId;
    }
    public long getDatacenterId() {
        return datacenterId;
    }
    public long getTimestamp() {
        return System.currentTimeMillis();
    }

    //是否发生了时钟回拨
    private boolean isBackwordsFlag = false;
    //是否是第一次发生时钟回拨, 用于设置时钟回拨时间点
    private boolean isFirstBackwordsFlag = true;
    //记录时钟回拨发生时间点, 用于判断回拨后的时间达到回拨时间点时, 跳过 已经用过的 时钟回拨发生时间点 之后的时间 到 未来时间的当前时间点
    private long backBaseTimestamp = -1L;

    public SnowFlow() {
    }

    public SnowFlow(long workerId, long datacenterId, long sequence) {

        // 检查机房id和机器id是否超过31 不能小于0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(
                    String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }

        if (datacenterId > maxDatacenterId || datacenterId < 0) {

            throw new IllegalArgumentException(
                    String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    // 这个是核心方法,通过调用nextId()方法,
    // 让当前这台机器上的snowflake算法程序生成一个全局唯一的id
    public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();

        //--20220813--1---------------------------------------
        if (isBackwordsFlag) {
            //当回拨时间再次叨叨回拨时间点时, 跳过回拨这段时间里已经使用了的未来时间
            if (timestamp >= backBaseTimestamp && timestamp < lastTimestamp) {
                //直接将当前时间设置为最新的未来时间
                timestamp = lastTimestamp;
            } else if(timestamp > lastTimestamp) {
                //当前时间已经大于上次时间, 重置时钟回拨标志
                isBackwordsFlag = false;
                isFirstBackwordsFlag = true;
                System.out.println("时间已恢复正常-->" + timestamp);
            } else {
                // timestamp == lastTimestamp 等于的情况在后面
            }
        }
        //--20220813--1----------------------------------------

        // 判断是否小于上次时间戳,如果小于的话,就抛出异常
        if (timestamp < lastTimestamp) {

            System.err.printf("lastTimestamp=%d, timestamp=%d, l-t=%d \n", lastTimestamp, timestamp, lastTimestamp - timestamp);
//            throw new RuntimeException(
//                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
//                            lastTimestamp - timestamp));

            //--20220813--2---------------------------------------
            //这里不再抛出异常, 改为记录时钟回拨发生时间点

            //发生时钟回拨后, 当前时间 timestamp 就变成了 过去的时间
            //此时将 timestamp 设置为 上一次时间, 即相对于当前时间的未来时间
            timestamp = lastTimestamp;
            isBackwordsFlag = true;

            //记录时钟回拨发生的时间点, 后续需要跳过已经使用的未来时间段
            if (isFirstBackwordsFlag) {
                backBaseTimestamp = timestamp;
                isFirstBackwordsFlag = false;
                System.out.println("时钟回拨已发生-->" + backBaseTimestamp);
            }
            //--20220813--2---------------------------------------
        }

        // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
        // 这个时候就得把seqence序号给递增1,最多就是4096
        if (timestamp == lastTimestamp) {

            // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
            //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
            sequence = (sequence + 1) & sequenceMask;
            //当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
            if (sequence == 0) {
                //timestamp = tilNextMillis(lastTimestamp);

                //--20220813--3---------------------------------------
                //这里也不能阻塞了, 因为阻塞方法中需要用到当前时间, 改为将此时代表未来的时间 加 1
                if (isBackwordsFlag) {
                    lastTimestamp++;

                    //根据博友评论反馈, 这里可能需要重新赋值, 如果有人看到这个, 可以验证
                    //timestamp = lastTimestamp++;

                } else {
                    timestamp = tilNextMillis(lastTimestamp);
                }
                //--20220813--3---------------------------------------
            }

        } else {
            //sequence = 0;
            //每毫秒的序列号都从0开始的话,会导致没有竞争情况返回的都是偶数。解决方法是用时间戳&1,这样就会随机得到1或者0。
            sequence = timestamp & 1;
        }
        // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        //lastTimestamp = timestamp;

        //--20220813--4---------------------------------------
        if(isBackwordsFlag) {
            //什么都不做
        } else {
            lastTimestamp = timestamp;
        }
        //--20220813--4---------------------------------------

        // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
        // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
        // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
        long sn = ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) | sequence;

        if (isBackwordsFlag) {
            System.out.printf("sn=%d\n", sn);
        }
        return sn;
    }

    /**
     * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {

        long timestamp = timeGen();

        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    //获取当前时间戳
    private long timeGen(){
        return System.currentTimeMillis();
    }

    /**
     *  main 测试类
     * @param args
     */
    public static void main(String[] args) throws IOException, InterruptedException {

        SnowFlow snowFlow = new SnowFlow(1, 1, 1);
        int count = 10000000;
        //int count = 100;
        for (int i = 0; i < count; i++) {
            //实际测试发现遍历太快输出日志过多导致卡顿, 增加睡眠时间, 或输出到文件
            snowFlow.nextId();

            //Thread.sleep(100);

//            System.out.println(snowFlow.nextId());

//            if (i == 1000) {
            //不具有管理员权限的用户, 修改不成功
            //testClockMvoedBackwords(30);
//            }
            //改为 手动修改,  右键cmd,以管理员权限打开后,使用time命令即可, time 16:15:00
        }


        System.out.println(System.currentTimeMillis());
        /**
         * 这里为什么特意输出一个开始时间呢, 其实就是一个运行了两年的程序突然有一天出bug了,导致了严重的生产事件!
         * 那么时间初始化影响什么呢, 答案是 序列的长度
         * 有人就说了, 这个一般是作为 主键用的, 长度貌似影响不大, 确实是这样的
         * 这次的bug不是雪花算法本身的问题, 而是程序里面有个功能是严格长度截取的, 并且只考虑了长度不够的情况, 没有考虑到变长的情况
         * 最根本的原因是 本人截取的时候 序列的长度一直是18位, 然后截取9位的代码是这么写的 substring(9);
         * 当未来的某一天序列长度增加到了19位,那么这个截取就会返回10位长度, 最终导致一个大范围的交易失败......
         * 锅当然是本人背, 这里提出这种情况, 供大家参考.
         * 经过仔细研究所谓的序列可以使用69年, 序列的长度变化是这样的, 假设以当前时间为初始化值
         * 12 13 14 15 16 17 18(约7年) 19(约58年)
         * 长度随时间增加, 长度越长, 保持相同长度的时间越长
         */
        DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String dateString = "2015-01-01 00:00:00";
        LocalDateTime localDateTime = LocalDateTime.parse(dateString,dtf2);
        System.out.println(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());

    }

    //windows os 模拟时钟回拨, 将当前时间减去几秒
    private static void testClockMvoedBackwords(long seconds) throws IOException {
        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));

        LocalDateTime localDateTime = LocalDateTime.now();
        String backTime = localDateTime.minusSeconds(seconds).format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        System.out.println(backTime);

        if (System.getProperty("os.name").contains("Windows")) {
            String cmd = "cmd /c start time 15:41:56";// + backTime;
            //不具有管理员权限的用户, 修改不生效, 提示 客户端无所需特权
            Runtime.getRuntime().exec(cmd);
//            Runtime.getRuntime().exec("cmd /c notepad");
            System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
        }
    }
}

3. La solución al problema de devolución de llamada del reloj del plan de producción de Snowflake

El primer método es desactivar la sincronización del reloj para evitar problemas de sincronización del reloj, pero esto no es realista, porque los sistemas que dependen en gran medida del tiempo generalmente tienen que sincronizar el reloj para evitar errores de tiempo graves, implementar algunas cosas en máquinas virtuales, jugar después la máquina virtual hiberna y se reanuda nuevamente, la hora en la máquina virtual y la hora del host a menudo no están sincronizadas, lo que provoca que algunos sistemas distribuidos de datos grandes se bloqueen, y la comunicación entre los nodos dependerá de las marcas de tiempo para la comparación y el latido del corazón Lento, hará que el nodo se cuelgue


El segundo método es registrar la hora en que se generó por última vez el ID. Si se encuentra que la marca de tiempo es más pequeña que la última marca de tiempo cuando se generó el ID esta vez, significa que el reloj se ha retrocedido. En este tiempo, la generación de ID no está permitida dentro de este tiempo. Espere, espere hasta que el tiempo actual alcance el tiempo de última generación. El problema es, ¿qué pasa si el tiempo de devolución de llamada es demasiado? Puede llevar mucho tiempo, lo que afecta la disponibilidad del sistema, por lo que no es una forma particularmente buena de almacenar la marca de tiempo de la última ID única generada en la memoria. Cuando se vuelve a marcar el reloj, se marcará la marca de tiempo actual. volver a antes de la última marca de tiempo. La solicitud llega, para generar una identificación única, no le devuelve directamente una identificación, primero hace una comparación, si encuentra que la marca de tiempo actual se compara con la marca de tiempo de la última identificación única generada , es más pequeño que él, y el reloj se vuelve a llamar, siempre que genere una identificación, es posible que si la disponibilidad repetida de la identificación es tan pobre, si el servicio comercial de alguien quiere generar datos de facturación en este momento y solicita una identificación, en este momento finalmente esperó cientos de milisegundos, también le dice que tiene una excepción interna y no puede obtener la identificación única. Los reintentos repetidos afectarán el funcionamiento de sus servicios comerciales.
 

El tercer método es para la optimización del segundo método. Si encuentra que la devolución de llamada del reloj es demasiado dura, como exceder 1 minuto, llamará a la policía directamente en este momento y, al mismo tiempo, ya no proporcionará servicios externos. y elimínese del clúster, por ejemplo, si se registra en función del centro de registro de microservicios, debe tomar la iniciativa de desconectarse. Cuando descubra que la marca de tiempo actual es más pequeña que la marca de tiempo del último ID generado, y encuentre que el reloj se ha vuelto a marcar, juzgue cuántos milisegundos se han vuelto a marcar. Por ejemplo, si el tiempo de devolución de llamada está dentro de los 500 ms, puede colgar la solicitud en este momento y esperar 500 ms. Después de 500 ms, la marca de tiempo actual es mayor que la marca de tiempo de la última ID generada. En este momento, normalmente puede
generar una ID única y devolverla al lado comercial. , para el lado comercial, solo en el caso de algunas devoluciones de llamada de reloj, la solicitud generalmente solo toma 50 ms, 500 ms, que todavía está dentro del rango aceptable, por lo que todavía es posible, pero la solicitud es más lenta. Si se
encuentra que su marca de tiempo actual se compara con la marca de tiempo de la última ID única generada. Cuando la compare, verá encuentre que excede los 500 ms, pero dentro de los 5 segundos, puede devolver un estado anormal + continuación anormal Déle tiempo al cliente, no diga que hay un problema, puede notificarle que vuelva a intentar el mecanismo de reintento
por sí mismo, es mejor no hacerlo deje que el lado comercial lo haga usted mismo, puede encapsular completamente un cliente de su servicio de generación de ID único, basado en la solicitud de RPC Su interfaz, pero encapsula un mecanismo de reintento automático en su propio cliente.Una vez que descubre que un servidor devuelve una respuesta que dice que no puede proporcionar el servicio en poco tiempo, automáticamente solicitará el servicio en otras máquinas para obtener una identificación única
. siempre afectará la disponibilidad del sistema, y ​​no es particularmente bueno para la detección de devolución de llamada del reloj en el
lado del servidor Mecanismo + El cliente se encapsula
en 1s: la solicitud de bloqueo espera, y el período de tiempo de espera del cliente también debe ser 1s, lo que expone el número de serie más grande de la identificación única generada cada milisegundo dentro de 1 s, y ubica la identificación generada previamente de acuerdo con los milisegundos de la marca de tiempo actual El número de secuencia de identificación máxima de este milisegundo, continúa generando la identificación en este momento e incrementa directamente en la base del número de secuencia de ID máximo de este milisegundo generado antes Después de la optimización, se puede garantizar que no hay necesidad de bloquear y esperar
Entre 1s y 10s: devuelve el código de excepción y la duración de la excepción, el cliente no solicita esta máquina dentro del tiempo especificado por
más de 10s: devuelve el código de falla, solicita al centro de registro del servicio que se deje desconectar, después de la el cliente recibe el código de falla, simplemente eliminará esta máquina de la lista de máquinas de servicio y no lo solicitará más. Luego, cuando el servicio de identificación se implementó en esa máquina, descubre que su tiempo puede haber pasado unos segundos. Después de ralentizarse, recuperarse y estar disponible, se puede hacer. Vuelva a registrar el servicio. Cuando su cliente actualice la lista de registro del servicio, lo encontrará. En este momento, puede volver a solicitarlo.
 

El cuarto método es mantener el valor de ID generado en los últimos segundos en la memoria. Generalmente, la devolución de llamada del reloj es de decenas de milisegundos a cientos de milisegundos, y rara vez supera los segundos, por lo que es suficiente para guardar los últimos segundos, y luego, si se produce la devolución de llamada del reloj, verifique en qué milisegundo está la devolución de llamada en este momento, porque la marca de tiempo está en el nivel de milisegundos, y luego mire ese milisegundo y
continúe generando el número de serie de ID que se produjo en ese milisegundo. milisegundo es seguido por analogía, por lo que el problema de la repetición se puede evitar perfectamente, y no hay necesidad de esperar, pero
aquí también hay un mecanismo de abajo hacia arriba, es decir, si mantiene la ID generada cada milisegundo en el último 10 s, luego, en caso de que la devolución de llamada del reloj ocurra ¿Qué pasa con más de 10 s? En este momento, la probabilidad es muy baja. Puede combinar los dos o tres esquemas y establecer varios umbrales. Por ejemplo, si mantiene la ID de los últimos 10, puede asegurarse de que no habrá repeticiones ni pausas dentro de los 10. de la devolución de llamada; si supera los 10 s, dentro de los 60 s, puede haber un proceso de espera para permitirle avanzar al rango de 10 s que reservó antes; si la devolución de llamada supera los 60 s, la marca de tiempo de la última ID única generada directamente
fuera de línea El número de serie de ID máximo por milisegundo se ha ido. Después de reiniciar, se produce la devolución de llamada de tiempo y no se puede encontrar el problema de devolución de llamada de tiempo. En segundo lugar, no hay forma de continuar generando ID únicos que no se repiten de acuerdo con el pensamiento anterior.

4. Optimización de devolución de llamada de reloj

1. Por lo general, necesitamos activar la función de sincronización del reloj, de modo que la ID se pueda maximizar para garantizar que se ordene de acuerdo con el tiempo, pero después de activar la sincronización del reloj, el reloj se puede volver a marcar. se vuelve a marcar, la ID generada se repetirá, por lo que generalmente activamos la sincronización del reloj y apagamos la función de devolución de llamada del reloj,
2. La cantidad de dígitos en el número de serie es limitada y la cantidad de ID que se pueden representar es limitado. Cuando el reloj está sincronizado, si un servidor es mucho más rápido, incluso si el reloj está apagado. Vuelva a llamar, pero antes de que se agote el tiempo, es posible que la ID se haya agotado. Cuando se agota el número de serie autoincremental. , podemos hacer el siguiente trabajo: detener el servicio de generación de ID y dar una alarma, esperar si la devolución de llamada del reloj es inferior a cierto umbral, como Si es mayor que cierto umbral, un componente de terceros como ZK lo hará ser usado para regenerar un ID de trabajador o tomar prestada la ID de la siguiente marca de tiempo de la marca de tiempo autoincremental 3. Después de
reiniciar el servicio, la ID puede repetirse Por esta razón, generalmente necesitamos guardar la marca de tiempo regularmente y reiniciarla La marca de tiempo final debe ser mayor que la marca de tiempo guardada + varias veces el intervalo de guardado (como 3 veces), por qué varias veces, principalmente considerando la pérdida de datos, pero si se guarda en el disco duro local y fsync cada vez, en esta vez, 1 veces es suficiente. Después de reiniciar, si es más pequeño que el segundo punto, se puede manejar de manera similar al segundo punto
4. Si el QPS de la ID solicitada no es alto, como uno por milisegundo, entonces el número de cola de la ID obtenida cada time es 0, y luego la base de datos y la tabla se dividen en función de la ID. Tal vez la distribución de datos sea desigual. En este momento, podemos aumentar el intervalo de tiempo de la marca de tiempo o el número de serie aumentará automáticamente a partir de un valor aleatorio cada tiempo.

Es principalmente el método de generación de identificación. Lo optimizaré nuevamente. Esto se optimiza aún más de acuerdo con la hoja de Meituan. Consulte

    /**
     * 通过雪花算法生成下一个id,注意这里使用synchronized同步
     * @return 唯一id
     */
    public synchronized long nextId() {
        long currentTimeMillis = System.currentTimeMillis();
        //System.out.println(currentTimeMillis);
        // 当前时间小于上一次生成id使用的时间,可能出现服务器时钟回拨问题
        //long timestamp = timeGen();
        if (currentTimeMillis < lastTimeMillis) {
            long offset = lastTimeMillis - currentTimeMillis;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    currentTimeMillis = timeGen();
                    if (currentTimeMillis < lastTimeMillis) {
                        throw new RuntimeException("id生成失败");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException("生成id时出现错误");
                }
            } else {
                throw new RuntimeException(
                        String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                                lastTimeMillis));
            }
        }


        /*if (currentTimeMillis < lastTimeMillis) {
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }*/
        if (currentTimeMillis == lastTimeMillis) {
            // 还是在同一毫秒内,则将序列号递增1,序列号最大值为4095
            // 序列号的最大值是4095,使用掩码(最低12位为1,高位都为0)进行位与运行后如果值为0,则自增后的序列号超过了4095
            // 那么就使用新的时间戳
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 核心算法,将不同部分的数值移动到指定的位置,然后进行或运行
        // <<:左移运算符, 1 << 2 即将二进制的 1 扩大 2^2 倍
        // |:位或运算符, 是把某两个数中, 只要其中一个的某一位为1, 则结果的该位就为1
        // 优先级:<< > |
        return
                // 时间戳部分
                ((currentTimeMillis - INIT_EPOCH) << TIMESTAMP_SHIFT)
                        // 数据中心部分
                        | (dataCenterId << DATA_CENTER_ID_SHIFT)
                        // 机器表示部分
                        | (workerId << WORK_ID_SHIFT)
                        // 序列号部分
                        | sequence;
    }

[Distribuido] 8 esquemas de generación para identificaciones únicas distribuidas

Explicación detallada e implementación del algoritmo Snowflake Snowflake: se busca programador

Algoritmo de copo de nieve (SnowFlake)

Identificación única distribuida (algoritmo de copo de nieve), Principio + Comparación + Esquema - Libro breve

Explicación detallada del principio de generación de id distribuido de copo de nieve del algoritmo de copo de nieve y discusiones sobre varias soluciones para resolver el problema de devolución de llamada del reloj_51CTO blog_id distribuido de copo de nieve

Leaf——Meituan Dianping Distributed ID Generation System- Equipo técnico de Meituan

 https://github.com/Meituan-Dianping/Leaf/tree/master/leaf-core

Plan de producción de copos de nieve ideas para resolver problemas de devolución de llamada del reloj_Error de inicio solución de devolución de llamada del reloj del copo de nieve_Blog inferior

Supongo que te gusta

Origin blog.csdn.net/Alex_81D/article/details/131924058
Recomendado
Clasificación