Dubbo学习笔记

news2024/11/27 5:30:41

目录

简介

Dubbo高可用

集群容错

服务治理

Dubbo线程IO模型

源码层面

Java SPI 的问题

源码解析


简介

Apache Dubbo是一款高性能的Java RPC框架。其前身是阿里巴巴公司开源的一个高性能、轻量级的开源Java RPC框架,可以和Spring框架无缝集成。

Dubbo提供了三大核心能力:

  • 面向接口的远程方法调用
  • 智能容错和负载均衡
  • 服务自动注册和发现

官网: Apache Dubbo

Dubbo高可用

集群容错

  • 服务路由:服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消 费者可调用哪些服务提供者,dubbo提供三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由ScriptRouter、标签路由TagRouter,本课程重点分析条件路由 条件路由规则的格式:

    [服务消费者匹配条件] => [服务提供者匹配条件]

    host = 10.20.153.10 => host = 10.20.153.11

    该条规则表示 IP 为 10.20.153.10 的服务消费者只可调用 IP 为 10.20.153.11 机器上的服务,不可 调用其他机器上的服务。

    如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务

    常见路由配置:

    • 白名单:

      host!=10.20.153.10,10.20.153.11=>

    • 黑名单

      host=10.20.153.10,10.20.153.11=>

    • 读写分离

      method=find,list,get,is=>host=172.22.3.94,172.22.3.95,172.22.3.96

      method!=find,list,get,is=>host=172.22.3.97,172.22.3.98

    • 前后台分离

      application=front=>host=172.22.3.91,172.22.3.92,172.22.3.93

      application!=front=>host=172.22.3.94,172.22.3.95,172.22.3.96

  • 集群容错

    • Failover Cluster 失败自动切换,当出现失败,重试其它服务器。(缺省) 通常用于读操作,但 重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。

      重试次数配置如下:

      <dubbo:service retries="2" />

      <dubbo:reference retries="2" />

    • Failfast Cluster 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作, 比如新增记录。

    • Failsafe Cluster 失败安全,出现异常时,直接忽略。通常用于写入日志等操作。

    • Failback Cluster 失败自动恢复,后台记录失败请求,定时重发。 通常用于消息通知操作。

    • Forking Cluster 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过forks="2"来设置最大并行数。

  • 负载均衡 :在集群负载均衡时,Dubbo提供多种均衡策略,缺省random随机调用

    • Random LoadBalance:按照权重设置随机概率,无状态

    • RoundRobin LoadBalance:轮询,有状态

    • LeastActive LoadBalance:最少活跃数随机,方法维度的统计服务调用数

    • ConsistentHash LoadBalance:一致性Hash

 Redis集群模式下保证可迁移和高可用——一致性算法

服务治理

  1. 添加依赖<packaging>war</packaging>

  2. main下新建目录webapp

  3. 项目结构-modules里添加web.xml

  • 服务降级

    可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略 可以向注册中心写入动态配置覆盖规则

    RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService? category=configurators&dynamic=false&application=foo&mock=force:return+null" ));

    使用mock实现服务降级,mock只出现在非业务异常(如,超时,网络异常等)时执行。两种配置:

    boolean值,默认为false,如果配置为true,则缺省使用mock类名,即类名+Mock后缀;

    "return null",可以简单忽略异常。

    mock实现接口方式(推荐)

    配置mock=true,同时实现mock接口,类名注意命名规范:接口名+Mock后缀,此时如果调用失败会调用Mock实现,mock实现需要保证有无参的构造方法

    • mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。

    • 还可以改为 mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。

  • dubbo配置服务降级方式

    • 在dubbo-admin中配置

  • 整合hystrix,实现高可用(当出现异常 时,可以调用自定义的回滚方法)

    Hystrix旨在通过控制那些访问远程系统,服务和第三方库的 节点,从而对延迟和故障提供更强大的容错能力,Hystrix具备拥有回退机制和断路器功能 的线程和信号隔离,请求缓存和请求打包,以及监控和配置等功能

