[Spring Cloud Series] Principe et mise en œuvre de l'algorithme Snowflake

[Spring Cloud Series] Principe et mise en œuvre de l'algorithme Snowflake

I. Aperçu

Dans un environnement distribué à forte concurrence, une réservation de billets courante est le jour férié 12306. Lorsqu'un grand nombre d'utilisateurs se précipitent pour acheter des billets dans la même direction, des dizaines de milliers de commandes peuvent être générées en quelques millisecondes. garantir l’unicité de l’ID de commande généré, est crucial. Dans cet environnement de vente flash, non seulement le caractère unique de l'identifiant doit être assuré, mais la priorité de génération de l'identifiant doit également être assurée.

2. Une partie des exigences rigides pour générer des règles d'identification

  1. Globalement unique : les numéros d'identification en double ne peuvent pas apparaître. Puisqu'il s'agit d'un identifiant unique, il s'agit de l'exigence la plus fondamentale.
  2. Tendance à la hausse : Les index clusterisés sont applicables dans le moteur InnoDB de MySQL. Étant donné que la plupart des SGBDR utilisent la structure de données B+Tree pour stocker les données d'index, lors de la sélection des clés primaires, nous essayons d'utiliser des clés primaires ordonnées pour garantir les performances d'écriture.
  3. Augmentation monotone : assurez-vous que l'ID suivant doit être supérieur à l'ID précédent, comme le numéro de version de la transaction, le tri et d'autres exigences particulières.
  4. Sécurité de l'information : Si l'identifiant est continu, il sera très facile pour des utilisateurs malveillants de le capturer. Il leur suffira de télécharger l'URL spécifiée dans l'ordre ; s'il s'agit d'un numéro de commande, ce sera dangereux.
  5. Contient l'horodatage : l'ID généré contient des informations d'horodatage complètes.

3. Exigences de disponibilité du système de génération de numéros d'identification

  1. Haute disponibilité : Envoyez une demande pour obtenir un identifiant distribué, et le serveur est assuré de créer un identifiant distribué unique pour moi dans 99,9999% des cas.
  2. Faible latence : Pour envoyer une requête pour obtenir un identifiant distribué, le serveur doit être rapide et extrêmement rapide.
  3. QPS élevé : si 100 000 ID distribués sont demandés en même temps, le serveur doit résister et créer avec succès 100 000 ID distribués.

4. Solution générale à l'identification distribuée

4.1 UUID

La forme standard de l'UUID (Universally Unique Identifier) ​​​​contient 32 chiffres hexadécimaux, divisés en cinq segments avec des tirets, sous la forme : 36 caractères de 8-4-4-4-12, exemple : 1E785B2B-111C-752A-997B. -3346E7495CE2 ; les performances de l'UUID sont très élevées, ne dépendent pas du réseau et sont générées localement.

Inconvénients de l'UUID :

  1. Non ordonné, il est impossible de prédire l'ordre dans lequel il est généré, et il ne peut pas générer de nombres par ordre croissant. MySql recommande officiellement que la clé primaire soit aussi courte que possible. L'UUID est une chaîne de 32 bits, ce n'est donc pas recommandé.

  2. Indice, division de l'indice B+Tree

    L'ID distribué est la clé primaire et la clé primaire est l'index clusterisé. L'index de MySQL est implémenté par B+Tree. Chaque fois que de nouvelles données UUID sont insérées, dans le but d'insérer de nouvelles données UUID et d'optimiser les requêtes, le B+Tree en bas de l'index sera modifié ; parce que les données UUID ne sont pas ordonnées. Ainsi, chaque fois que des données UUID sont insérées, l'index clusterisé de la clé primaire sera considérablement modifié. Lors de l'insertion de données, la clé primaire sera insérée dans le désordre, ce qui entraînera la division de certains nœuds intermédiaires et conduira à un grand nombre de nœud insaturé. Cela réduit considérablement les performances d'insertion de la base de données.

4.2 Clé primaire à incrémentation automatique de la base de données

Autonome

Dans les systèmes distribués, le principe principal du mécanisme d'identification à croissance automatique de la base de données est le suivant : l'ID à croissance automatique de la base de données est implémenté par remplacement dans la base de données MySql.

La signification de Remplacer dans est d'insérer un enregistrement. Si la valeur de l'index unique dans la table rencontre un conflit, les anciennes données seront remplacées.

