第2-2-4章 常见组件与中台化-常用组件服务介绍-分布式ID-附Snowflake雪花算法的代码实现

2.3 分布式ID

2.3.1 功能概述

ID,全称Identifier,中文翻译为标识符,是用来唯一标识对象或记录的符号。比如我们每个人都有自己的身份证号,这个就是我们的标识符,有了这个唯一标识,就能快速识别出每一个人。

在计算机世界里,复杂的分布式系统中,经常需要对大量的数据、消息、HTTP 请求等进行唯一标识。比如对于分微服务架构的系统中,服务间相互调用需要唯一标识,幂等处理,调用链路分析,日志追踪的时候都需要使用这个唯一标识,此时我们的系统就迫切的需要一个全局唯一的ID。

另外随着社会的发展,各种金融、电商、支付、等系统中产生的数据越来越多,对数据库进行分库分表是比较常见的,而分库后则需要有一个唯一ID来标识一条数据或消息,单个数据库的自增ID显然不能满足需求,此时也会需要一个能够生成全局唯一ID的系统。

工程结构:

在这里插入图片描述

2.3.2 应用场景

1、全局唯一

这个最简单,就是说不能出现重复的ID,既然是唯一标识,这是最基本的要求。比如采用UUID.randomUUID()的方式产生唯一且不重复的分布式主键。最终生成一个字符串类型的主键。缺点是生成的主键无序。

2、趋势递增

先来了解下什么是趋势递增?

简单说就是在一段时间内,生成的ID是递增的趋势,而不强求下一个ID必须大于前一ID。例如在一段时间内生成的ID在【0,1000】之间,过段时间生成的ID在【1000,2000】之间。

为什么要趋势递增?

目前大部分的互联网公司使用了开源的MySQL数据库,存储引擎选择InnoDB。MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS数据库使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键,这样在插入新的数据时B-tree的结构不会时常被打乱重塑,能有效的提高存取效率。

3、单调递增

通俗的说就是下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。

4、信息安全

如果ID是连续递增的,那么恶意用户可以根据当前ID推测出下一个ID,爬取系统中数据的工作就非常容易实现,直接按照顺序访问指定URL即可;如果是订单号就更加危险,竞争对手可以直接知道系统一天的总订单量。所以在一些应用场景下,会需要ID无规则、不规则,切不易被破解。

5、雪花算法SNOWFLAKE

雪花算法,能够保证不同进程主键的不重复性,相同进程主键的有序性。二进制形式包含4部分,从高位到低位分表为:1bit符号位、41bit时间戳位、10bit工作进程位以及12bit序列号位。

  • 符号位(1bit)

预留的符号位,恒为零。

  • 时间戳位(41bit)

41位的时间戳可以容纳的毫秒数是2的41次幂,一年所使用的毫秒数是:365 * 24 * 60 * 60 * 1000 Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L) = 69.73年不重复;

  • 工作进程位(10bit)

该标志在Java进程内是唯一的,如果是分布式应用部署应保证每个工作进程的id是不同的。该值默认为0,可通过属性设置。

  • 序列号位(12bit)

该序列是用来在同一个毫秒内生成不同的ID。如果在这个毫秒内生成的数量超过4096(2的12次幂),那么生成器会等待到下个毫秒继续生成。

在这里插入图片描述

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。

  • 不依赖第三方组件,稳定性高,生成ID的性能也非常高。

  • 可以根据自身业务特性分配bit位,非常灵活

    缺点:

    强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。

2.3.3 使用说明