springboot官方提供了对hystrix的集成,直接在pom.xml里加入依赖

<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>           
    <version>1.4.4.RELEASE</version>
</dependency>

Application启动类中新增@EnableHystrix来启用hystrix starter

provider增加@HystrixCommand

Consumer method上配置@HystrixCommand(fallbackMethod= methodName))

Dubbo线程IO模型

Dubbo使用NIO模型

  • config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置 类,也可以通过 spring 解析配置生成配置类

  • proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory

  • registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService

  • cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口 为 Cluster, Directory, Router, LoadBalance

  • monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService

  • protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter

  • exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口 为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer

  • transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec

  • serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput,ThreadPool

源码层面

Java SPI 的问题

  • 接口的所有实现类全部都需要加载并实例化

  • 无法根据参数来获取所对应的实现类

    • 不能解决IOC、AOP的问题

基于以上问题,dubbo在java原生的SPI机制进行了增强,解决了以上问题

Dubbo 重新实现了一套功能更强的 SPI 机制,Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类 中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在META-INF/dubbo 路径下,配置内容如下

//Dubbo SPI配置文件
college=cn.itheima.api.impl.Czxy 
shortTrain=cn.itheima.api.impl.Itheima
​
//Java 原生SPI
cn.itheima.api.impl.Czxy 
cn.itheima.api.impl.Itheima

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解

源码解析

通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,该方法方法先从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例

 

 createExtension方法包含如下步骤

  • 通过 getExtensionClasses 获取所有的拓展类

  • 通过反射创建拓展对象

  • 向拓展对象中注入依赖

  • 将拓展对象包裹在相应的 Wrapper 对象中

private T createExtension(String name) { 
    // 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表 
    Class<?> clazz = getExtensionClasses().get(name); 
    if (clazz == null) { 
        throw findException(name);
    } 
    try { 
        T instance = (T) EXTENSION_INSTANCES.get(clazz); 
        if (instance == null) { 
            // 通过反射创建实例 
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); 
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        } 
        // 向实例中注入依赖 
        injectExtension(instance); 
        Set<Class<?>> wrapperClasses = cachedWrapperClasses; 
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) { 
            // 循环创建 Wrapper 实例 
            for (Class<?> wrapperClass : wrapperClasses) { 
                // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建Wrapper 实例。 
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); 
            }
        }
        return instance;
    } catch (Throwable t) { 
        throw new IllegalStateException("...");
    }
}

此处injectExtension方法依赖注入实现原理:Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中

