分布式ID之雪花算法

news2024/9/27 9:30:34

分布式ID常见生成策略

分布式ID生成策略常见的有如下几种:

  • 数据库自增ID。
  • UUID生成。
  • Redis的原子自增方式。
  • 数据库水平拆分,设置初始值和相同的自增步长。
  • 批量申请自增ID。
  • 雪花算法。
  • 百度UidGenerator算法(基于雪花算法实现自定义时间戳)。
  • 美团Leaf算法(依赖于数据库,ZK)。

本文主要介绍SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。

其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,保持自增性且不重复。

雪花算法的结构:

雪花算法的结构如下:
在这里插入图片描述

主要分为 5 个部分:

  1. 是 1 个 bit:0,这个是无意义的。
  2. 是 41 个 bit:表示的是时间戳。
  3. 是 10 个 bit:表示的是机房 id,0000000000,因为我传进去的就是0。
  4. 是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 0000 0000。

接下去我们来解释一下四个部分:

  • 1 bit,是无意义的:

因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

  • 41 bit:表示的是时间戳,单位是毫秒。

41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。

  • 10 bit:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。

但是 10 bit 里 5 个 bit 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房),每个机房里可以代表 2 ^ 5 个机器(32 台机器),这里可以随意拆分,比如拿出4位标识业务号,其他6位作为机器号。可以随意组合。

  • 12 bit:这个是用来记录同一个毫秒内产生的不同 id。

12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。也就是同一毫秒内同一台机器所生成的最大ID数量为4096

  • 简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。这个 SnowFlake 算法系统首先肯定是知道自己所在的机器号,(这里姑且讲10bit全部作为工作机器ID)接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。接着用当前时间戳(单位到毫秒)占用41 个 bit,然后接着 10 个 bit 设置机器 id。最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。

代码实现:

代码中将 10 bit 拆分成 5bit表示工作机器ID,5bit表示数据中心ID。

@Slf4j
public class SnowflakeIdWorker {

    // ==============================Fields===========================================
    /** 开始时间戳 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间戳向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间戳 */
    private long lastTimestamp = -1L;

    private final long maxAreaId = -1L ^ (-1L << datacenterIdBits);//最大能够分配的Areaid =31

    /**机器名**/
    private String hostName;
    /**应用名**/
    private String appName;
    @Value("${deploy.region}")
    private String areaName;
    @Value("${server.port}")
    private String port;
    @Value("${zookeeper.address}")
    private String zk;

