关闭页面特效

springCloud

0|1传统项目转型


存在的问题

· 代码耦合,开发维护困难

· 无法针对不同模块进行针对性优化

· 无法水平扩展

· 单点容错率低,并发能力差

0|1垂直拆分


当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分:

优点

· 系统拆分实现了流量分担,解决了并发问题

· 可以针对不同模块进行优化

· 方便水平扩展,负载均衡,容错率提高

缺点

· 系统间相互独立,会有很多重复开发工作,影响开发效率

0|1分布式服务


当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,
使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。

把垂直架构中每个项目中都需要写一遍的功能,我单独拿出来,封装成一个项目,然后使用maven安装到我们的本地仓库。

基础服务:是将业务功能中重复的代码进行了抽取,抽取出一个单独的功能,这样的话这个功能代码我们只需要写一次 不需要在每一个业务功能中都去写一遍。

优点

· 将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率

缺点

· 系统间耦合度变高,调用关系错综复杂,难以维护

0|1服务治理(SOA)


当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键

0|1微服务概述


Martin Fowler在2014年提出了“微服务”架构,它是一种全新的架构风格。

  • 微服务把一个庞大的单体应用拆分为一个个的小型服务,比如我们原来的图书管理项目中,有登录、注册、添加、删除、搜索等功能,那么我们可以将这些功能单独做成一个个小型的SpringBoot项目,独立运行。
  • 每个小型的微服务,都可以独立部署和升级,这样,就算整个系统崩溃,那么也只会影响一个服务的运行。
  • 微服务之间使用HTTP进行数据交互,不再是单体应用内部交互了,虽然这样会显得更麻烦,但是带来的好处也是很直接的,甚至能突破语言限制,使用不同的编程语言进行微服务开发,只需要使用HTTP进行数据交互即可。
  • 我们可以同时购买多台主机来分别部署这些微服务,这样,单机的压力就被分散到多台机器,并且每台机器的配置不一定需要太高,这样就能节省大量的成本,同时安全性也得到很大的保证。
  • 甚至同一个微服务可以同时存在多个,这样当其中一个服务器出现问题时,其他服务器也在运行同样的微服务,这样就可以保证一个微服务的高可用。

0|1SpringCloud


优点

  • 每个服务足够内聚,足够小,比较容易聚焦 一个服务就是一个功能
  • 开发简单且效率高,一个服务只做一件事情
  • 开发团队小,一般2-5人足以(当然按实际为准)
  • 微服务是松耦合的,无论开发还是部署都可以独立完成
  • 微服务能用不同的语言开发
  • 易于和第三方集成,微服务允许容易且灵活的自动集成部署(持续集成工具有Jenkins,Hudson,bamboo等)
  • 微服务易于被开发人员理解,修改和维护,这样可以使小团队更加关注自己的工作成果,而无需一定要通过合作才能体现价值
  • 微服务允许你融合最新的技术
  • 微服务只是业务逻辑的代码,不会和HTML,CSS或其他界面组件融合。
  • 每个微服务都可以有自己的存储能力,数据库可自有也可以统一,十分灵活。

缺点

  • 开发人员要处理分布式系统的复杂性
  • 多服务运维难度,随着服务的增加,运维的压力也会增大
  • 依赖系统部署
  • 服务间通讯的成本
  • 数据的一致性
  • 系统集成测试
  • 性能监控的难度

0|1调用方式


常见的远程调用方式有以下几种:

  1. RPC:Remote Produce Call远程过程调用,类似的还有RMI(Remote Method Invocation,远程方法调用)。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型 框架
    缺点:如果我们使用RPC 要求两个微服务必须使用同一种编程语言。
    优点: 速度快,效率高
  2. Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。现在热门的Rest风格,就可以通过http协议来实现。

认识RPC

概念解释:

RPC,即 Remote Procedure Call(远程过程调用),是一个计算机通信协议。 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。

说得通俗一点就是:A计算机提供一个服务,B计算机可以像调用本地服务那样调用A计算机的服务。

作用体现:

通过上面的概念,我们可以知道,实现RPC主要是做到两点:

  1. 实现远程调用其他计算机的服务
  2. 像调用本地服务一样调用远程服务

问题思考:

要实现远程调用,肯定是通过网络传输数据。A程序提供服务,B程序通过网络将请求参数传递给A,A程序接收参数调用本地服务执行后得到结果,再将结果返回给B程序。这里需要关注的有两点:

1)采用何种网络通讯协议?

现在比较流行的RPC框架,都会采用TCP作为底层传输协议

2)数据传输的格式怎样?

两个程序进行通讯,必须约定好数据传输格式。就好比两个人聊天,要用同一种语言,否则无法沟通。所以,我们必须定义好请求和响应的格式。另外,数据在网路中传输需要进行序列化,所以还需要约定统一的序列化的方式。

如果仅仅是远程调用,还不算是RPC,因为RPC强调的是过程调用,调用的过程对用户而言是应该是透明的,用户不应该关心调用的细节,可以像调用本地服务一样调用远程服务。

