ZooKeeper 实战(六) - 分布式ID实现方案

news2024/11/22 11:33:18

ZooKeeper 实战(六) - 生成分布式ID

文章目录

  • ZooKeeper 实战(六) - 生成分布式ID
    • 1.何为分布式ID
    • 2.分布式ID方案
    • 3.创建ZooKeeper节点
    • 4.获取序列ID
    • 5.处理序列ID
    • 6.使用分布式ID
    • 7.完整代码
    • 8.功能优化
      • 8.1.问题思考?
        • 1.容量问题
        • 2.并发问题
        • 3.内存问题
      • 8.2.解决并发问题
      • 8.3.内存问题

1.何为分布式ID

分布式唯一ID指在分布式系统中用于标识和区分各个实体、资源或事件的唯一标识符。由于分布式系统可能包含多个节点和多个并发操作,需要确保在整个系统中每个实体都具有唯一的标识,避免冲突和重复的情况。

分布式系统唯一ID的设计通常需要满足以下要求:

  1. 唯一性:每个ID在整个分布式系统中都是唯一的,不会发生冲突。
  2. 高并发性:ID的生成应该是高并发的,能应对绝大部分(接近于100%)的并发场景,不会成为系统的性能瓶颈。
  3. 可读性:ID尽可能具有可读性,方便开发人员和用户进行识别和使用。
  4. 可扩展性:能够适应分布式系统的扩展性需求,支持产生大规模、高并发的ID。
  5. 可排序性:ID可以按照一定规则进行排序,方便查询和排序操作。主键的排序性也能提升数据库索引的效率。数据库的索引结构通常是基于B树或B+树的,有序的主键可以保持索引结构的有序性,减少数据的分裂和平衡,从而提高索引的维护效率和查询性能。

2.分布式ID方案

使用ZooKeeper实现生成分布式ID可以保证分布式系统中每个节点生成的ID是唯一且递增的。以下是使用ZooKeeper实现生成分布式ID的基本步骤:

  1. 创建ZooKeeper节点:在ZooKeeper集群中创建一个顺序节点来实现全局递增的功能。节点路径可以选择一个统一的命名规则,例如"/appName/distributeStr"。
  2. 获取序列ID:每个节点需要在生成ID的时候,向ZooKeeper集群发起一个创建节点的请求。在请求的路径中添加顺序节点的标志,例如"/appName/distributeStr/id_"。ZooKeeper将为每个节点创建一个唯一的有序节点,并返回节点的路径。
  3. 处理序列ID:获取到ZooKeeper返回的节点路径后,需要解析路径中的序列号,也可以附加上某些信息,作为生成的分布式ID。
  4. 使用分布式ID:使用解析出的分布式ID进行业务处理。分布式ID可以在多个节点上同时生成,并且保证每个节点生成的ID是唯一且递增的。

3.创建ZooKeeper节点

问题来了,我们要明确创建一个什么样的ZooKeeper节点呢?首先,要知道ID是用来标识一个实体的,而不是作用于整个系统。比如一个系统中,实体有用户、商品、订单等,但是我们并不需要保证用户、商品和订单之间的ID是唯一的,而是要保证同一类实体比如用户这一个类,即任意两个用户A和B的ID不能重复。所以我们可以把系统中用于产生分布式ID的Znode的粒度控制在实体维度,例如/app/user 节点是用户实体的分布式ID节点,/app/product 节点是商品实体的分布式ID节点等。

    /**
     * 分布式ID节点缓存
     */
    private Map<String,String> NodePathMap = new HashMap<>();

    /**
     * 生成分布式ID节点路径
     * @param module 模块名称
     * @return
     */
    public String getIdNodePath(String module){
        if (module == null || module.isBlank()){
            throw new NullPointerException("请设置模块名称");
        }
        if (NodePathMap.get(module) == null){
            if (appName == null || appName.isBlank()){
                throw new NullPointerException("请设置系统名称");
            }
            synchronized (IdGenerator.class){
                if (NodePathMap.get(module) == null){
                    NodePathMap.put(module,"/"+appName+"/"+module);
                }
            }
        }
        return NodePathMap.get(module);
    }