    public boolean init() {

        appName  = Utils.getAppName();
        if(null == port)
            port    = Utils.getPort();
        hostName = Utils.getHostName();
        Assert.notNull(appName,"appName is null");
        Assert.notNull(hostName,"hostName is null");
        Assert.notNull(areaName,"areaName is null");
        Assert.notNull(port,"port is null");

        SnowflakeZookeeperHolder snowflakeZookeeperHolder = new SnowflakeZookeeperHolder(hostName,port,zk,"global",(int)maxWorkerId,appName);
        boolean init = snowflakeZookeeperHolder.init();
        Assert.isTrue(init,"init workId error!!!");

        workerId = snowflakeZookeeperHolder.getWorkerID();

        if(Utils.areaMap.containsKey(areaName.toUpperCase())) {
            datacenterId = Utils.areaMap.get(areaName.toUpperCase()).shortValue();
        }
        Assert.isTrue(datacenterId>0,"get areaId error!!!");

        Assert.isTrue(workerId >= 0 && workerId <= maxWorkerId , "workerID must gte 0 and lte "+maxWorkerId);
        Assert.isTrue(datacenterId >= 0 && datacenterId <= maxAreaId , "areaId must gte 0 and lte "+maxAreaId);
        log.info("snowflake global info:workId={} areaId={}",workerId,datacenterId);
        return true;
    }

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        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;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个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 - twepoch) << 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();
    }

    //==============================Test=============================================
    /** 测试 */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.RetryUntilElapsed;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SnowflakeZookeeperHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeZookeeperHolder.class);
    private String  zk_AddressNode  = null;
    private String  listenAddress   = null;
    private int     workerID;
    private static  final String PREFIX_ZK_PATH = "/snowflake";
    private static  String PATH_FOREVER;
    private String  connectionString;
    private long    lastUpdateTime;
    private String     port;
    private int     maxWordId;
    private String  lockPath;

    public SnowflakeZookeeperHolder(String hostName,String port, String connectionString,String snowTye,int maxWordId,String app) {
        this.listenAddress = hostName+":"+port;
        this.port = port;
        this.connectionString = connectionString;
        PATH_FOREVER = PREFIX_ZK_PATH + "/"+snowTye+"/"+app;
        this.maxWordId = maxWordId;
        this.lockPath = PREFIX_ZK_PATH + "/"+snowTye;
    }

    public boolean init() {
        try {
            CuratorFramework curator = createWithOptions(connectionString, new RetryUntilElapsed(1000, 4), 10000, 6000);
            curator.start();

            InterProcessMutex lock = new InterProcessMutex(curator,lockPath);

            try {
                if(lock.acquire(2,TimeUnit.SECONDS)) {
                    Stat stat = curator.checkExists().forPath(PATH_FOREVER);
                    if (stat == null) {
                        //不存在根节点,机器第一次启动,并上传数据
                        zk_AddressNode = createNode(curator);
                        //保存workID
                        updateNewData(curator, zk_AddressNode);
                    }else {
                        ArrayList<Integer> usedWorkIddSet= new ArrayList<>();
                        //存在根节点,先检查是否有属于自己的根节点
                        List<String> keys = curator.getChildren().forPath(PATH_FOREVER);
                        //临时节点未找到当前节点
                        if(!keys.contains(listenAddress)) {
                            for (String key : keys) {
                                Endpoint endpoint = getNodeData(curator, PATH_FOREVER + "/" + key);
                                if (null != endpoint) {
                                    usedWorkIddSet.add(endpoint.getWorkId());
                                }
                            }
                            if(keys.size() == 0){
                                zk_AddressNode = createNode(curator);
                                updateNewData(curator, zk_AddressNode);
                            }else {
                                ArrayList<Integer> allWordId = new ArrayList<>(maxWordId);
                                for (int i = 0; i <= maxWordId; i++) {
                                    allWordId.add(i);
                                }
                                allWordId.removeAll(usedWorkIddSet);
                                if(allWordId.size()<=0){
                                    LOGGER.error("workId全部被占用");
                                    return false;
                                }else{
                                    workerID = allWordId.get(0);
                                    zk_AddressNode = createNode(curator);
                                    //保存workID
                                    updateNewData(curator, zk_AddressNode);
                                }
                            }
                        }else{
                            zk_AddressNode = PATH_FOREVER + "/" + listenAddress;
                            Endpoint endpoint = getNodeData(curator, zk_AddressNode);
                            workerID = endpoint.getWorkId();
                            updateNewData(curator, zk_AddressNode);
                        }
                    }
                }else{
                    LOGGER.error("get zk lock fail!!!");
                    return false;
                }
            }catch (Exception e){
                LOGGER.error("get zk lock ERROR {}", e);
                return false;
            }finally {
                lock.release();
            }

        } catch (Exception e) {
            LOGGER.error("Start node ERROR {}", e);
            return false;
        }
        LOGGER.info(PATH_FOREVER+" workId="+workerID);
        return true;
    }


    private Endpoint getNodeData(CuratorFramework curator, String zk_AddressNode) throws Exception {
        byte[] bytes = curator.getData().forPath(zk_AddressNode);
        Endpoint endPoint = deBuildData(new String(bytes));
        return endPoint;
    }

    /**
     * 创建持久顺序节点 ,并把节点数据放入 value
     *
     * @param curator
     * @return
     * @throws Exception
     */
    private String createNode(CuratorFramework curator) throws Exception {
        try {
            return curator.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(PATH_FOREVER + "/" + listenAddress, buildData().getBytes());
        } catch (Exception e) {
            LOGGER.error("create node error msg {} ", e.getMessage());
            throw e;
        }
    }

    private void updateNewData(CuratorFramework curator, String path){
        try {
            if (System.currentTimeMillis() < lastUpdateTime) {
                return;
            }
            curator.setData().forPath(path, buildData().getBytes());
            lastUpdateTime = System.currentTimeMillis();
        } catch (Exception e) {
            try {
                init();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            LOGGER.info("update init data error path is {} error is {}", path, e);
        }
    }

    /**
     * 构建需要上传的数据
     *
     * @return
     */
    private String buildData() throws JsonProcessingException {
        Endpoint endpoint = new Endpoint(workerID, System.currentTimeMillis());
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(endpoint);
        return json;
    }

    private Endpoint deBuildData(String json) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        Endpoint endpoint = mapper.readValue(json, Endpoint.class);
        return endpoint;
    }


    private CuratorFramework createWithOptions(String connectionString, RetryPolicy retryPolicy, int connectionTimeoutMs, int sessionTimeoutMs) {
        return CuratorFrameworkFactory.builder().connectString(connectionString)
            .retryPolicy(retryPolicy)
            .connectionTimeoutMs(connectionTimeoutMs)
            .sessionTimeoutMs(sessionTimeoutMs)
            .build();
    }

    /**
     * 上报数据结构
     */
    static class Endpoint {
        private int workId;
        private long timestamp;

        public Endpoint() {
        }

        public Endpoint(int workId, long timestamp) {
            this.workId = workId;
            this.timestamp = timestamp;
        }

        public int getWorkId() {
            return workId;
        }

        public void setWorkId(int workId) {
            this.workId = workId;
        }

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }
    }

    public String getZk_AddressNode() {
        return zk_AddressNode;
    }

    public void setZk_AddressNode(String zk_AddressNode) {
        this.zk_AddressNode = zk_AddressNode;
    }

    public String getListenAddress() {
        return listenAddress;
    }

    public void setListenAddress(String listenAddress) {
        this.listenAddress = listenAddress;
    }

    public int getWorkerID() {
        return workerID;
    }

    public void setWorkerID(int workerID) {
        this.workerID = workerID;
    }
}