Dans une application unique, un ID auto-croissant est utilisé, mais dans une application distribuée en cluster, une application unique ne convient pas.

  1. Il est difficile d'étendre le système horizontalement. Par exemple, après avoir défini la taille du pas de croissance et le nombre de machines, lors de l'ajout d'un grand nombre de serveurs, les valeurs initiales doivent être réinitialisées. Cela a une mauvaise opérabilité, donc le système La solution d’expansion horizontale est très complexe et difficile à mettre en œuvre.
  2. La base de données est soumise à une forte pression. Chaque fois que vous obtenez l'ID, vous devez lire et écrire dans la base de données, ce qui affecte considérablement les performances. Elle ne respecte pas les règles de faible latence et de QPS élevé dans l'ID distribué (sous forte concurrence, si vous allez dans la base de données pour obtenir l'ID, cela sera très affecté). performances.)

4.3 Générer une stratégie d'identification globale basée sur Redis

Dans le cas du cluster Redis, différentes étapes de croissance doivent être définies comme MySql, et la clé doit avoir une période de validité. Le cluster Redis peut être utilisé pour obtenir un débit plus élevé.

5. SnowFlake (algorithme de flocon de neige)

SnowFlake de Twitter a résolu ce besoin. Initialement, Twitter a migré le système de stockage de MySQL vers Cassandra (un système de base de données NoSQL distribué open source développé par Facebook). Comme Cassandra ne disposait pas d'un mécanisme de génération d'identifiants séquentiels, il a développé un tel ensemble d'identifiants uniques au monde. .Générer des services. SnowFlake peut générer 260 000 identifiants triables à augmentation automatique par seconde.

5.1 Fonctionnalités de SnowFlake

  1. Les identifiants générés par SnowFlake par Twitter peuvent être générés dans l'ordre chronologique.
  2. Le résultat de l'ID généré par l'algorithme SnowFlake est un entier de 64 bits, qui est de type Long (la longueur maximale après conversion en chaîne est de 19).
  3. Il n'y aura pas de collisions d'ID dans le système distribué (distingué par le centre de données et le workid) et l'efficacité est élevée.

5.2 Structure du flocon de neige

Insérer la description de l'image ici

5.3 Principe de l'algorithme de flocon de neige

Le principe de l'algorithme du flocon de neige est de générer un identifiant unique de type long de 64 bits

  1. Le bit le plus élevé a une valeur fixe de 0, car l'ID généré est un entier positif, et s'il est 1, c'est une valeur négative.
  2. Suivi de 41 bits pour stocker les horodatages en millisecondes, 2^41/(1000 * 60 * 24 * 365) = 69, qui peuvent être utilisés pendant environ 69 ans.
  3. Les 10 chiffres suivants stockent le code machine, y compris le DataCenterId à 5 chiffres et le WorkerId à 5 chiffres. Jusqu'à 2 ^ 10 = 1 024 machines peuvent être déployées.
  4. Les 12 derniers bits stockent le numéro de séquence. Lorsque le même horodatage en millisecondes est utilisé, il se distingue par ce numéro de séquence incrémentiel. Autrement dit, pour la même machine, sous le même horodatage en millisecondes, 2^12=4096 ID uniques peuvent être généré.

L'algorithme Snowflake peut être déployé en tant que service distinct, puis pour les systèmes qui nécessitent un identifiant globalement unique, il suffit de demander au service d'algorithme Snowflake d'obtenir l'ID.

Pour chaque service d'algorithme Snowflake, vous devez d'abord spécifier un code machine à 10 chiffres. Celui-ci peut être défini en fonction de votre propre entreprise. Par exemple, numéro de salle informatique + numéro de machine, numéro de machine + numéro de service ou autres entiers à 10 chiffres qui distinguent l'identifiant.

5.4 Implémentation de l'algorithme

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 Avantages de l'algorithme de flocon de neige

  1. Générez des identifiants uniques dans un environnement distribué à haute concurrence et pouvez générer des millions d'identifiants uniques par seconde.
  2. Sur la base de l'horodatage et de l'incrémentation automatique du numéro de séquence sous le même horodatage, il est fondamentalement garanti que l'identifiant augmente de manière ordonnée.
  3. Ne repose pas sur des bibliothèques ou des middlewares tiers.
  4. L'algorithme est simple, exécuté en mémoire et très efficace.

5.5 Inconvénients de l'algorithme de flocon de neige :

  1. En fonction de l'heure du serveur, des identifiants en double peuvent être générés lorsque l'horloge du serveur est réglée. L'algorithme peut être résolu en enregistrant l'horodatage de la génération du dernier identifiant. Avant chaque génération d'identifiant, comparez si l'horloge actuelle du serveur a été réglée pour éviter de générer des identifiants en double.

6. Résumé

En fait, le nombre de bits occupés par chaque partie de l’algorithme en flocon de neige n’est pas fixe. Par exemple, votre entreprise n'a peut-être pas 69 ans, vous pouvez donc réduire le nombre de bits occupés par l'horodatage. Si le service d'algorithme Snowflake doit déployer plus de 1 024 nœuds, vous pouvez utiliser le nombre réduit de bits pour le code machine.

Notez que les 41 bits de l'algorithme Snowflake ne sont pas directement utilisés pour stocker l'horodatage actuel du serveur en millisecondes, mais nécessitent l'horodatage actuel du serveur moins une valeur d'horodatage initiale. Généralement, l'heure en ligne du service peut être utilisée comme valeur d'horodatage initiale.

Pour le code machine, vous pouvez l'ajuster en fonction de votre propre situation. Par exemple, le numéro de la salle informatique, le numéro du serveur, le numéro d'entreprise, l'adresse IP de la machine, etc. Pour différents services d'algorithme de flocon de neige déployés, le code machine calculé final peut être distingué.

Je suppose que tu aimes

Origine blog.csdn.net/songjianlong/article/details/132782298
conseillé
Classement