一、简介

  zookeeper是一个分布式的,开放源码的分布式应用程序协调服务,它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。选择zookeeper的原因,因为zookeeper具有以下优点:

  • 可靠性:如果消息被到一台服务器接受,那么它将被所有的服务器接受。
  • 实时性:在一定事件范围内,client能读到最新数据,如果需要最新数据,应该在读数据之前调用sync()接口
  • 独立性:各个Client之间互不干预
  • 顺序性:更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次执行
  • 原子性:一次数据的更新只能成功或者失败,没有其他中间状态
  • 最终一致性:全局唯一数据视图,client无论连接到哪个server,数据视图都是一致的

  snowflake在单机环境使用是简单高效,但是实际中遇到分布式,容器各种环境下,需要考虑的东西还是蛮多的,今天我们就实现一个简单版的。

二、maven依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.alian</groupId>
    <artifactId>snowflake</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>snowflake</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- 本例中你不加入容器也可以运行-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${parent.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>log4j</groupId>
                    <artifactId>log4j</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.2.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.14</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  我这里把zookeeper里面的日志框架都过滤了,加入了lombok,你们可以根据自己的需要进行相应的处理。

三、配置

3.1、属性配置文件

  我们在resource目录下建一个config文件夹,然后在config目录下创建zookeeper的自定义配置文件zookeeper.properties,主要是zookeeper连接和目录相关的配置,因为本机的配置原因,我这里就没有采用zookeeper的集群模式了,Zookeeper建议集群节点个数为奇数(大于等于3),只要超过一半的机器能够正常提供服务,那么整个集群都是可用的状态。

zookeeper.properties

# zookeeper服务器地址(ip+port)
zookeeper.server=10.130.3.16:2181
#zookeeper.server=192.168.0.101:2181
# 节点创建的路径约定用"/"结尾
zookeeper.rootPath=/root/alian/
# 锁路径
zookeeper.lockPath=/root/snowflake
# 休眠时间
zookeeper.sleep-time=1000
# 最大重试次数
zookeeper.max-retries=3
# 会话超时时间
zookeeper.session-timeout=5000
# 连接超时时间
zookeeper.connection-timeout=5000

3.2、属性配置类

ZookeeperProperties.java

package com.alian.snowflake.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "zookeeper")
@PropertySource(value = "classpath:config/zookeeper.properties", encoding = "UTF-8", ignoreResourceNotFound = true)//读取指定路径配置文件,暂不支持*.yaml文件
public class ZookeeperProperties {

    /**
     * zookeeper服务地址
     */
    private String server;

    /**
     * 根路径
     */
    private String rootPath;

    /**
     * 分布式锁路径
     */
    private String lockPath;

    /**
     * 重试等待时间
     */
    private int sleepTime;

    /**
     * 最大重试次数
     */
    private int maxRetries;

    /**
     * session超时时间
     */
    private int sessionTimeout;

    /**
     * 连接超时时间
     */
    private int connectionTimeout;

}

  此配置类不懂的可以参考我另一篇文章:Spring Boot读取配置文件常用方式