总结

SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。但是依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务。

其实对于分布式ID的生成策略。无论是我们上述提到的哪一种。无非需要具有以下两种特点。 自增的、不重复的。而对于不重复且是自增的,那么我们是很容易想到的是时间,而雪花算法就是基于时间戳。但是毫秒级的并发下如果直接拿来用,显然是不合理的。那么我们就要在这个时间戳上面做一些文章。至于怎么能让这个东西保持唯一且自增。就要打开自己的脑洞了。可以看到雪花算法中是基于 synchronized 锁进行实现的。

鸣谢

雪花算法原理解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/139958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【ZooKeeper】第一章 快速入门

【ZooKeeper】第一章 快速入门 文章目录【ZooKeeper】第一章 快速入门一、概念二、安装1.环境准备2.安装3.配置二、命令操作1.数据模型2.服务端常用命令3.客户端常用命令一、概念 ZooKeeper 是 Apache Hadoop 项目下的一个子项目&#xff0c;是一个树形目录服务ZooKeeper 翻译…

SpringBoot使用SchedulingConfigurer实现多个定时任务多机器部署问题

目录一、使用SchedulingConfigurer实现多个定时任务二、定时任务多机器部署解决方案三、基于redis实现的代码示例3.1、基于redis实现的概述3.2、基于redis实现的代码3.2.1、代码目录结构3.2.2、引入依赖包3.2.3、配置文件新增redis连接配置3.2.4、自定义redis锁注解类3.2.5、自…

Linux 块设备驱动

1.块设备是针对存储设备的&#xff0c;比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动&#xff0c;块设备驱动相比字符设备驱动的主要区别如下&#xff1a; ①、块设备只能以块为单位进行读写访问&am…

【阶段二】Python数据分析Pandas工具使用07篇:探索性数据分析:数据的描述:数据的集中趋势

本篇的思维导图: 探索性数据分析:数据的描述 数据的描述是为了让数据使用者或开发者更加了解数据,进而做到“心中有数”,其描述过程侧重于统计运算和统计绘图。通过统计运算可以得到具体的数据特征,如反映集中趋势中的均值水平、中位数、分位数和众数等;反映分散趋势的方…

Unity脚本 --- VS调试工具

一般游戏逻辑调试的时候用的都是VS调试工具来进行调试 1.在Unity脚本中启动调试后并不会立刻开始调试&#xff0c;还需要我们在Unity中点击play&#xff08;游戏运行&#xff09;后调试才会开始进行 2.在调试的时候点击f11可以逐语句调试&#xff0c;同时当我们在调试的时候想…

螺旋桨k线的意义?

相信大家即使没坐过直升机&#xff0c;也很看见过螺旋桨吧&#xff1f;它的动能巨大&#xff0c;刮起的旋风能支撑起一架飞机的升降。但大家是否知道&#xff0c;在K线技术分析中&#xff0c;也有一种特殊的形态叫“螺旋桨”呢&#xff1f; 三、螺旋桨K线的形态概念 如下图&am…

2023-1-4目前市面上存在的树莓派rp2040控制器

目前市面上存在的树莓派rp2040控制器 1、树莓派pico原装 2、微雪rp2040 3、Ultimate pico rp2040兼容树莓派pico RaspberryPi Pico是一款低成本&#xff0c;高性能的微控制器开发板&#xff0c;具有灵活数字接口。硬件上&#xff0c;采用Raspberry Pi官方自主研发的RP2040微控…

高精度PWM脉宽调制信号转模拟信号隔离变送器0-5V/0-10V/1-5V,0-10mA/0-20mA/4-20mA

主要特性:>>精度等级&#xff1a;0.1级。产品出厂前已检验校正&#xff0c;用户可以直接使用>>辅助电源&#xff1a;8-32V 宽范围供电>>PWM脉宽调制信号输入: 1Hz~10KHz>>输出标准信号&#xff1a;0-5V/0-10V/1-5V,0-10mA/0-20mA/4-20mA等&#xff0c;…

国家法定节假日安排,节假日查询API接口有哪些?