private T injectExtension(T instance) {
        try {
            if (objectFactory != null) { // 遍历目标类的所有方法
                for (Method method : instance.getClass().getMethods()) {
                    // 检测方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public
                    if (method.getName().startsWith("set") 
                        && method.getParameterTypes().length == 1 
                        && Modifier.isPublic(method.getModifiers())) {
                        // 获取 setter 方法参数类型
                        Class<?> pt = method.getParameterTypes()[0];
                        try { 
                            // 获取属性名,比如 setName 方法对应属性名 name
                            String property = method.getName().length() > 3 ? 
                                method.getName().substring(3, 4).toLowerCase() + 
                                method.getName().substring(4) : "";
                            // 从 ObjectFactory 中获取依赖对象
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) { 
                                // 通过反射调用 setter 方法设置依赖
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method...");
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

第一个步骤中获取所有的扩展类方法getExtensionClasses,该方法先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过loadExtensionClasses 加载拓展类

private Map<String, Class<?>> getExtensionClasses() {
        // 从缓存中获取已加载的拓展类
        Map<String, Class<?>> classes = cachedClasses.get();
        // 双重检查
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 加载拓展类
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

loadExtensionClasses 方法做了两件事,一是解析SPI注解,二是调用 loadDirectory 方法加载指定文件夹配置文件

private Map<String, Class<?>> loadExtensionClasses() {
        // 获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
                // 对 SPI 注解内容进行切分
                String[] names = NAME_SEPARATOR.split(value);
                // 检测 SPI 注解内容是否合法,不合法则抛出异常
                if (names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension...");
                }
​
                // 设置默认名称,参考 getDefaultExtension 方法
                if (names.length == 1) {
                    cachedDefaultName = names[0];
                }
            }
        }
        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        // 加载指定文件夹下的配置文件
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

loadDirectory 方法获取classLoader ,通过classLoader获取URL资源信息,遍历URL通过 loadResource加载资源

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
        // fileName = 文件夹路径 + type 全限定名 
        String fileName = dir + type.getName();
        try {
            Enumeration<URL> urls;
            ClassLoader classLoader = findClassLoader();
            // 根据文件名加载所有的同名文件 
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();
                    // 加载资源 
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("...");
        }
​
    }

loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
            try {
                String line;
                // 按行读取配置内容
                while ((line = reader.readLine()) != null) {
                    // 定位 # 字符
                    final int ci = line.indexOf('#');
                    if (ci >= 0) {
                        // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
                        line = line.substring(0, ci);
                    }
                    line = line.trim();
                    if (line.length() > 0) {
                        try {
                            String name = null;
                            int i = line.indexOf('=');
                            if (i > 0) {
                                // 以等于号 = 为界,截取键与值
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            }
                            if (line.length() > 0) {
                                // 加载类,并通过 loadClass 方法对类进行缓存
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader),
                                        name);
                            }
                        } catch (Throwable t) {
                            IllegalStateException e = new IllegalStateException("Failed to load extension class...");
                        }
                    }
                }
            } finally {
                reader.close();
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class...");
        }
    }

loadClass方法主要用用用于操作缓存

private void loadClass(Map<String, Class<?>> extensionClasses, 
                       java.net.URL resourceURL, Class<?> clazz, 
                       String name) throws NoSuchMethodException {
        if (!type.isAssignableFrom(clazz)) {
            throw new IllegalStateException("...");
        }
​
        // 检测目标类上是否有 Adaptive 注解 
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            if (cachedAdaptiveClass == null) {
                // 设置 cachedAdaptiveClass缓存 
                cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
                throw new IllegalStateException("...");
            }
​
            // 检测 clazz 是否是 Wrapper 类型 
        } else if (isWrapperClass(clazz)) {
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            // 存储 clazz 到 cachedWrapperClasses 缓存中 
            wrappers.add(clazz);
​
            // 程序进入此分支,表明 clazz 是一个普通的拓展类 
        } else {
            // 检测 clazz 是否有默认的构造方法,如果没有,则抛出异常 
            clazz.getConstructor();
            if (name == null || name.length() == 0) {
                // 如果 name 为空,则尝试从 Extension 注解中获取 name,或使用小写的类名作为name
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("...");
                }
            }
            // 切分 name 
            String[] names = NAME_SEPARATOR.split(name);
            if (names != null && names.length > 0) {
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键, 
                    // 存储 name 到 Activate 注解对象的映射关系 
                    cachedActivates.put(names[0], activate);
                }
                for (String n : names) {
                    if (!cachedNames.containsKey(clazz)) {
                        // 存储 Class 到名称的映射关系 
                        cachedNames.put(clazz, n);
                    }
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        // 存储名称到 Class 的映射关系 
                        extensionClasses.put(n, clazz);
                    } else if (c != clazz) {
                        throw new IllegalStateException("...");
                    }
                }
            }
        }
    }

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

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

相关文章

主流总线通信和系统接口技术

一、关于现场控制总线 现场总线是自动控制领域的计算机局域网&#xff0c;应用在生产现场&#xff0c;在微机测控设备之间实现双向、串行、多节点数字通信&#xff0c;是一种开放式、数字化、多点通信的底层控制网络。 现场总线具有较高的测控能力指数 得益于仪表的微机化&am…