所以RPC一定要对调用的过程进行封装 dubbo框架

认识http

概念解释

Http协议:超文本传输协议,是一种应用层协议。规定了网络传输的请求格式、响应格式、资源定位和操作的方式等。但是底层采用什么网络传输协议,并没有规定,不过现在都是采用TCP协议作为底层传输协议。说到这里,大家可能觉得,Http与RPC的远程调用非常像,都是按照某种规定好的数据格式进行网络通信,有请求,有响应。没错,在这点来看,两者非常相似,但是还是有一些细微差别。

数据从A计算机传输到B计算机 遵循的是TCP传输协议。

http和rpc差别:

  • · RPC并没有规定数据传输格式,这个格式可以任意指定,不同的RPC协议,数据格式不一定相同。只要服务提供者和服务消费者自己商量好数据传输格式就可以。 UserService.findAll
  • · Http中定义了资源定位的路径,RPC中并不需要 http://
  • · 最重要的一点:RPC需要满足像调用本地服务一样调用远程服务,也就是对调用过程在API层面进行封装。Http协议没有这样的要求,因此请求、响应等细节需要我们自己去实现。
  • 优点:RPC方式更加透明,对用户更方便。Http方式更灵活,没有规定API和语言,跨语言、跨平台
    缺点:RPC方式需要在API层面进行封装,限制了开发的语言环境

例如我们通过浏览器访问网站,就是通过Http协议。只不过浏览器把请求封装,发起请求以及接收响应,解析响应的事情都帮我们做了。如果是不通过浏览器,那么这些事情都需要自己去完成。

0|1如何选择


既然两种方式都可以实现远程调用,我们该如何选择呢?

· 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。

· 难度来看,RPC实现较为复杂,http相对比较简单

· 灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。

因此,两者都有不同的使用场景:

· 如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。

· 如果需要更加灵活,跨语言、跨平台,显然http更合适

那么我们该怎么选择呢?

微服务,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。

0|1SpringCloud入门概述


SpringCloud是Spring提供的一套分布式解决方案,集合了一些大型互联网公司的开源产品,包括诸多组件,共同组成SpringCloud框架。并且,它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、熔断机制、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

可以看到,SpringCloud整体架构的亮点是非常明显的,分布式架构下的各个场景,都有对应的组件来处理,比如基于Netflix(奈飞)的开源分布式解决方案提供的组件:

  • Eureka - 实现服务治理(服务注册与发现),我们可以对所有的微服务进行集中管理,包括他们的运行状态、信息等。
  • Ribbon - 为服务之间相互调用提供负载均衡算法(现在被SpringCloudLoadBalancer取代)
  • Hystrix - 断路器,保护系统,控制故障范围。暂时可以跟家里电闸的保险丝类比,当触电危险发生时能够防止进一步的发展。
  • Zuul - api网关,路由,负载均衡等多种作用,就像我们的路由器,可能有很多个设备都连接了路由器,但是数据包要转发给谁则是由路由器在进行(已经被SpringCloudGateway取代)
  • Config - 配置管理,可以实现配置文件集中管理

0|1SpringCloud和SpringBoot的关系


Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的云应用开发工具;

Spring Boot专注于快速方便开发单个微服务个体,Spring Cloud关注全局的服务治理框架,它将SpringBoot开发的一个个微服务整合并管理起来,为各个服务之间提供 服务发现(框架)、负载均衡(框架)、断路器(框架)、路由、配置管理、微代理,消息总线、全局锁、分布式会话等集成服务;

Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,可以不基于Spring Boot吗?不可以。

Spring Boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring Boot,属于依赖的关系。

你要想使用springcloud中的框架,首先必须先创建一个springboot项目 然后想用哪个框架,就导入响应依赖,springboot已经完成了自动配置

0|1SpringCloud Netflix主要框架


Netflix是个公司名 Eureka Feign Hystrix Zuul

为了和spring公司自己研发的框架 作一个区分 所以我们没有把这个框架的公司名去掉

服务发现——Netflix Eureka

服务调用——Netflix Feign

熔断器——Netflix Hystrix

服务网关——Netflix Zuul(SpringCloud Gateway)

分布式配置——Spring Cloud Config

消息总线 —— Spring Cloud Bus

0|1服务发现组件Eureka


Eureka 注册中心

官方文档:https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/

Eureka 是Spring Cloud Netflix 微服务套件中的一部分, 它基于Netflix Eureka 做了二次封装, 主要负责完成微服务架构中的服务治理功能。我们只需通过简单引入依赖和注解配置就能让Spring Boot 构建的微服务应用轻松地与Eureka 服务治理体系进行整合。

Eureka包含两个组件:Eureka Server和Eureka Client。

Eureka Server提供服务注册服务。zookeeper

Eureka Client是一个java客户端,用来简化与Eureka Server的交互、客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。

微服务项目结构

相同步骤分别创建eureka服务端,供给者,消费者

左侧内部项目pom文件为黄色,为未加载,手动加入maven即可。

父级项目

pom配置

