[Spring Cloud Series] Principio e implementación del algoritmo Snowflake

[Spring Cloud Series] Principio e implementación del algoritmo Snowflake

I. Descripción general

En un entorno distribuido de alta concurrencia, una reserva de boletos común es el feriado 12306. Cuando una gran cantidad de usuarios se apresuran a comprar boletos en la misma dirección, se pueden generar decenas de miles de pedidos en milisegundos. Garantizar la unicidad del ID del pedido generado es crucial. En este entorno de venta flash, no solo se debe garantizar la unicidad de la identificación, sino que también se debe garantizar la prioridad de generación de la identificación.

2. Parte de los rígidos requisitos para generar reglas de identificación.

  1. Único global : No pueden aparecer números de identificación duplicados. Dado que es un identificador único, este es el requisito más básico.
  2. Tendencia creciente : Los índices agrupados son aplicables en el motor InnoDB de MySQL. Dado que la mayoría de los RDBMS utilizan la estructura de datos B + Tree para almacenar datos de índice, en la selección de claves primarias, intentamos utilizar claves primarias ordenadas para garantizar el rendimiento de escritura.
  3. Aumento monótono : asegúrese de que la siguiente ID sea mayor que la ID anterior, como el número de versión de la transacción, la clasificación y otros requisitos especiales.
  4. Seguridad de la información : si el ID es continuo, será muy fácil para los usuarios malintencionados capturarlo, simplemente pueden descargar la URL especificada en orden, si es un número de pedido, será peligroso.
  5. Contiene marca de tiempo : el ID generado contiene información completa de la marca de tiempo.

3. Requisitos de disponibilidad del sistema de generación de números de identificación

  1. Alta disponibilidad : envíe una solicitud para obtener una ID distribuida y se garantiza que el servidor creará una ID distribuida única para mí en el 99,9999% de los casos.
  2. Baja latencia : para enviar una solicitud para obtener una ID distribuida, el servidor debe ser rápido y extremadamente rápido.
  3. QPS alto : si se solicitan 100.000 ID distribuidas a la vez, el servidor debe soportar y crear con éxito 100.000 ID distribuidas.

4. Solución general para la identificación distribuida

4.1 UUID

La forma estándar de UUID (Identificador único universal) contiene 32 dígitos hexadecimales, divididos en cinco segmentos con guiones, en la forma: 36 caracteres de 8-4-4-4-12, ejemplo: 1E785B2B-111C-752A-997B -3346E7495CE2; el rendimiento del UUID es muy alto, no depende de la red y se genera localmente.

Desventajas del UUID:

  1. Desordenado, es imposible predecir el orden en que se genera y no puede generar números en orden creciente. MySql recomienda oficialmente que la clave principal sea lo más corta posible. UUID es una cadena de 32 bits, por lo que no se recomienda.

  2. Índice, división del índice B+Tree

    La ID distribuida es la clave principal y la clave principal es el índice agrupado. El índice de Mysql es implementado por B+Tree. Cada vez que se insertan nuevos datos UUID, con el fin de insertar nuevos datos UUID y para optimizar la consulta, el B+Tree en la parte inferior del índice se modificará; porque los datos UUID son desordenado, por lo que cada inserción de datos UUID modificará en gran medida el índice agrupado de la clave primaria. Al realizar la inserción de datos, la clave primaria se insertará desordenadamente, lo que provocará la división de algunos nodos intermedios y provocará una gran cantidad de nodo insaturado. Esto reduce en gran medida el rendimiento de la inserción de la base de datos.

4.2 Clave primaria de incremento automático de la base de datos

Ser único

En los sistemas distribuidos, el principio fundamental del mecanismo de ID de aumento automático de la base de datos es: el ID de aumento automático de la base de datos se implementa reemplazando la base de datos MySql.

El significado de Reemplazar en es insertar un registro. Si el valor del índice único en la tabla encuentra un conflicto, los datos antiguos serán reemplazados.

En una aplicación única, se utiliza un ID de aumento automático, pero en una aplicación distribuida en clúster, la aplicación única no es adecuada.

  1. Es difícil expandir el sistema horizontalmente. Por ejemplo, después de definir el tamaño del paso de crecimiento y la cantidad de máquinas, al agregar una gran cantidad de servidores, es necesario restablecer los valores iniciales. Esto tiene poca operatividad, por lo que el sistema La solución de expansión horizontal es muy compleja y difícil de implementar.
  2. La base de datos está bajo una gran presión. Cada vez que obtiene la identificación, necesita leer y escribir la base de datos una vez, lo que afecta en gran medida el rendimiento. No cumple con las reglas de baja latencia y alto QPS en la identificación distribuida (bajo alta concurrencia, si vas a la base de datos para obtener el ID, se verá muy afectado).

4.3 Generar una estrategia de identificación global basada en Redis

En el caso del clúster Redis, se deben configurar diferentes pasos de crecimiento como MySql y la clave debe tener un período de validez. El clúster de Redis se puede utilizar para obtener un mayor rendimiento.