C++实现闭散列/开放定址法

前言 哈希冲突是无法避免的&#xff0c;只能尽可能的减少冲突的可能性&#xff0c;通常我们可以设计适合的哈希函数。但是&#xff0c;哈希冲突还是会发生&#xff0c;那我们如何解决呢&#xff1f; 我们可以使用闭散列/开放定址法的方法&#xff0c;解决哈希冲突 文章目录 前…

世界超高清大会发布重大技术成果:博冠自主创新推动8K摄像机攻关

一、世界超高清大会背景介绍&#xff1a; 近日&#xff0c;由工业和信息化部、国家广播电视总局、中央广播电视总台、广东省人民政府主办的2023世界超高清视频产业发展大会在广州越秀国际会议展览中心盛大召开。自2018年创办以来&#xff0c;大会已成功举办四届&#xff0c;成…

第08讲:搭建 SkyWalking 源码环境,开启征途

搭建 SkyWalking 源码环境 下载 SkyWalking 源码 执行 git clone 命令从 GitHub下载 SkyWalking 源码&#xff0c;如下所示 &#xff1a; git clone gitgithub.com:apache/skywalking.git 切换分支 等待 clone 完成之后&#xff0c;我们通过命令行窗口进入 SkyWalking 源码根…

SSM 三大框架原理、核心技术,运行流程讲解

作者:arrows 来源:https://www.cnblogs.com/arrows/p/10537733.html 一、Spring部分 1、 Spring的运行流程 第一步&#xff1a;加载配置文件ApplicationContext ac new ClassPathXmlApplicationContext(“beans.xml”); &#xff0c;ApplicationContext接口&#xff0c;它由…

存储卡目录变成未知文件?这些技巧能让你恢复数据!

当存储卡的目录变成未知文件时&#xff0c;我们无法直接访问存储卡中的数据。但是&#xff0c;这并不意味着这些数据永远无法恢复。以下是几种可能恢复存储卡数据的方法&#xff1a; 使用数据恢复软件。从互联网上下载并安装专业的数据恢复软件这些软件可以扫描存储卡&#xf…

分布式接口幂等性设计实现

面对分布式架构和微服务复杂的系统架构和网络超时服务器异常等带来的系统稳定性问题&#xff0c;分布式接口的幂等性设计显得尤为重要。本文简要介绍了几种分布式接口幂等性设计实现&#xff0c;包括Token去重机制、乐观锁机制、数据库主键和状态机实现等&#xff0c;以加深理解…

面板安全增强,网站支持反向代理设置,1Panel开源面板v1.2.0发布

2023年5月15日&#xff0c;现代化、开源的Linux服务器运维管理面板1Panel正式发布v1.2.0版本。 在这一版本中&#xff0c;1Panel着重增强了安全方面的功能&#xff0c;包括安全入口访问、面板SSL设置、网站密码访问等&#xff0c;同时网站新增支持反向代理设置&#xff0c;并带…

JVM学习(三)

1. JAVA 四中引用类型 1.1. 强引用 在 Java 中最常见的就是强引用&#xff0c; 把一个对象赋给一个引用变量&#xff0c;这个引用变量就是一个强引 用。当一个对象被强引用变量引用时 &#xff0c;它处于可达状态&#xff0c;它是不可能被垃圾回收机制回收的&#xff0c;即…

Java阶段二Day21

Java阶段二Day21 文章目录 Java阶段二Day21整合Lombok基础组件1 Lombok简介2 安装和配置 Lombok3 Lombok 注解及其用法3.1 Getter 和 Setter3.2 ToString3.3 AllArgsConstructor 和 NoArgsConstructor3.4 Data 4. 总结5 微博项目优化 Knife4j1 Knife4j的优点2 Knife4j快速上手2…

使用Docker构建的MySQL主从架构:高可用性数据库解决方案