4.获取序列ID

先通过上一节中的临时顺序节点的路径,向ZooKeeper集群发起一个创建节点的请求并返回节点的名称,最后去除节点路径的前缀,获取最后的序列号。

		/**
		 * 分布式ID节点的前缀
		 */
		public static final String ID_PREFIX = "id_";

    /**
     * 创建临时节点,并返回Id
     * @param module
     * @return
     */
    public String createNodeId(String module){
        String idNodePath = getIdNodePath(module);
        try {
            String idPrefix = idNodePath + "/" + ID_PREFIX;
            String id = client.create() // 创建节点
                    .creatingParentsIfNeeded() // 如果需要,递归创建节点
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL) // 指定创建节点类型,使用临时顺序节点
                    .forPath(idPrefix); // 设置节点路径
            // 去除nodeId的前缀
            return id.replace(idPrefix,"");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

5.处理序列ID

在ZK生成的临时顺序节点ID的前附加时日期时间,以便直观显示此ID的生成日期。

		/**
		 * 日期格式
		 */
		public static final String DATE_FORMAT = "yyyyMMdd";

    /**
     * 获取当天的日期
     * 格式:yyyyMMdd
     * @return
     */
    public String datePrefix(){
        DateTimeFormatter yyyyMMdd = DateTimeFormatter.ofPattern(DATE_FORMAT);
        LocalDateTime now = LocalDateTime.now();
        return now.format(yyyyMMdd);
    }

    /**
     * 生成id
     * @param module 模块
     * @return
     */
    public String nextId(String module){
      	// id前缀以日期开头
        return datePrefix()+createNodeId(module);
    }

6.使用分布式ID

一切从简,直接把测试方法写在启动类中😂。先启动对应的Zookeeper,然后启动同一个应用的多个实例,并发开始生成ID。

@Slf4j
@SpringBootApplication
public class CuratorDemoApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(CuratorDemoApplication.class, args);
    }

    @Autowired
    private IdGenerator idGenerator;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 为了保证两个应用能并发生成id,同时为了方便偷懒,不想写的很复杂
        // 这里控制在当前时间的分钟为06时开始往下执行
        while (new Date().getMinutes() != 6){
        }
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
        System.out.println("分布式id生成:"+idGenerator.nextId("test"));
    }

}

日志输出如下:

实例1

在这里插入图片描述

实例2

在这里插入图片描述

7.完整代码

/**
 * @Name: IdGenerator
 * @Description: 分布式id生成器
 * @Author: ahao
 * @Date: 2024/8/24 23:42
 */
@Component
public class IdGenerator {

    @Value("${spring.application.name}")
    private String appName;

    @Autowired
    private CuratorFramework client;

    /**
     * 分布式ID节点的前缀
     */
    public static final String ID_PREFIX = "id_";

    /**
     * 日期格式
     */
    public static final String DATE_FORMAT = "yyyyMMdd";

    /**
     * 分布式ID节点缓存
     * K -> 模块名称
     * V -> 分布式ID节点路径
     */
    private static final Map<String, String> NodePathMap = new HashMap<>();

    /**
     * 生成分布式ID节点路径
     *
     * @param module 模块名称
     * @return
     */
    public String getIdNodePath(String module) {
        if (module == null || module.isBlank()) {
            throw new NullPointerException("请设置模块名称");
        }
        if (NodePathMap.get(module) == null) {
            if (appName == null || appName.isBlank()) {
                throw new NullPointerException("请设置系统名称");
            }
            synchronized (IdGenerator.class) {
                if (NodePathMap.get(module) == null) {
                    NodePathMap.put(module, "/" + appName + "/" + module);
                }
            }
        }
        return NodePathMap.get(module);
    }