5. SnowFlake (algoritmo de copo de nieve)

SnowFlake de Twitter resolvió esta necesidad. Inicialmente, Twitter migró el sistema de almacenamiento de MySQL a Cassandra (un sistema de base de datos NoSQL distribuido de código abierto desarrollado por Facebook). Debido a que Cassandra no tenía un mecanismo de generación de ID secuencial, desarrolló un conjunto de ID globalmente únicos. Generar servicios. SnowFlake puede generar 260.000 ID ordenables de aumento automático por segundo.

5.1 Funciones del copo de nieve

  1. Los ID generados por SnowFlake de Twitter se pueden generar en orden temporal.
  2. El resultado del Id generado por el algoritmo SnowFlake es un entero de 64 bits, que es de tipo Long (la longitud máxima después de la conversión a una cadena es 19).
  3. No habrá colisiones de ID en el sistema distribuido (distinguido por centro de datos e ID de trabajador) y la eficiencia es alta.

5.2 Estructura del copo de nieve

Insertar descripción de la imagen aquí

5.3 Principio del algoritmo del copo de nieve

El principio del algoritmo del copo de nieve es generar una identificación única de tipo largo de 64 bits.

  1. El 1 bit más alto tiene un valor fijo de 0, porque la ID generada es un número entero positivo y si es 1, es un valor negativo.
  2. Seguido de 41 bits para almacenar marcas de tiempo de milisegundos, 2^41/(1000 * 60 * 24 * 365) = 69, que se pueden usar durante aproximadamente 69 años.
  3. Los siguientes 10 dígitos almacenan el código de la máquina, incluido el DataCenterId de 5 dígitos y el WorkerId de 5 dígitos. Se pueden implementar hasta 2^10=1024 máquinas.
  4. Los últimos 12 bits almacenan el número de secuencia. Cuando se utiliza la misma marca de tiempo de milisegundos, se distingue por este número de secuencia incremental. Es decir, para la misma máquina, bajo la misma marca de tiempo de milisegundos, se pueden almacenar 2^12=4096 ID únicos. generado.

El algoritmo Snowflake se puede implementar como un servicio independiente y luego, para los sistemas que requieren una ID única a nivel mundial, simplemente solicite el servicio del algoritmo Snowflake para obtener la ID.

Para cada servicio del algoritmo Snowflake, primero debe especificar un código de máquina de 10 dígitos, que puede configurar según su propio negocio. Por ejemplo, número de sala de ordenadores + número de máquina, número de máquina + número de servicio u otros números enteros de 10 dígitos que distingan el identificador.

5.4 Implementación del algoritmo

package com.goyeer;
import java.util.Date;

/**
 * @ClassName: SnowFlakeUtil
 * @Author: goyeer
 * @Date: 2023/09/09 19:34
 * @Description:
 */
public class SnowFlakeUtil {
    
    

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

    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    //
    private static final long INIT_EPOCH = 1694263918335L;

    // 时间位取&
    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());
    }

    /**
     * 从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());

    }

}

5.4 Ventajas del algoritmo de copo de nieve

  1. Genere identificaciones únicas en un entorno distribuido de alta concurrencia y puede generar millones de identificaciones únicas por segundo.
  2. Según la marca de tiempo y el incremento automático del número de secuencia bajo la misma marca de tiempo, básicamente se garantiza que la identificación aumentará de manera ordenada.
  3. No depende de bibliotecas ni middleware de terceros.
  4. El algoritmo es simple, se realiza en memoria y altamente eficiente.

5.5 Desventajas del algoritmo de copo de nieve:

  1. Dependiendo de la hora del servidor, es posible que se generen identificaciones duplicadas cuando se retrasa el reloj del servidor. El algoritmo se puede resolver registrando la marca de tiempo cuando se generó la última ID. Antes de cada generación de ID, compare si el reloj actual del servidor se ha retrocedido para evitar generar ID duplicadas.

6. Resumen

De hecho, el número de bits ocupados por cada parte del algoritmo del copo de nieve no es fijo. Por ejemplo, es posible que su empresa no tenga 69 años, por lo que puede reducir la cantidad de bits ocupados por la marca de tiempo. Si el servicio del algoritmo Snowflake necesita implementar más de 1024 nodos, puede usar la cantidad reducida de bits para el código de máquina.

Tenga en cuenta que los 41 bits del algoritmo Snowflake no se utilizan directamente para almacenar la marca de tiempo de milisegundos del servidor actual. En cambio, la marca de tiempo del servidor actual se resta de un determinado valor de marca de tiempo inicial. Generalmente, el tiempo en línea del servicio se puede utilizar como valor de marca de tiempo inicial. .

Para el código de máquina, puede ajustarlo según su propia situación, como el número de la sala de computadoras, el número del servidor, el número de la empresa, la IP de la máquina, etc. Para los diferentes servicios de algoritmo de copo de nieve implementados, se puede distinguir el código de máquina calculado final.

Supongo que te gusta

Origin blog.csdn.net/songjianlong/article/details/132782298
Recomendado
Clasificación