节假日&#xff0c;对于我们每个人来说都息息相关。特别是国家法定节假日的安排&#xff0c;大家都希望清楚知道并合理安排好。因为&#xff0c;节假日是国务院统一安排的&#xff0c;我们就为此编写了这样一个节假日查询API接口&#xff0c;供大家方便查询。 节假日API接口正广…

佳能6D误格式化覆盖后的恢复方法

佳能6D算是佳能众多摄像机中的明星机型&#xff0c;销量很大。同样遇到的问题也很多&#xff0c;今天要说的案例就是佳能6D格式化覆盖后的恢复案例&#xff0c;比较特殊的是其结构部分全部覆盖了&#xff0c;下面我们来看看这个案例。故障存储:64G sd卡&#xff0c;采用exFAT文…

软件项目如何进行任务分配,减少冲突?

1、任务分配并非平均分配 项目经理往往有个认知误区&#xff0c;认为把工作任务详细分解&#xff0c;平均分配给每个人&#xff0c;不偏不倚&#xff0c;公平公正就没有问题。但在实际开发过程中&#xff0c;这样很容易出问题&#xff0c;尤其是到项目后期&#xff0c;掉链子的…

Python中的时间序列数据操作总结

时间序列数据是一种在一段时间内收集的数据类型&#xff0c;它通常用于金融、经济学和气象学等领域&#xff0c;经常通过分析来了解随着时间的推移的趋势和模式 Pandas是Python中一个强大且流行的数据操作库&#xff0c;特别适合处理时间序列数据。它提供了一系列工具和函数可以…

意想不到的前端三个小妙招

大厂面试题分享 面试题库前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面试题库整理下本人在工作中撸代码遇到的一些刚看时一脸懵逼&#xff0c;实则很简单就能解决的小妙招&#xff0c;希望对大家有所帮助哟~伪元素动态改变…

「诗人艺术家ll著名诗人」胭脂茉莉十四行诗10首

【诗人艺术家ll著名诗人】胭脂茉莉十四行10首胭脂茉莉诗人简介&#xff1a;胭脂茉莉&#xff0c;女&#xff0c;江苏人&#xff0c;年少习诗&#xff0c;作家、诗人。评论及随笔见诸媒体及报刊&#xff0c;诗歌被选编入海内外多种选本及刊物&#xff0c;其主要代表作有现代禅诗…

vue3-init

Vue3快速上手 1.Vue3简介 2020年9月18日&#xff0c;Vue.js发布3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;海贼王&#xff09;耗时2年多、2600次提交、30个RFC、600次PR、99位贡献者github上的tags地址&#xff1a;https://github.com/vuejs/vue-next/release…

maven中的scope

provided: 编译运行时期&#xff0c;目标容器已经提供&#xff0c;打jar包时候不带optional&#xff0c;依赖传递test: 举例子junit&#xff0c;为什么Test在src的java蓝包的测试类的方法上面不能用&#xff1f;src的java绿包里的测试类的方法上可以用。 依赖传递&#xff1a;间…

【数据篇】32 # 如何选择合适的方法对数据进行可视化处理?

说明 【跟月影学可视化】学习笔记。 从原始数据中过滤出有用的信息 下面通过航拍公园人群分布例子&#xff0c;按照某些属性对数据进行过滤&#xff0c;再将符合条件的结果展现出来。 数据来源&#xff1a;https://github.com/akira-cn/graphics/blob/master/data/park-peo…

Java on Azure Tooling 2022年12月更新|Azure SDK 参考书代码样例支持及用户体验提升

作者&#xff1a;Jialuo Gan - Program Manager, Developer Division at Microsoft 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎回到12月的 Java on Azure Tooling 的更新。首先&#xff0c;提前祝大家新年快乐。在这次更新中&#xff0c;我们将介绍 Azure SDK 参考书的…

Linux系统之部署MxsDoc个人文件管理系统

Linux系统之部署MxsDoc个人文件管理系统一、MxsDoc介绍1.MxsDoc简介2.MxsDoc功能3.MxsDoc应用场景二、检查本地系统环境1.检查系统版本2.检查系统内核三、下载MxsDoc软件包1.创建软件目录2.下载MxsDoc软件3.查看下载软件四、部署MxsDoc1.解压MxsDoc软件包2.一键部署MxsDoc五、登…

【Linux逻辑卷管理】之pvcreate、pvdisplay和pvremove

文章目录一、逻辑卷管理(LVM)概念1. LVM 定义2. 物理卷(PV)3. 卷组(VG)4. 逻辑卷(LV)二、创建逻辑卷1. pvcreate命令将分区标记为物理卷&#xff0c;创建物理卷2. vgcreate命令将一个或多个物理卷结合为一个卷组&#xff0c;创建卷组3. lvcreate命令根据卷组中的可用物理区块,创…