3.3、zookeeper配置类(核心

  为什么要使用Curator,因为Curator提供了简化使用zookeeper更高级的API接口,CuratorFramework实例都是线程安全的。它包涵很多优秀的特性:

  • 自动连接管理:自动处理zookeeper的连接和重试存在一些潜在的问题,可以监控节点数据改变和获取更新服务的列表,而监控又可以自动被Cruator recipes删除
  • 更简洁的API:提供现代流式API接口,简化了原生zookeeper方法,事件等
  • Recipe实现:可以应用到leader选举,分布式锁,path缓存,和watcher,分布式队列等

ZookeeperConfig.java

package com.alian.snowflake.config;

import com.alian.snowflake.common.ZookeeperClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class ZookeeperConfig {

    @Autowired
    private ZookeeperProperties zookeeperProperties;

    @Bean
    public CuratorFramework curatorFrameworkClient() {
        //重试策略,ExponentialBackoffRetry(1000,3)这里表示等待1s重试,最大重试次数为3次
        RetryPolicy policy = new ExponentialBackoffRetry(zookeeperProperties.getSleepTime(), zookeeperProperties.getMaxRetries());
        //构建CuratorFramework实例
        CuratorFramework curatorFrameworkClient = CuratorFrameworkFactory
                .builder()
                .connectString(zookeeperProperties.getServer())
                .sessionTimeoutMs(zookeeperProperties.getSessionTimeout())
                .connectionTimeoutMs(zookeeperProperties.getConnectionTimeout())
                .retryPolicy(policy)
                .build();
        //启动实例
        curatorFrameworkClient.start();
        return curatorFrameworkClient;
    }

    @Bean(destroyMethod = "destroy")
    public ZookeeperClient zookeeperClient(ZookeeperProperties zookeeperProperties, CuratorFramework curatorFrameworkClient) {
        return new ZookeeperClient(zookeeperProperties, curatorFrameworkClient);
    }

}

四、详细使用

4.1、zookeeperClient(核心

  此组件主要是完成,顺序节点的创建,我这里创建的是临时顺利节点。我想我代码的注释比这个惨白的文字要更有说服力。很多小伙伴可能会采用@Component注解,我使用@Bean(destroyMethod = “destroy”)来创建,可以比较优雅的关闭连接,当然在使用上是一样的,只要不是对一个bean同时使用@Component @Bean
zookeeperClient.java

package com.alian.snowflake.common;

import com.alian.snowflake.config.ZookeeperProperties;
import com.sun.istack.internal.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;

@Slf4j
public class ZookeeperClient {

    private ZookeeperProperties zookeeperProperties;

    private CuratorFramework curatorFramework;

    public CuratorFramework getCuratorFramework() {
        return curatorFramework;
    }

    public ZookeeperClient(ZookeeperProperties zookeeperProperties, CuratorFramework curatorFramework) {
        this.zookeeperProperties = zookeeperProperties;
        this.curatorFramework = curatorFramework;
    }

    public void destroy() {
        try {
            log.info("ZookeeperClient销毁方法,如果zookeeper连接不为空,则关闭连接");
            if (getCuratorFramework() != null) {
                //这种方式比较优雅的关闭连接
                getCuratorFramework().close();
            }
        } catch (Exception e) {
            log.error("stop zookeeper client error {}", e.getMessage());
        }
    }

    /**
     * 分布式ID生成
     */
    public String generateId(String nodeName) {
        //创建临时自动编号的顺序节点,返回的是一个路径
        String nodeFullPath = createSeqNode(nodeName);
        if (null == nodeFullPath) {
            return "";
        }
        String generateId = splitSeqNode(nodeFullPath);
        log.info("节点编号:" + generateId);
        return generateId;
    }

    /**
     * 创建顺序节点,约定nodePrefix不能为空,也不能用"/"开头或结尾
     */
    private String createSeqNode(@NotNull String nodePrefix) {
        if ("".equals(nodePrefix.trim()) || nodePrefix.startsWith("/") || nodePrefix.endsWith("/")) {
            log.error("节点前缀错误");
            return "";
        }
        String nodeFullPath = "";
        //根路径+要创建节点的名称(可以是路径,此处传入的nodeName不要用"/"开头或结尾)
        String fullPath = zookeeperProperties.getRootPath().concat(nodePrefix);
        try {
            //关键点:创建临时顺序节点
            //creatingParentsIfNeeded():如果传入的是路径,并且节点父路径不存在则创建父节点
            nodeFullPath = curatorFramework
                    .create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    .forPath(fullPath);
        } catch (Exception e) {
            log.error("创建临时顺序节点异常{}", e.getMessage());
            e.printStackTrace();
        }
        return nodeFullPath;
    }

    private String splitSeqNode(String path) {
        //获取最后一个"/"的索引,用于字符串截取
        int index = path.lastIndexOf("/");
        if (index >= 0) {
            //如果"/"位置比路径长度小,则截取"/"后面的作为节点返回
            //如果"/"位置等于路径长度,说明它后面没有元素,返回空字符串
            return index <= path.length() ? path.substring(index + 1) : "";
        }
        //不含"/",说明是节点数据,直接返回(理论上不会,因为都是"/"开头的路径,至少有一个"/")
        return path;
    }

	//删除节点
    public void deleteTheNode(String fullNodePath) {
        try {
            Stat stat = curatorFramework.checkExists().forPath(fullNodePath);
            if (stat != null) {
                curatorFramework.delete().forPath(fullNodePath);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据路径获取子节点
     * path  以 / 开头不能以 / 结尾
     *
     * @param path
     * @return
     */
    public List<String> getChildren(String path) {
        try {
            List<String> childrenList = curatorFramework.getChildren().forPath(path);
            if (childrenList != null && childrenList.size() > 0) {
                return childrenList;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Collections.emptyList();
    }

}

关于zookeeper里节点的模式,新版又增加了两个带TTL的持久化节点,节点模式:

  • PERSISTENT:持久化节点
  • PERSISTENT_SEQUENTIAL:持久化的顺序自动编号节点
  • EPHEMERAL:临时节点
  • EPHEMERAL_SEQUENTIAL:临时顺序自动编号节点
  • CONTAINER:容器节点
  • PERSISTENT_WITH_TTL:带TTL(time-to-live,存活时间)的持久化节点,节点在TTL时间之内没有得到更新并且没有子节点,就会被自动删除
  • PERSISTENT_SEQUENTIAL_WITH_TTL:带TTL(time-to-live,存活时间)和单调递增序号的持久节点,节点在TTL时间之内没有得到更新并且没有子节点,就会被自动删除

4.2、Snowflake算法(核心

  要了解snowflake算法,首先得了解它的结构

在这里插入图片描述

  • 第一位:占用1bit,其值始终是0,没有实际作用
  • 时间戳:占用41bit,精确到毫秒,总共可以容纳约69年的时间((1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
  • 工作机器id:占用10bit,最多可以容纳1024个节点(可以改造为5位工作中心+5位数据中心,具体如上图第二部分
  • 序列号:占用12bit,最多可以累加到4095。这个值在同一毫秒同一节点上从0开始不断累加。

Snowflake算法是开源的如下:

Snowflake.java

package com.alian.snowflake.common;

/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
public class Snowflake {

    /**
     * 基本变量
     */
    private final long startTimestamp = 1636560000000L;//开始时间截 (2021-11-11)
    private long dataCenterId;//数据中心ID(0~31)
    private long workerId;//工作机器ID(0~31)
    private long sequence = 0L;//毫秒内序列(0~4095)
    private long lastTimestamp = -1L;//上次生成ID的时间截

    /**
     * 每一部分占用的位数
     */
    private final long dataCenterIdBits = 5L;//数据标识id所占的位数
    private final long workerIdBits = 5L;//机器id所占的位数
    private final long sequenceBits = 12L;//序列在id中占的位数

    /**
     * 每一部分最大值
     */
    private final long maxDataCenterId = ~(-1L << dataCenterIdBits);//支持的最大数据标识id,结果是31
    private final long maxWorkerId = ~(-1L << workerIdBits);//支持的最大机器id,结果是31
    private final long sequenceMask = ~(-1L << sequenceBits);//生成序列的掩码,这里为4095

    /**
     * 每一部分向左的位移
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;//时间截向左移22位(5+5+12)
    private final long dataCenterIdShift = sequenceBits + workerIdBits;//数据标识id向左移17位(12+5)
    private final long workerIdShift = sequenceBits;//机器ID向左移12位

    /**
     * 构造函数
     *
     * @param dataCenterId 数据中心ID (0~31)
     * @param workerId     工作ID (0~31)
     */
    public Snowflake(long dataCenterId, long workerId) {
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDataCenterId));
        }
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }
        //上次生成ID的时间截
        lastTimestamp = timestamp;
        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - startTimestamp) << timestampLeftShift) //
                | (dataCenterId << dataCenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

}

4.3、dataCenterId定义

SystemSequence.java

package com.alian.snowflake.common;

public class SystemSequence {

    /**
     * 支付
     */
    public static final long PAY = 0;

    /**
     * 查询
     */
    public static final long QUERY = 1;

    /**
     * 通知
     */
    public static final long NOTICE = 2;

    /**
     * snowflake
     */
    public static final long SNOWFLAKE = 4;

    /**
     * 管理
     */
    public static final long OMS = 5;

    /**
     * 账户
     */
    public static final long AMS = 6;

}

  改造后的工作中心id最大为31(2的5次方),也就是可以有32个系统。当然一般这种定义文件一般是公共包或者存数据库,我这里只是一个事例,为了本文的阅读性。

4.4、service层(核心实现

DistributedService.java

package com.alian.snowflake.service;

import com.alian.snowflake.common.Snowflake;
import com.alian.snowflake.common.SystemSequence;
import com.alian.snowflake.common.ZookeeperClient;
import com.alian.snowflake.config.ZookeeperProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
public class DistributedService {

    private Snowflake idWorker = null;

    @Autowired
    private ZookeeperProperties zookeeperProperties;

    @Autowired
    private ZookeeperClient zookeeperClient;

    /**
     * 系统启动就执行
     */
    @PostConstruct
    private void init() {
        //获取锁路径
        String lockPath = zookeeperProperties.getLockPath();
        //创建InterProcessMutex实例
        InterProcessMutex lock = new InterProcessMutex(zookeeperClient.getCuratorFramework(), lockPath);
        boolean success = false;
        try {
            success = lock.acquire(20, TimeUnit.SECONDS);
            if (success) {
                initSnowflake();
            }
        } catch (Exception e) {
            log.error("snowflake系统启动初始化方法异常", e);
        } finally {
            try {
                if (success) {
                    lock.release();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void initSnowflake() {
        boolean create = true;
        do {
            //也可以是  xxx/id-
            String nodeName = "snowflake/id-";
            //生成节点的完整路径
            String fullPath = zookeeperProperties.getRootPath() + nodeName;
            //截取开始到最后一个/作为父路径
            String path = fullPath.substring(0, fullPath.lastIndexOf("/"));
            //获取路径下所有子节点
            List<String> children = zookeeperClient.getChildren(path);
            log.info("{}", children);
            if (children.size() >= 32) {
                log.error("节点已经超过32个了");
                throw new RuntimeException();
            }
            //生成唯一id
            String generateId = zookeeperClient.generateId(nodeName);
            log.info("zookeeper生成的原始id:{}", generateId);
            //去除前缀id-
            long workerId = Long.parseLong(generateId.substring(3)) % 32;
            if (children.size() > 0) {
                //路径下的所有节点转为long型并对对32求模
                List<Long> collect = children.stream().map(p -> Long.parseLong(p.substring(3)) % 32).collect(Collectors.toList());
                if (!collect.contains(workerId)) {
                    //求模后列表里不重复
                    create = false;
                    //实例化
                    idWorker = new Snowflake(SystemSequence.SNOWFLAKE, workerId);
                } else {
                    //workerId占用的可以删除节点
                    zookeeperClient.deleteTheNode(path + "/" + generateId);
                }
            } else {
                create = false;
                idWorker = new Snowflake(SystemSequence.SNOWFLAKE, workerId);
            }
        } while (create);
    }

    public String generateId() {
        //我这里就不做严格的判断了
        if (idWorker == null) {
            return "";
        }
        return "" + idWorker.nextId();
    }
}

  在这里我们要解释下一段代码:

	idWorker = new Snowflake(SystemSequence.SNOWFLAKE, workerId);

  SystemSequence是我们定义的一个系统一个编号,不管是单机还是多实例,同一个系统不管是第一次启动还是第二次启动,都是不会变的,编号是从0开始到31,相当于是一个固定的值。而workerId是我们通过zookeeper生成的,具有顺序性和唯一性。比如从0000000000~9999999999,我这里实现的基本流程图如下:
在这里插入图片描述
我大概介绍下我们实现的基本流程

  • 整个初始化的流程中我使用curator分布式锁
  • 首先我们定义一个节点的前缀,我建议每个系统带不同的前缀比如pay/id-query/id-snowflake/id-,这样有个好处就是每个系统的节点都在一起便于分析节点,如果所有系统都在一起,那么你还得去分析节点的值才能判断节点是否可用
  • 获取生成节点的父路径下的所有的子节点,如果节点的数目大于等于32,说明实例数已经满了,不能创建了,因为本文这里的workerId最大是31
  • 当子节点数目小于32,我们创建一个新的临时顺序节点,去除节点的前缀后,用32对其求模(得到的workerId就是1~31)
  • 如果子节点的数目大于0,小于32,我们把子节点里是数据都转为long型,并对用32对其求模
  • 判断计算后的workerId是否在求模后的列表中,如果workerId在列表中,说明workerId已经占用,则删除节点后,继续循环上述流程,如果workerId不在求模后的列表中,直接初始化snowflake
  • 还有就是如果节点列表为空,说明还没有实例启动,则创建一个新的临时顺序节点,去除节点的前缀后,对32求模,然后初始化snowflake

关于curator分布式锁不懂的可以参考:SpringBoot基于Zookeeper和Curator实现分布式锁并分析其原理

但是为什么要对该数据求模呢?

  首先我们知道workerId最大31,举个例子,我们系统每次启动就会生成一个唯一id,假设我们的实例部署了,31个(生成的节点编号是0000000000~0000000030),然后假设第30号服务重启了一次,重启时生成的节点编号是0000000031,这个时候,我们应该还可以部署一个新的服务,或者对任意一个系统进行重启,对吧?不管是新的服务,还是重启生成的编号是不是编号就是0000000032,这个32我们的snowflake肯定是接收不不了的,因为它的范围是0到31
  假设我们是第3台机器重启了,那么求模后的数为0,明显0已经被第一号机器占用了,我们只能继续生成节点,直到生成0000000034节点,求模后是2,也就是3号机器(3号机器重启时0000000002节点已经自动删除了),这下大家应该知道为什么对32求模了吧?

4.5、controller层

  简单的一个获取id的接口,仅仅是为了演示,实际中肯定更加严格和规范。

DistributedIdController.java

package com.alian.snowflake.controller;

import com.alian.snowflake.service.DistributedService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/distributed")
public class DistributedIdController {

    @Autowired
    private DistributedService distributedService;

    /**
     * 生成分布式id
     *
     * @return
     */
    @RequestMapping("/generateId")
    public String generateId(HttpServletRequest request) {
        String distributedId = distributedService.generateId();
        log.info("获取到的分布式id:{}", distributedId);
        return distributedId;
    }
}

五、测试

5.1、多实例

  由于我们是windows环境下的本机开发及测试,我们使用idea启动三个实例进行负载均衡,端口分别为606160626063

application.yml

server:
  port: 6061
  servlet:
    context-path: /snowflake

三个实例启动的示例图:
在这里插入图片描述

5.2、nginx转发配置

  自定义配置文件localhost_80.confserver模块里增加转发配置,通过负载均衡到两个实例上。

	location ~ ^/snowflake/ {
        proxy_redirect off;
		#端口
        proxy_set_header Host $host;
		#远程地址
        proxy_set_header X-Real-IP $remote_addr;
		#程序可获取远程ip地址
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		#此处会用的upstream.conf,此文件在nginx.conf已经引入了
        proxy_pass http://snowflake;
    }

负载均衡配置upstream.conf文件增加下面的配置,其中snowflake 就是localhost_80.conf文件里配置的http://snowflake;

	upstream snowflake {
	    server 127.0.0.1:6061 ;
		server 127.0.0.1:6062 ;
		server 127.0.0.1:6063 ;
	}

如果你的nginx已经启动,最后记得使用命令 nginx -t 检查和 nginx -s reload 应用。如果不懂的可以参考我另一篇文章:windows下Nginx配置及负载均衡使用

5.3、使用jmeter并发测试

  本文中使用使用100个线程请求我的接口获取id,150表示线程数,0表示0秒内一起发送,1表示请求循环的次数。
在这里插入图片描述
我们请求的地址是:http://localhost/snowflake/distributed/generateId,注意是没有端口的,会通过nginx转发到后台实例。
在这里插入图片描述

5.4、测试结果

端口为6061实例的结果:

2021-11-17 17:38:54 170 [http-nio-6061-exec-24] INFO generateId 27:获取到的分布式id:2440808817528832
2021-11-17 17:38:54 170 [http-nio-6061-exec-8] INFO generateId 27:获取到的分布式id:2440808817528836
2021-11-17 17:38:54 171 [http-nio-6061-exec-26] INFO generateId 27:获取到的分布式id:2440808821723136
2021-11-17 17:38:54 170 [http-nio-6061-exec-3] INFO generateId 27:获取到的分布式id:2440808817528835
2021-11-17 17:38:54 171 [http-nio-6061-exec-1] INFO generateId 27:获取到的分布式id:2440808821723140
2021-11-17 17:38:54 171 [http-nio-6061-exec-2] INFO generateId 27:获取到的分布式id:2440808821723139
2021-11-17 17:38:54 171 [http-nio-6061-exec-22] INFO generateId 27:获取到的分布式id:2440808821723138
2021-11-17 17:38:54 170 [http-nio-6061-exec-40] INFO generateId 27:获取到的分布式id:2440808817528833
2021-11-17 17:38:54 170 [http-nio-6061-exec-34] INFO generateId 27:获取到的分布式id:2440808817528834
2021-11-17 17:38:54 171 [http-nio-6061-exec-49] INFO generateId 27:获取到的分布式id:2440808821723143
2021-11-17 17:38:54 171 [http-nio-6061-exec-29] INFO generateId 27:获取到的分布式id:2440808821723146
2021-11-17 17:38:54 171 [http-nio-6061-exec-48] INFO generateId 27:获取到的分布式id:2440808821723144
2021-11-17 17:38:54 171 [http-nio-6061-exec-17] INFO generateId 27:获取到的分布式id:2440808821723145
2021-11-17 17:38:54 171 [http-nio-6061-exec-36] INFO generateId 27:获取到的分布式id:2440808821723142
2021-11-17 17:38:54 171 [http-nio-6061-exec-20] INFO generateId 27:获取到的分布式id:2440808821723137
2021-11-17 17:38:54 172 [http-nio-6061-exec-35] INFO generateId 27:获取到的分布式id:2440808825917441
2021-11-17 17:38:54 172 [http-nio-6061-exec-4] INFO generateId 27:获取到的分布式id:2440808825917442
2021-11-17 17:38:54 172 [http-nio-6061-exec-38] INFO generateId 27:获取到的分布式id:2440808825917443
2021-11-17 17:38:54 171 [http-nio-6061-exec-43] INFO generateId 27:获取到的分布式id:2440808821723141
2021-11-17 17:38:54 172 [http-nio-6061-exec-41] INFO generateId 27:获取到的分布式id:2440808825917448
2021-11-17 17:38:54 172 [http-nio-6061-exec-11] INFO generateId 27:获取到的分布式id:2440808825917447
2021-11-17 17:38:54 172 [http-nio-6061-exec-13] INFO generateId 27:获取到的分布式id:2440808825917449
2021-11-17 17:38:54 172 [http-nio-6061-exec-28] INFO generateId 27:获取到的分布式id:2440808825917440
2021-11-17 17:38:54 172 [http-nio-6061-exec-44] INFO generateId 27:获取到的分布式id:2440808825917444
2021-11-17 17:38:54 173 [http-nio-6061-exec-31] INFO generateId 27:获取到的分布式id:2440808830111744
2021-11-17 17:38:54 173 [http-nio-6061-exec-16] INFO generateId 27:获取到的分布式id:2440808830111745
2021-11-17 17:38:54 173 [http-nio-6061-exec-9] INFO generateId 27:获取到的分布式id:2440808830111746
2021-11-17 17:38:54 172 [http-nio-6061-exec-39] INFO generateId 27:获取到的分布式id:2440808825917446
2021-11-17 17:38:54 173 [http-nio-6061-exec-27] INFO generateId 27:获取到的分布式id:2440808830111747
2021-11-17 17:38:54 173 [http-nio-6061-exec-37] INFO generateId 27:获取到的分布式id:2440808830111750
2021-11-17 17:38:54 173 [http-nio-6061-exec-12] INFO generateId 27:获取到的分布式id:2440808830111749
2021-11-17 17:38:54 173 [http-nio-6061-exec-18] INFO generateId 27:获取到的分布式id:2440808830111748
2021-11-17 17:38:54 173 [http-nio-6061-exec-7] INFO generateId 27:获取到的分布式id:2440808830111751
2021-11-17 17:38:54 172 [http-nio-6061-exec-19] INFO generateId 27:获取到的分布式id:2440808825917445
2021-11-17 17:38:54 173 [http-nio-6061-exec-10] INFO generateId 27:获取到的分布式id:2440808830111752
2021-11-17 17:38:54 173 [http-nio-6061-exec-15] INFO generateId 27:获取到的分布式id:2440808830111753
2021-11-17 17:38:54 173 [http-nio-6061-exec-50] INFO generateId 27:获取到的分布式id:2440808830111754
2021-11-17 17:38:54 173 [http-nio-6061-exec-46] INFO generateId 27:获取到的分布式id:2440808830111756
2021-11-17 17:38:54 173 [http-nio-6061-exec-23] INFO generateId 27:获取到的分布式id:2440808830111755
2021-11-17 17:38:54 173 [http-nio-6061-exec-6] INFO generateId 27:获取到的分布式id:2440808830111757
2021-11-17 17:38:54 173 [http-nio-6061-exec-21] INFO generateId 27:获取到的分布式id:2440808830111759
2021-11-17 17:38:54 173 [http-nio-6061-exec-42] INFO generateId 27:获取到的分布式id:2440808830111758
2021-11-17 17:38:54 173 [http-nio-6061-exec-45] INFO generateId 27:获取到的分布式id:2440808830111760
2021-11-17 17:38:54 174 [http-nio-6061-exec-47] INFO generateId 27:获取到的分布式id:2440808834306049
2021-11-17 17:38:54 174 [http-nio-6061-exec-33] INFO generateId 27:获取到的分布式id:2440808834306050
2021-11-17 17:38:54 174 [http-nio-6061-exec-25] INFO generateId 27:获取到的分布式id:2440808834306051
2021-11-17 17:38:54 174 [http-nio-6061-exec-5] INFO generateId 27:获取到的分布式id:2440808834306052
2021-11-17 17:38:54 174 [http-nio-6061-exec-32] INFO generateId 27:获取到的分布式id:2440808834306048
2021-11-17 17:38:54 174 [http-nio-6061-exec-30] INFO generateId 27:获取到的分布式id:2440808834306053
2021-11-17 17:38:54 174 [http-nio-6061-exec-14] INFO generateId 27:获取到的分布式id:2440808834306054

端口为6062实例的结果:

2021-11-17 17:38:54 195 [http-nio-6062-exec-50] INFO generateId 27:获取到的分布式id:2440808922390531
2021-11-17 17:38:54 195 [http-nio-6062-exec-40] INFO generateId 27:获取到的分布式id:2440808922390533
2021-11-17 17:38:54 196 [http-nio-6062-exec-45] INFO generateId 27:获取到的分布式id:2440808926584834
2021-11-17 17:38:54 196 [http-nio-6062-exec-34] INFO generateId 27:获取到的分布式id:2440808926584835
2021-11-17 17:38:54 195 [http-nio-6062-exec-8] INFO generateId 27:获取到的分布式id:2440808922390529
2021-11-17 17:38:54 196 [http-nio-6062-exec-37] INFO generateId 27:获取到的分布式id:2440808926584832
2021-11-17 17:38:54 195 [http-nio-6062-exec-21] INFO generateId 27:获取到的分布式id:2440808922390530
2021-11-17 17:38:54 195 [http-nio-6062-exec-41] INFO generateId 27:获取到的分布式id:2440808922390532
2021-11-17 17:38:54 195 [http-nio-6062-exec-18] INFO generateId 27:获取到的分布式id:2440808922390528
2021-11-17 17:38:54 196 [http-nio-6062-exec-5] INFO generateId 27:获取到的分布式id:2440808926584836
2021-11-17 17:38:54 196 [http-nio-6062-exec-35] INFO generateId 27:获取到的分布式id:2440808926584837
2021-11-17 17:38:54 196 [http-nio-6062-exec-12] INFO generateId 27:获取到的分布式id:2440808926584838
2021-11-17 17:38:54 196 [http-nio-6062-exec-39] INFO generateId 27:获取到的分布式id:2440808926584840
2021-11-17 17:38:54 196 [http-nio-6062-exec-22] INFO generateId 27:获取到的分布式id:2440808926584839
2021-11-17 17:38:54 196 [http-nio-6062-exec-14] INFO generateId 27:获取到的分布式id:2440808926584833
2021-11-17 17:38:54 199 [http-nio-6062-exec-6] INFO generateId 27:获取到的分布式id:2440808939167744
2021-11-17 17:38:54 197 [http-nio-6062-exec-17] INFO generateId 27:获取到的分布式id:2440808930779136
2021-11-17 17:38:54 199 [http-nio-6062-exec-25] INFO generateId 27:获取到的分布式id:2440808939167745
2021-11-17 17:38:54 199 [http-nio-6062-exec-24] INFO generateId 27:获取到的分布式id:2440808939167746
2021-11-17 17:38:54 200 [http-nio-6062-exec-4] INFO generateId 27:获取到的分布式id:2440808943362048
2021-11-17 17:38:54 200 [http-nio-6062-exec-3] INFO generateId 27:获取到的分布式id:2440808943362050
2021-11-17 17:38:54 200 [http-nio-6062-exec-23] INFO generateId 27:获取到的分布式id:2440808943362049
2021-11-17 17:38:54 200 [http-nio-6062-exec-9] INFO generateId 27:获取到的分布式id:2440808943362051
2021-11-17 17:38:54 200 [http-nio-6062-exec-48] INFO generateId 27:获取到的分布式id:2440808943362052
2021-11-17 17:38:54 200 [http-nio-6062-exec-29] INFO generateId 27:获取到的分布式id:2440808943362054
2021-11-17 17:38:54 200 [http-nio-6062-exec-16] INFO generateId 27:获取到的分布式id:2440808943362055
2021-11-17 17:38:54 201 [http-nio-6062-exec-1] INFO generateId 27:获取到的分布式id:2440808947556352
2021-11-17 17:38:54 200 [http-nio-6062-exec-20] INFO generateId 27:获取到的分布式id:2440808943362056
2021-11-17 17:38:54 200 [http-nio-6062-exec-28] INFO generateId 27:获取到的分布式id:2440808943362053
2021-11-17 17:38:54 201 [http-nio-6062-exec-30] INFO generateId 27:获取到的分布式id:2440808947556353
2021-11-17 17:38:54 201 [http-nio-6062-exec-31] INFO generateId 27:获取到的分布式id:2440808947556354
2021-11-17 17:38:54 201 [http-nio-6062-exec-27] INFO generateId 27:获取到的分布式id:2440808947556355
2021-11-17 17:38:54 201 [http-nio-6062-exec-46] INFO generateId 27:获取到的分布式id:2440808947556356
2021-11-17 17:38:54 201 [http-nio-6062-exec-44] INFO generateId 27:获取到的分布式id:2440808947556357
2021-11-17 17:38:54 201 [http-nio-6062-exec-33] INFO generateId 27:获取到的分布式id:2440808947556358
2021-11-17 17:38:54 201 [http-nio-6062-exec-36] INFO generateId 27:获取到的分布式id:2440808947556359
2021-11-17 17:38:54 201 [http-nio-6062-exec-26] INFO generateId 27:获取到的分布式id:2440808947556361
2021-11-17 17:38:54 201 [http-nio-6062-exec-43] INFO generateId 27:获取到的分布式id:2440808947556362
2021-11-17 17:38:54 201 [http-nio-6062-exec-32] INFO generateId 27:获取到的分布式id:2440808947556360
2021-11-17 17:38:54 202 [http-nio-6062-exec-7] INFO generateId 27:获取到的分布式id:2440808951750656
2021-11-17 17:38:54 202 [http-nio-6062-exec-19] INFO generateId 27:获取到的分布式id:2440808951750657
2021-11-17 17:38:54 202 [http-nio-6062-exec-38] INFO generateId 27:获取到的分布式id:2440808951750658
2021-11-17 17:38:54 202 [http-nio-6062-exec-13] INFO generateId 27:获取到的分布式id:2440808951750659
2021-11-17 17:38:54 203 [http-nio-6062-exec-42] INFO generateId 27:获取到的分布式id:2440808955944960
2021-11-17 17:38:54 203 [http-nio-6062-exec-49] INFO generateId 27:获取到的分布式id:2440808955944961
2021-11-17 17:38:54 203 [http-nio-6062-exec-2] INFO generateId 27:获取到的分布式id:2440808955944962
2021-11-17 17:38:54 204 [http-nio-6062-exec-15] INFO generateId 27:获取到的分布式id:2440808960139264
2021-11-17 17:38:54 205 [http-nio-6062-exec-10] INFO generateId 27:获取到的分布式id:2440808964333568
2021-11-17 17:38:54 205 [http-nio-6062-exec-47] INFO generateId 27:获取到的分布式id:2440808964333569
2021-11-17 17:38:54 205 [http-nio-6062-exec-11] INFO generateId 27:获取到的分布式id:2440808964333570

端口为6063实例的结果:

2021-11-17 17:38:54 167 [http-nio-6063-exec-17] INFO generateId 27:获取到的分布式id:2440808804954129
2021-11-17 17:38:54 167 [http-nio-6063-exec-5] INFO generateId 27:获取到的分布式id:2440808804954120
2021-11-17 17:38:54 168 [http-nio-6063-exec-33] INFO generateId 27:获取到的分布式id:2440808809148416
2021-11-17 17:38:54 168 [http-nio-6063-exec-25] INFO generateId 27:获取到的分布式id:2440808809148422
2021-11-17 17:38:54 167 [http-nio-6063-exec-46] INFO generateId 27:获取到的分布式id:2440808804954124
2021-11-17 17:38:54 167 [http-nio-6063-exec-30] INFO generateId 27:获取到的分布式id:2440808804954116
2021-11-17 17:38:54 168 [http-nio-6063-exec-48] INFO generateId 27:获取到的分布式id:2440808809148423
2021-11-17 17:38:54 168 [http-nio-6063-exec-8] INFO generateId 27:获取到的分布式id:2440808809148425
2021-11-17 17:38:54 167 [http-nio-6063-exec-38] INFO generateId 27:获取到的分布式id:2440808804954126
2021-11-17 17:38:54 168 [http-nio-6063-exec-44] INFO generateId 27:获取到的分布式id:2440808809148427
2021-11-17 17:38:54 168 [http-nio-6063-exec-29] INFO generateId 27:获取到的分布式id:2440808809148428
2021-11-17 17:38:54 167 [http-nio-6063-exec-47] INFO generateId 27:获取到的分布式id:2440808804954118
2021-11-17 17:38:54 168 [http-nio-6063-exec-11] INFO generateId 27:获取到的分布式id:2440808809148430
2021-11-17 17:38:54 167 [http-nio-6063-exec-32] INFO generateId 27:获取到的分布式id:2440808804954123
2021-11-17 17:38:54 167 [http-nio-6063-exec-20] INFO generateId 27:获取到的分布式id:2440808804954130
2021-11-17 17:38:54 168 [http-nio-6063-exec-13] INFO generateId 27:获取到的分布式id:2440808809148420
2021-11-17 17:38:54 168 [http-nio-6063-exec-45] INFO generateId 27:获取到的分布式id:2440808809148418
2021-11-17 17:38:54 167 [http-nio-6063-exec-39] INFO generateId 27:获取到的分布式id:2440808804954122
2021-11-17 17:38:54 167 [http-nio-6063-exec-35] INFO generateId 27:获取到的分布式id:2440808804954115
2021-11-17 17:38:54 168 [http-nio-6063-exec-9] INFO generateId 27:获取到的分布式id:2440808809148421
2021-11-17 17:38:54 167 [http-nio-6063-exec-16] INFO generateId 27:获取到的分布式id:2440808804954125
2021-11-17 17:38:54 167 [http-nio-6063-exec-34] INFO generateId 27:获取到的分布式id:2440808804954127
2021-11-17 17:38:54 169 [http-nio-6063-exec-10] INFO generateId 27:获取到的分布式id:2440808813342721
2021-11-17 17:38:54 167 [http-nio-6063-exec-36] INFO generateId 27:获取到的分布式id:2440808804954114
2021-11-17 17:38:54 167 [http-nio-6063-exec-28] INFO generateId 27:获取到的分布式id:2440808804954121
2021-11-17 17:38:54 167 [http-nio-6063-exec-4] INFO generateId 27:获取到的分布式id:2440808804954117
2021-11-17 17:38:54 167 [http-nio-6063-exec-24] INFO generateId 27:获取到的分布式id:2440808804954128
2021-11-17 17:38:54 167 [http-nio-6063-exec-41] INFO generateId 27:获取到的分布式id:2440808804954119
2021-11-17 17:38:54 167 [http-nio-6063-exec-22] INFO generateId 27:获取到的分布式id:2440808804954113
2021-11-17 17:38:54 169 [http-nio-6063-exec-40] INFO generateId 27:获取到的分布式id:2440808813342720
2021-11-17 17:38:54 168 [http-nio-6063-exec-19] INFO generateId 27:获取到的分布式id:2440808809148429
2021-11-17 17:38:54 169 [http-nio-6063-exec-43] INFO generateId 27:获取到的分布式id:2440808813342723
2021-11-17 17:38:54 170 [http-nio-6063-exec-12] INFO generateId 27:获取到的分布式id:2440808817537025
2021-11-17 17:38:54 170 [http-nio-6063-exec-6] INFO generateId 27:获取到的分布式id:2440808817537026
2021-11-17 17:38:54 169 [http-nio-6063-exec-15] INFO generateId 27:获取到的分布式id:2440808813342722
2021-11-17 17:38:54 168 [http-nio-6063-exec-26] INFO generateId 27:获取到的分布式id:2440808809148426
2021-11-17 17:38:54 170 [http-nio-6063-exec-2] INFO generateId 27:获取到的分布式id:2440808817537024
2021-11-17 17:38:54 170 [http-nio-6063-exec-50] INFO generateId 27:获取到的分布式id:2440808817537027
2021-11-17 17:38:54 167 [http-nio-6063-exec-23] INFO generateId 27:获取到的分布式id:2440808804954112
2021-11-17 17:38:54 170 [http-nio-6063-exec-27] INFO generateId 27:获取到的分布式id:2440808817537028
2021-11-17 17:38:54 168 [http-nio-6063-exec-1] INFO generateId 27:获取到的分布式id:2440808809148424
2021-11-17 17:38:54 168 [http-nio-6063-exec-18] INFO generateId 27:获取到的分布式id:2440808809148419
2021-11-17 17:38:54 168 [http-nio-6063-exec-37] INFO generateId 27:获取到的分布式id:2440808809148417
2021-11-17 17:38:54 170 [http-nio-6063-exec-21] INFO generateId 27:获取到的分布式id:2440808817537029
2021-11-17 17:38:54 171 [http-nio-6063-exec-7] INFO generateId 27:获取到的分布式id:2440808821731328
2021-11-17 17:38:54 171 [http-nio-6063-exec-42] INFO generateId 27:获取到的分布式id:2440808821731329
2021-11-17 17:38:54 171 [http-nio-6063-exec-14] INFO generateId 27:获取到的分布式id:2440808821731330
2021-11-17 17:38:54 171 [http-nio-6063-exec-3] INFO generateId 27:获取到的分布式id:2440808821731331
2021-11-17 17:38:54 171 [http-nio-6063-exec-49] INFO generateId 27:获取到的分布式id:2440808821731332
2021-11-17 17:38:54 171 [http-nio-6063-exec-31] INFO generateId 27:获取到的分布式id:2440808821731333

六、Snowflake优缺点

6.1、优点

  • ID在内存生成,不依赖于数据库,高性能高可用。
  • 每秒可生成几百万ID,容量大
  • 由于ID呈趋势递增,插入数据库后,使用索引的时候性能较高。

6.2、缺点

  • 依赖于系统时钟的一致性,如果某台机器的系统时钟回拨,有可能造成ID冲突或者ID乱序。
  • 同一台机器的系统时间回拨过,那么有可能出现ID重复的情况

结语

  其实在这里大家也看到了我们在使用snowflake的一个局限性,一个是数据中心只有32,机器位也只有32,当然大家可以扩展,实际上机器位32对于绝大大部分公司来说足够了,只是数据中心可能会不够,那么就向时间戳那里借几位,比如你借两位就可以支持128了,当然相应的时间戳可用时间也缩短成( (1L << 39) / (1000L * 60 * 60 * 24 * 365)=17年),大家根据需求自行选择。
  还有一个就是实际使用这个workerId时其实不是很好维护,当然如果有用到数据库或者redis那维护就更容易了,为了不把本文搞复杂,本文就这么写,大家就参考下实现的思路吧,不管怎么样,本服务不管是单台机器部署多个,还是不同机器部署多个都不会有问题,包括单机,分布式,容器环境,当然有兴趣的小伙伴可以使用美团的 leaf-Snowflake

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