分布式ID生成系统部署完成后,第三方系统接入即可直接获取ID。

  1. 引入distributedid-client依赖:在项目pom.xml添加坐标

    <dependencies>
         <dependency>
              <groupId>com.itheima.distributedid</groupId>
              <artifactId>distributedid-client</artifactId>
              <version>1.0-SNAPSHOT</version>
         </dependency>
    </dependencies>
    
  2. 分布式ID生成系统客户端配置,在项目resources目录下编辑distributedid_client.properties

    #服务器地址
    distributedid.server=211.103.136.244:7315
    #部署多个的话,可自行添加
    #distributedid.server=211.103.136.244:7315,ip2:port,...
    #超时时间
    distributedid.readTimeout=5000
    distributedid.connectTimeout=5000
    
  3. 获取ID时,直接调用即可

    Long id = 0L;
    //从服务端获取自增型ID
    id = DistributedId.autoincrementId("your service name");
                    
    //本地生成雪花算法ID
    id = DistributedId.snowflake();
    
    //从服务端获取雪花算法ID
    id = DistributedId.snowflakeFromServer();
    
    //使用号段模式获取单个ID
    id = DistributedId.segment();
    
  4. 数据库脚本

    /*
     Navicat Premium Data Transfer
    
     Source Server         : 本地MySQL数据库
     Source Server Type    : MySQL
     Source Server Version : 50728
     Source Host           : localhost:3306
     Source Schema         : distributedid
    
     Target Server Type    : MySQL
     Target Server Version : 50728
     File Encoding         : 65001
     
    */
    
    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for segment_id_info
    -- ----------------------------
    DROP TABLE IF EXISTS `segment_id_info`;
    CREATE TABLE `segment_id_info`  (
      `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
      `biz_type` varchar(63) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
      `begin_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同',
      `max_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '当前最大id',
      `step` int(11) NULL DEFAULT 0 COMMENT '步长',
      `delta` int(11) NOT NULL DEFAULT 1 COMMENT '每次id增量',
      `remainder` int(11) NOT NULL DEFAULT 0 COMMENT '余数',
      `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
      `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '更新时间',
      `version` bigint(20) NOT NULL DEFAULT 0 COMMENT '版本号',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE INDEX `uniq_biz_type`(`biz_type`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '号段ID信息表' ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Table structure for sequence_id
    -- ----------------------------
    DROP TABLE IF EXISTS `sequence_id`;
    CREATE TABLE `sequence_id`  (
      `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
      `biz_type` char(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '业务类型,唯一',
      PRIMARY KEY (`id`) USING BTREE,
      UNIQUE INDEX `biz_type`(`biz_type`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 62 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    

2.3.4 项目截图

  • 后端代码

在这里插入图片描述

  • swagger页面

在这里插入图片描述

2.3.5 Snowflake雪花算法的代码实现

package com.itheima.distributedid.core;

import com.itheima.distributedid.core.domain.DistributedIdException;

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
 * Twitter的Snowflake算法
 * <p>
 * 协议格式   1:  41位时间戳      2:5位数据中心标识     3:5位机器标识   4:12位序列号
 * <p>
 * 1111111111111111111111111111111   11111    11111    111111111111
 */
public class Snowflake {

    //起始时间戳,可以修改为服务器第一次启动的时间
    //一旦服务已经开始使用,起始时间戳就不能改变了,理论上可以使用69年
    private final static long START_TIME = 1484754361114L;

    /**
     * 每一个部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12;//序列号占用的位数
    private final static long MACHINE_BIT = 5;//序机器标识 占用的位数
    private final static long DATA_CENTER_BIT = 5;//数据中心标识占用的位数

    /**
     * 每一个部分的最大值  11111111111111111    1111111100000   000000000011111
     */
    private final static long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左位移数 1111111111111111111111111111111   11111    11111    111111111111
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private long dataCenterId;//数据中心ID
    private long machineId;//数据中心ID
    private long sequence = 0L;//数据中心ID
    private long lastTimestamp = -1L;//数据中心ID


    /**
     * 分布式部署的时候,数据节点标识和机器标识作为联合键,必须唯一的
     *
     * @param dataCenterId 数据中心标识ID
     * @param machineId    机器标识ID
     */
    public Snowflake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
            throw new DistributedIdException("数据中心ID不合法");
        }

        if (machineId > MAX_MACHINE_ID || machineId < 0) {
            throw new DistributedIdException("机器标识ID不合法");
        }

        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * 获取下一个ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currentTmestamp = getNowTimestamp();

        if (currentTmestamp < lastTimestamp) {
            throw new RuntimeException("时钟错误,拒绝生成ID");
        }

        if (currentTmestamp == lastTimestamp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;

            //同一毫秒的序列号已经达到最大
            if (sequence == 0L) {
                currentTmestamp = getNexMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }

        lastTimestamp = currentTmestamp;

        return (currentTmestamp - START_TIME) << TIMESTAMP_LEFT  //时间戳的部分
                | dataCenterId << DATA_CENTER_LEFT //数据中心的部分
                | machineId << MACHINE_LEFT //机器标识的部分
                | sequence;  //序列号的部分


    }

    /**
     * 保证获取到的毫秒值是在最后一次分发ID的毫秒值之后lastTimestamp
     * 当某一个毫秒,序列号用完了之后,等待到下一个毫秒,在进行序列号的使用
     *
     * @return
     */
    private long getNexMill() {
        long timestamp = this.getNowTimestamp();

        //不断的遍历,直到获取到lastTimestamp下一个毫秒值
        while (timestamp <= lastTimestamp) {
            //进行时间回拨
            timestamp = this.getNowTimestamp();
        }

        return timestamp;
    }

    //获取当前毫秒值
    private long getNowTimestamp() {
        return System.currentTimeMillis();
    }


    /**
     * 使用当前计算机的MAC生成数据中心标识ID
     *
     * @param maxDataCenterId
     * @return
     */
    private static long getDataCenterId(long maxDataCenterId) {
        long id = 0L;

        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);

            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();
                if (mac != null) {
                    id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2])
                            << 8))) >> 6;
                    id = id % (maxDataCenterId + 1);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return id;
    }


    //根据当前计算机的进程PID生成机器识别ID
    private static long getMachineId(long dataCenterId, long maxMachineId) {
        StringBuilder sb = new StringBuilder();
        sb.append(dataCenterId);

        //获取JVM进程的PID
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (name != null) {
            sb.append(name.split("@")[0]);
        }

        /**
         * MAC+PID 的hashcode 获取16个低位
         */
        int id = sb.toString().hashCode() & 0xffff;
        return id % (maxMachineId + 1);
    }

    public Snowflake() {
        dataCenterId = getDataCenterId(MAX_DATA_CENTER_ID);
        machineId = getMachineId(dataCenterId, MAX_MACHINE_ID);
    }

    //public static void main(String[] args) {
    //    //指定数据中心和机器识别id
    //    Snowflake snowflake = new Snowflake(2, 3);
    //    System.out.println("指定数据中心和机器识别ID来生成ID");
    //    for (int i = 0; i < 10; i++) {
    //        System.out.println(snowflake.nextId());
    //    }
    //
    //    //默认快速使用方式
    //    snowflake = new Snowflake();
    //    System.out.println("快速使用方式来生成ID");
    //    for (int i = 0; i < 10; i++) {
    //        System.out.println(snowflake.nextId());
    //    }
    //}

}