<?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.4.6</version> <relativePath/> <!-- lookup parent from repository --> </parent> <!-- Generated by https://start.springboot.io --> <!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn --> <groupId>com.yxh</groupId> <artifactId>springcloud01_parents</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloud01_parents</name> <description>springcloud01_parents</description> <properties> <java.version>8</java.version> <spring-cloud.version>2020.0.5</spring-cloud.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

eureka_server

pom配置

<?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>com.yxh</groupId> <artifactId>springcloud01_parents</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <!-- Generated by https://start.springboot.io --> <!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn --> <groupId>com.yxh</groupId> <artifactId>springcloud01_parents_eureka_server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloud01_parents_eureka_server</name> <description>springcloud01_parents_eureka_server</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

application.properties

server.port=8888 spring.application.name=eurekaserver001 eureka.instance.hostname=localhost eureka.client.fetch-registry=false eureka.client.service-url.defaultZone=http://localhost:8888/eureka

Springcloud01ParentsEurekaServerApplication

package com.yxh.springcloud01_parents_eureka_server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; // Generated by https://start.springboot.io // 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn @SpringBootApplication @EnableEurekaServer public class Springcloud01ParentsEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(Springcloud01ParentsEurekaServerApplication.class, args); } }

consumer

pom配置

<?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>com.yxh</groupId> <artifactId>springcloud01_parents</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <!-- Generated by https://start.springboot.io --> <!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn --> <groupId>com.yxh</groupId> <artifactId>springcloud01_parents_consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloud01_parents_consumer</name> <description>springcloud01_parents_consumer</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

application.properties

server.port=7777 spring.application.name=springcloud01_consumer eureka.client.service-url.defaultZone=http://localhost:8888/eureka

Springcloud01ParentsConsumerApplication

package com.yxh.springcloud01_parents_consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; // Generated by https://start.springboot.io // 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn @SpringBootApplication @EnableDiscoveryClient public class Springcloud01ParentsConsumerApplication { @Bean public RestTemplate getRestTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(Springcloud01ParentsConsumerApplication.class, args); } }

controller

package com.yxh.springcloud01_parents_consumer.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @RequestMapping("/consumer") public class DemoController { @Autowired private DiscoveryClient discoveryClient; @Autowired private RestTemplate restTemplate; @RequestMapping("/service") public String getService(){ List<ServiceInstance> springcloud01Provider = discoveryClient.getInstances("SPRINGCLOUD01_PROVIDER"); String url = "http://"; if (springcloud01Provider!=null) { url=url+springcloud01Provider.get(0).getHost()+":"+springcloud01Provider.get(0).getPort()+"/provider/service"; } ResponseEntity<String> forEntity = restTemplate.getForEntity(url,String.class); return forEntity.getBody(); } }

provider

pom配置

<?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>com.yxh</groupId> <artifactId>springcloud01_parents</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <!-- Generated by https://start.springboot.io --> <!-- 优质的 spring/boot/data/security/cloud 框架中文文档尽在 => https://springdoc.cn --> <groupId>com.yxh</groupId> <artifactId>springcloud01_parents_provider</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springcloud01_parents_provider</name> <description>springcloud01_parents_provider</description> <properties> <java.version>8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

application.properties

server.port=8889 spring.application.name=springcloud01_provider eureka.client.service-url.defaultZone=http://localhost:8888/eureka

Springcloud01ParentsProviderApplication

package com.yxh.springcloud01_parents_provider; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class Springcloud01ParentsProviderApplication { public static void main(String[] args) { SpringApplication.run(Springcloud01ParentsProviderApplication.class, args); } }

controller

package com.yxh.springcloud01_parents_provider.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/provider") public class DemoController { @RequestMapping("/service") private String cc(){ return "服务提供者提供的服务"; } }

启动所有项目

Eureka的服务剔除与保护机制

服务剔除

注册到eureka的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。

自我保护机制的工作机制是:如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制,此时会出现以下几种情况:

  1. Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。 是网络导致微服务和注册中新连接不上,而不是微服务下线了。 等会儿网好了,注册中心和微服务又可以正常沟通了。
  2. Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点(集群中的其他节点上)上,保证当前节点依然可用。
  3. 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它集群节点中。

因此Eureka Server可以很好的应对因网络故障导致部分节点失联的情况,而不会像ZK那样如果有一半不可用的情况会导致整个集群不可用而变成瘫痪。

服务保护

我们关停一个服务,很可能会在Eureka面板看到一条警告:

image-20230803115259562

这是触发了Eureka的自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。

在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。

Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服
务的失败容错。

可以通过下面的配置来关停自我保护:

image-20230803115308879


__EOF__

作  者YXH
出  处https://www.cnblogs.com/YxinHaaa/p/17599095.html
关于博主:编程路上的小学生,热爱技术,喜欢专研。评论和私信会在第一时间回复。或者直接私信我。
版权声明:署名 - 非商业性使用 - 禁止演绎,协议普通文本 | 协议法律文本
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!

posted @   YxinHaaa  阅读(41)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
0
0
关注
跳至底部
点击右上角即可分享
微信分享提示