前言 MySQL主从架构&#xff0c;我们已经在vmware虚拟机上实践过了&#xff0c;接下来我们一起探讨在docker中如何使用MySQL主从架构。 &#x1f3e0;个人主页&#xff1a;我是沐风晓月 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是沐风晓月&#xff0c;阿里云社…

《论文阅读》基于提示的知识生成解决对话情感推理难题

《论文阅读》基于提示的知识生成解决对话情感推理难题 前言摘要作者新观点问题定义模型框架Global ModelLocal ModelPrompt Based Knowledge Generation分类器实验结果问题前言 你是否也对于理解论文存在困惑? 你是否也像我之前搜索论文解读,得到只是中文翻译的解读后感到失…

openEuler 成功适配 LeapFive InFive Poros 开发板

近日&#xff0c;openEuler RISC-V 23.03 创新版本在跃昉科技的 Poros 开发板上成功运行。 openEuler 在 Poros 上适配成功&#xff0c;XFCE 桌面启动正常&#xff0c;文件系统、终端模拟器和输入法等相关 GUI 应用也运行流畅&#xff0c;Chromium 浏览器和 LibreOffice 等应用…

【Pm4py第三讲】关于Output

本节用于介绍pm4py中的输出函数&#xff0c;包括日志输出、模型输出、面向对象日志输出等。 1.函数概述 本次主要介绍Pm4py中一些常见的输入函数&#xff0c;总览如下表&#xff1a; 函数名说明write_bpmn()用于写入bpmn模型write_dfg()用于写入dfg模型write_pnml() 用于写入p…

面试之高手回答

1.int与Integer的区别 int与Integer的区别有很多&#xff0c;我简单罗列三个方面 第一个作为成员变量来说Integer的初始值是null&#xff0c;int的初始值是0&#xff1b; 第二个Integer存储在堆内存&#xff0c;int类型是在直接存储在栈空间&#xff1b; 第三个integer是个对象…

项目管理6大避坑技巧

1、拒绝错位战略目标 明确目标方向 做项目&#xff0c;首先需要明确项目目标。项目中有很多目标都很重要&#xff0c;但只有一两个目标是最重要的。在任何时刻&#xff0c;我们主要精力都应该集中在一到两个最重要的目标上。 一般最重要的目标具有以下特点&#xff1a;能够给组…

CSS--空间转换及动画

01-空间转换 空间转换简介 空间&#xff1a;是从坐标轴角度定义的 X 、Y 和 Z 三条坐标轴构成了一个立体空间&#xff0c;Z 轴位置与视线方向相同。空间转换也叫 3D转换属性&#xff1a;transform 平移 transform: translate3d(x, y, z); transform: translateX(); transfor…

能源硕士为何受热捧?社科院与杜兰大学能源管理硕士项目为你解惑

能源行业可谓是全球最具发展前景的行业之一&#xff0c;能源管理硕士更是近几年被争相推荐的“大热门”。广泛的就业选择、较高且稳定的收入&#xff0c;是该专业的特点之一&#xff0c;毕业后可选择在政府相关机构、能源监管部门、全国节能减排领域的各类研究机构工作&#xf…

Linux学习 Day3

目录 1. 时间相关的指令 2. cal指令 3. find指令&#xff1a;&#xff08;灰常重要&#xff09; -name 4. grep指令 5. zip/unzip指令 6. tar指令&#xff08;重要&#xff09;&#xff1a;打包/解包&#xff0c;不打开它&#xff0c;直接看内容 7. bc指令 8. uname –…

Shell基础学习---1、Shell概述、脚本入门、变量

1、Shell 概述 Shell是一个命令解释器&#xff0c;它接收应用程序/用户命令&#xff0c;然后调用操作系统内核。 说明&#xff1a;Shell是一个功能相当强大的编程语言&#xff0c;易编写&#xff0c;易调试、灵活性强。 1、 提供的Shell解释器 2、bash和sh的关系 3、CentOS…