    /**
     * 创建临时节点,并返回Id
     *
     * @param module
     * @return
     */
    public String createNodeId(String module) {
        String idNodePath = getIdNodePath(module);
        // 创建节点
        try {
            String idPrefix = idNodePath + "/" + ID_PREFIX;
            String id = client.create()
                    // 如果需要,递归创建节点
                    .creatingParentsIfNeeded()
                    // 指定创建节点类型,使用临时顺序节点
                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
                    // 设置节点路径
                    .forPath(idPrefix);
            // 去除nodeId的前缀
            return id.replace(idPrefix, "");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取当天的日期
     * 格式:yyyyMMdd
     *
     * @return
     */
    public String datePrefix() {
        DateTimeFormatter yyyyMMdd = DateTimeFormatter.ofPattern(DATE_FORMAT);
        LocalDateTime now = LocalDateTime.now();
        return now.format(yyyyMMdd);
    }

    /**
     * 生成id
     *
     * @param module 模块
     * @return
     */
    public String nextId(String module) {
        return datePrefix() + createNodeId(module);
    }

}

8.功能优化

8.1.问题思考?

1.容量问题

在ZooKeeper中,临时顺序节点的序号长度默认为10个字符。这个长度是根据ZooKeeper解析生成的顺序节点路径时确定的。顺序节点路径由节点名称前缀和10位的顺序号组成。由此可见,最多可生成100亿的节点序列号,如果实体数据量超过了这个限度怎么办?

2.并发问题

还是一样的,每次生成分布式ID,都需要在Zookeeper创建一个临时顺序节点,而且每次请求都是远程调用,其并发性能远远低于本地调用。

3.内存问题

每次生成分布式ID,都需要在Zookeeper创建一个临时顺序节点,虽然说当客户端断开时,会自动删除这些遗留的节点。但是如果客户端生成id十分频繁,并且长期没有重启或者版本更新,这将会导致大量的”无效“节点存在,浪费了Zookeeper的内存资源‌,甚至会导致Zookeeper崩溃。

8.2.解决并发问题

如何把远程调用转变为本地调用呢?其实这是不可能的,本身就是依托于Zookeeper去实现分布式ID的分配。所以有没有别的办法呢,想象一下以下场景:假如,储户去银行柜台取钱,每一次取1块钱,然后柜员打开保险柜,从里面拿出一块钱,关闭保险柜,然后给储户一块钱。事实是这样吗?不是,柜员旁都会有部分现金,比如100张一元钱,每次我们取钱,就不需要去保险柜拿,而是直接从旁边的100张中拿一张给我们,等100张都发放完了,再去保险柜拿。由此省去了99次开关保险柜的重复动作,显著提高效率。

所以我们参照上述场景,Zookeeper好比存放现金的保险柜,应用是柜员,线程是储户。每次生成ID时,应用向Zookeeper申请一定份额的“ID”,然后所有的ID的发放完,再去向Zookeeper申请。

又一个问题接踵而至,如何实现请求一次就获取到100个ID呢?单位转换。比如本次生成的临时顺序节点的序号为0000000001,也就是1,但是在应用中,不认为这就是单纯的数字1,而是表示1份ID,这一份有100个ID,也可就是从100-199,即序号000000000100-000000000199。代码实现如下:

    /**
     * 份额
     */
    private static final long share = 100;

    /**
     * ID的长度 = DATE_FORMAT的长度 + zk顺序节点的序号长度 + 份额的长度
     */
    private static final int length = 20;

    /**
     * 用于存储每个模块,本地份额所用的数量
     * K -> 模块
     * V -> 已用份额
     */
    private static final Map<String, Long> LocalSerialNumber = new HashMap<>();

    /**
     * 用于存储每个模块所分配的份额序号
     * K -> 模块
     * V -> 当前是第几份ID
     */
    private static final Map<String, Long> RemoteSerialNumber = new HashMap<>();

    public synchronized String generateId(String module) {
        // 先判断是否已分配本地份额
        long local = LocalSerialNumber.get(module) == null ? 0 : LocalSerialNumber.get(module);
        long remote = RemoteSerialNumber.get(module) == null ? 0 : RemoteSerialNumber.get(module);
        if (remote == 0 // 表示还未分配本地份额
                || local >= share // 表示本地份额已用完
        ) {
            // 向Zookeeper请求分配"一份"ID
            String nodeId = createNodeId(module);
            remote = Long.valueOf(nodeId);
            RemoteSerialNumber.put(module, remote);
            // 重置local
            local = 0;
        }
        LocalSerialNumber.put(module, local + 1);
        // 当前序号
        long sort = remote * share + local;
        return padding(sort);
    }

    /**
     * 转成固定长度的字符串
     *
     * @param sort 序号
     * @return
     */
    private String padding(long sort) {
        String s = String.valueOf(sort);
        return "0000000000".substring(0, length - DATE_FORMAT.length() - s.length()) + s;
    }

    /**
     * 生成id
     *
     * @param module 模块
     * @return
     */
    public String nextId(String module) {
        return datePrefix() + generateId(module);
    }

在100次ID的生成中,只有一次远程调用,大大提高了系统的并发性能,同时也解决了容量问题,原本只能生成100亿个ID,经过单位转换(ZK中的序号1代表应用中的100),容量提高了100倍。如果有更高要求,可以提高份额至1000,10000等。

8.3.内存问题

为了模拟创建临时顺序节点的内存资源消耗,博主创建了创建了100w个临时顺序节点。并用visualVM监控Zookeeper的堆内存的占用情况,大约消耗内存460m,结果如下。

在这里插入图片描述

很明显,当客户端应用长期运行并且产生大量分布式ID时,Zookeeper需要承担大量的内存消耗。有什么办法能降低这种内存消耗呢?

org.apache.curator.framework.recipes.atomic.DistributedAtomicLong是Curator框架提供的一种分布式原子计数器的实现。内部使用乐观锁实现,当失败时,再尝试加互斥排他锁。不论是乐观锁还是排他锁,都会按照重试策略进行重试操作。

    /**
     * 分布式原子计数器
     *
     * @param module
     * @return
     */
    public String distributedAtomicLong(String module) {
        Assert.notNull(module, "模块名称不能为空");
        // 获取当前模块对应的Znode路径
        String idNodePath = getIdNodePath(module);
        try {
            // 创建分布式原子计数器的节点,并返回路径
            String nodePath = ZKPaths.makePath(idNodePath, "AtomicLong");
            // 重试策略 ExponentialBackoffRetry参数说明:
            //  baseSleepTimeMs 初始sleep时间
            //  maxRetries 最大重试次数
            //  maxSleepMs 最大sleep时间
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(50, 20, 200);
            // 可升级锁的配置信息
            String lockPath = idNodePath + "/AtomicLongLock";
            RetryPolicy lockRetryPolicy = new ExponentialBackoffRetry(50, 10, 500);
            PromotedToLock promotedToLock = PromotedToLock.builder()
                    .lockPath(lockPath)
                    .retryPolicy(lockRetryPolicy)
                    .timeout(500, TimeUnit.MILLISECONDS)
                    .build();
            // 分布式原子计数器,首先尝试使用乐观锁进行增量操作,如果失败,则采用可选的InterProcessMutex锁进行增量操作。
            // 对于乐观锁和悲观锁,重试策略都用于重试增量操作。
            DistributedAtomicLong distributedAtomicLong = new DistributedAtomicLong(client, nodePath, retryPolicy, promotedToLock);
            // 自增并获取分布式原子长整型
            AtomicValue<Long> longAtomicValue = distributedAtomicLong.increment();
            int retryTimes = 20;
            // !longAtomicValue.succeeded() && i < retryTimes 表示如果获取失败并且重试次数小于规定的20次
            for (int i = 0; !longAtomicValue.succeeded() && i < retryTimes; i++) {
                // 继续增加
                longAtomicValue= distributedAtomicLong.increment();
            }
            if (longAtomicValue.succeeded()) {
                // 获取自增后的值
                Long obj = longAtomicValue.postValue();
                return String.valueOf(obj);
            } else {
                throw new RuntimeException("获取分布式ID失败:DistributedAtomicLong获取超时");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

优化后的内存消耗如下:

在这里插入图片描述

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

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

相关文章

[Algorithm][综合训练][合唱团][跳台阶扩展问题][矩阵最长递增路径]详细讲解

目录 1.合唱团1.题目链接2.算法原理详解 && 代码实现 2.跳台阶扩展问题1.题目链接2.算法原理详解 && 代码实现 3.矩阵最长递增路径1.题目链接2.算法原理详解 && 代码实现 1.合唱团 1.题目链接 合唱团 2.算法原理详解 && 代码实现 解法&#…

爆火的《黑神话:悟空》对LabVIEW软件开发的启示

近期&#xff0c;《黑神话&#xff1a;悟空》在全球范围内爆火&#xff0c;引发了游戏行业和玩家群体的广泛关注。作为一款由中国开发团队Game Science历时多年打造的动作角色扮演游戏&#xff0c;它的成功不仅源于卓越的技术创新和对中国传统文化的深度挖掘&#xff0c;更在于…

本地部署Xinference实现智能体推理工作流(一)

提示&#xff1a;没有安装Docker的需要先提前安装好Docker 第一篇章 使用AutoDL平台快速部署xinference 备注&#xff1a;若使用AutoDL平台&#xff0c;以下过程使用无卡模型开机即可(省钱) 1. 下载Dify源码 Github下载Dify:https://github.com/langgenius/dify 2. 快速启动…

HTML粒子爱心

目录 写在前面 完整代码 代码分析 系列文章 写在最后 写在前面 教你用HTML语言实现粒子爱心动画的效果。 HTML&#xff0c;全称为HyperText Markup Language&#xff0c;即超文本标记语言&#xff0c;是构建网页的基本markup语言&#xff0c;它通过一系列标签tags来定义…

CMake Error at CMakeLists.txt (find_package)幕后真凶

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~ 个人主页&#xff1a; rainInSunny | 个人专栏&#xff1a; C那些事儿、 Learn OpenGL In Qt 文章目录 写在前面find_package报错解决Module模式Config模式 find_package()用法Module模式Config模式 写在前面 本文从CMake中find_packag…

AI无损放大工具 AI lossless zoomer 3.1.0.0

这款工具是作者基于腾讯开源免费的AI算法而开发的&#xff0c;腾讯开源算法地址&#xff1a;GitHub.com/xinntao/Real-ESRGAN 打开软件的主页面是黑色为主调的界面。把需要无损放大的图片直接拖入到软件中&#xff0c;再点【开始任务】&#xff0c;软件就会自动处理图片&#…

【论文阅读|cryoET】本周粗读汇总

论文1&#xff1a;CryoDRGN-ET&#xff1a;深度重建生成网络以可视化细胞内动态生物分子 Abstract 虽然冷冻电子断层扫描可以以分子分辨率揭示结构&#xff0c;但图像处理算法仍然是解决原位生物分子结构异质性的瓶颈。本文介绍CryoDRGN-ET用于cryoET断层图的异质重建。CryoD…

大厂产品经理面试:阿里、字节、百度、腾讯、拼多多等全国顶级大厂面试题一网打尽!

在互联网行业蓬勃发展的今天&#xff0c;产品经理作为连接技术、设计和市场的核心角色&#xff0c;其重要性日益凸显。想要进入国内顶尖的互联网大厂&#xff0c;如阿里巴巴、字节跳动、百度、腾讯、拼多多等&#xff0c;产品经理岗位的面试无疑是一场硬仗。本文将为你揭秘这些…

stm32定时器同步(二)

本文介绍使用一个定时器使能另一个定时器。 旧瓶装新酒&#xff0c;就是门控模式加一个TRGO&#xff0c;如果看到这里你还没有明白&#xff0c;那就去看看我前面的文章 代码部分也是旧瓶装新酒 void Timer3_Config(void) {TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;TIM…

皮皮虾保存视频怎么去除水印?教你3种技巧轻松去除水印

在如今这个信息爆炸的时代&#xff0c;我们经常会在网络上看到一些有趣的皮皮虾视频&#xff0c;想要保存下来&#xff0c;却发现视频带有水印。水印虽然可以保护视频的版权&#xff0c;但有时也会影响我们的观看体验。如何去除视频中的水印呢&#xff1f;本文将为您介绍三种实…

日期转换异常

报错 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: Error querying database. Cause: java.lang.NumberFormatException: For input string: "2024-07-01" Cause: java.lang.NumberFormatE…

Windows TCP/IP IPv6 DDos远程蓝屏复现及修复(CVE-2024-38063)

【前言】   最近&#xff0c;windows爆出重量级漏洞CVE-2024-38063)&#xff0c;攻击者通过不断发送ipv6数据包&#xff0c;可实现远程DDOS导致目标windows直接蓝屏&#xff0c;或RCE。   下面就直接开展下&#xff0c;如何复现的&#xff0c;以及怎么修复。 一、漏洞分析 …

如何从官方 Yum 仓库安装 MySQL 5.6

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 介绍 2013年10月&#xff0c;MySQL开发团队正式推出了对yum存储库的支持。这意味着您现在可以确保直接从源安装最新和最好的MySQL版本&a…

论文阅读:SimVP: Simpler yet Better Video Prediction

论文地址&#xff1a;arxiv 摘要 作者认为&#xff0c;现有的CNN&#xff0c;RNN&#xff0c;Transformer 之类的视频预测领域的模型都过于复杂了&#xff0c;作者想要找到一个简单的方式&#xff0c;同时可以达到与之相当的效果。 作者提出了 SimVP&#xff0c;这是一个简单…

Linux工具使用

Linux编辑器-vim使用 1.vim的基本概念 在vim中&#xff0c;主要的三种模式分别是命令模式&#xff0c;插入模式和底行模式。 正常/普通/命令模式(Normal mode) 控制屏幕光标的移动&#xff0c;字符、字或行的删除&#xff0c;移动复制某区段及进入Insert mode下&#xff0c;…

【软件工程】软件生命周期(生存周期)

考点3 软件生命周期&#xff08;生存周期&#xff09; 一、定义 软件产品或软件系统从设计、投入使用到被淘汰的全过程。 二、3周期 题目 一、判断题 二、选择题

冲击大厂算法面试=>链表专题【链表反转】

目录标题 最基础的反转链表上代码题解呀实在不会的时候记住 最基础的反转链表 上代码 class Solution {public ListNode reverseList(ListNode head) {ListNode pre null;ListNode cur head;while(cur ! null){ListNode nextNode cur.next;cur.next pre;pre cur;cur nex…

[HZNUCTF 2023 preliminary]flask

[HZNUCTF 2023 preliminary]flask 点开之后页面如图所示&#xff1a; 猜测是SSTI模板注入&#xff0c;先输入{7*7},发现模板是倒序输入的&#xff1a; 然后我们输入}}‘7’*7{{返回777777&#xff0c;这是jinja2模板&#xff1a; 我们需要让用户输入一个字符串&#xff0c;对其…

计算机视觉基础. 1 学习导论

1 .引言 学习的目的是从过去的经验中吸取教训&#xff0c;以解决未来的问题。通常&#xff0c;这涉及搜索解决问题过去实例的算法。然后&#xff0c;该算法可以应用于该问题的未来实例。 过去和未来不一定指日历日期&#xff1b;相反&#xff0c;它们指的是学习者之前看到的内…

Windows下devecostudio-windows-3.1.0.501的下载与安装教程

Windows下devecostudio-windows-3.1.0.501的下载与安装教程 1.华为开发者联盟 2.Deveco Studio下载 一、注册华为账号 进入华为开发者联盟&#xff0c;注册华为账号&#xff08;如果有手机华为账号&#xff0c;这里通用&#xff09; 二、下载Deveco Studio3.1.0.501 链接&am…