Dubbo的负载均衡、集群容错、服务降级等机制详解
文章目录
- 1. Dubbo与RPC的关系
- 2. Dubbo的基本使用
- 2.1 Dubbo是什么?
- 2.2 负载均衡
- 2.3 服务超时
- 2.4 集群容错
- 2.5 服务降级
- 2.6 本地存根
- 2.7 参数回调
- 2.8 异步调用
- 2.9 泛化调用、泛化服务
- 3. dubbo的REST协议
- 4. dubbo的控制台
- 5. dubbo的服务路由
1. Dubbo与RPC的关系
1.1 什么是RPC?
维基百科这样解释:
远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用,所以,对于Java程序员而言,RPC就是远程方法调用。
如何理解RPC是一个计算机通信协议呢?我们已经知道RPC是专注于远程方法调用,如果实现远程方法调用,基本的就是通过网络,通过传输数据来进行调用。如下图所示
可以看到远程方法A 想要调用远程方法B,需要定义 数据类型 和 传输协议。而这些需要定义的东西作为一个协议存在于调用方和接收方,后续所有调用都遵守这个已制定的协议,这就是RPC通信协议。所以,我们其实可以看到RPC的自定义性是很高的,各个公司内部都可以实现自己的一套RPC框架,而Dubbo就是阿里所开源出来的一套RPC框架。
RPC和 HTTP、TCP的关系就是:RPC是基于HTTP、TCP协议来传输数据的,对于所传输的数据,可以交由RPC的双方来协商定义,但基本都会包括:
- 调用的是哪个类或接口
- 调用的是哪个方法,方法名和方法参数类型(考虑方法重载)
- 调用方法的入参
1.2 Dubbo与RPC的关系
上面说到实现RPC框架需要定义 数据类型 和 传输协议。而Dubbo作为阿里开源出来的RPC框架,已经制定好了对应的 传输数据类型 和传输协议,使用Dubbo必须遵循Dubbo制定好的规则。
Dubbo的传输协议见下文!
3. 自定义RPC框架思路
服务端:
- 注册服务到zk或redis。以map的形式保存起来,key = 服务名,value = List<服务器地址>。客户端请求可以负载到value的某个地址上。
注意:如果只把服务放在本地缓存中,那么其他的服务将调用失败,因为不同的服务属于不同的jvm,其他服务将无法感知另一个服务中的本地缓存。 - 把服务和服务的实现类注册到本地缓存。以map的形式保存起来,key = 服务名,value = 服务的实现类。目的是:当服务端接受到客户端请求,可以根据客户端传来的接口名,从本地缓存中拿到其实现类,然后通过反射调用客户端想要调用的方法
- 根据不同的协议启动不同的服务器。如果是Http协议则启动Tomcat,如果是Dubbo协议则启动Netty。
客户端:
指定传输的数据类型,包括接口(服务)名、方法名、参数类型、参数名,并封装成一个类Invocation。
当客户端调用某个接口时,采用jdk动态代理的方式,调用invoke代理方法,在代理方法中做增强逻辑。逻辑如下:
2.1:填充数据类型Invocation
2.2:从zk或redis中根据服务名拉取服务器地址,并负载均衡到某一个服务器地址下
2.3:获取客户端协议(dubbo 或 http),并根据协议向服务端发送数据Invocation客户端DispartchServlet拦截到客户端发过来的请求。通过JSON序列化二进制数据为Invocation 对象。根据对象中的接口名,从本地缓存中拿到对应的实现类,利用反射调用客户端客户端想要调用的方法,并输出。完成了远程服务调用!
2. Dubbo的基本使用
首先附上dubbo官方使用文档:https://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/
2.1 Dubbo是什么?
Apache Dubbo 是一款高性能、轻量级的开源 Java 服务框架,提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
其中有以下几个关键点
注册与发现
:Dubbo使用zookeeper做服务的注册中心,就是服务的提供者以临时节点的形式将服务Server信息注册保存到Zookeeper的dubbo目录下的provider的节点下,供消费者发现调用。负载均衡
: Dubbo支持负载均衡策略,就是同一个Dubbo服务被多台服务器启用后,会在在Zookeeper提供者节点下显示多个相同接口名称节点。消费者在调用Dubbo负载均衡服务时,采用权重的算法策略选择具体某个服务器上的服务,权重策略以*2倍数设置。容错机制
:Dubbo的提供者在Zookeeper上使用的是临时节点,一旦提供者所在服务挂掉,该节点的客服端连接将会关闭,故节点自动消失。所以消费者调用接口时将不会轮询到已经挂掉的接口上(延迟例外)。Dubbo容器
:Dubbo在java jvm中有自己的容器,和Spring IOC的bean一样,将服务对象保存到自己的容器中。监控中心
:监控中心主要是用来服务监控和服务治理。服务治理包含:负载均衡策略、服务状态、容错、路由规则限定、服务降级等。具体可以下载Dubbo监控中心客户端查看与设置。Dubbo的协议
:点击链接获取更多协议的详细信息
①:dubbo协议: Dubbo默认协议是dubbo协议,采用单一长连接和 NIO 异步通讯,基于hessian作为序列化协议,适合于数据量小但并发高的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
②:hessian协议: Hessian底层采用Http通讯(同步),走hessian序列化协议。适用于提供者数量比消费者数量还多,适用于文件的传输,一般较少用
③:http协议: 走json序列化,适用于浏览器查看,同时给应用程序和浏览器JS使用的服务。
④:rmi协议:走java二进制序列化,多个短连接,适合消费者和提供者数量差不多,适用于文件的传输,一般较少用
⑤:webservice协议:采用SOAP文本序列化,适用HTTP传输,常用于系统集成,跨语言调用
⑥:redis协议:基于 Redis实现的 RPC 协议。
⑦:rest协议:基于标准的Java REST API实现的REST调用支持
2.2 负载均衡
生产者在为某个接口暴露服务时,可以根据协议、ip、端口号、服务、group、version等六要素暴露多个接口实例,达到类似于集群的形式。如下所示:任意修改某个要素就算是这个接口已暴露的实例!在代码中可以通过修改@Service注解的值来暴露不同的服务实例@Service(interfaceName = "com.tuling.DemoService", version = "generic")
这样就会暴露http://ip:port/DemoService + generic
服务,消费时要根据生产者暴露的规则来进行消费。
如果在application.properties
配置文件中,配置了多个协议,Dubbo会默认会根据配置暴露多个服务实例,如果做下面的配置,那么上面的DemoService接口在zookeeper上就会有两个服务实例,一个Http的,一个Dubbo的!
# 配置多协议# dubbo协议
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
# http协议
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0
那么面对多个服务实例,消费端调用时是如何进行选择的呢?Dubbo为我们提供了四种负载均衡策略,可以通过负载均衡策略来选择一个服务实例进行调用!默认的负载策略为 random
随机调用。四种策略如下:
Random 随机
:按权重设置随机概率,可通过配置权重修改概率RoundRobin 轮询
:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。LeastActive 最少活跃数
:活跃数是指调用前后的计数差,服务调用越快,活跃数越小。提供者越慢,接收的请求就越少,因为越慢的提供者的调用前后计数差会越大,活跃数也会变大ConsistentHash 一致性Hash
:相同参数的请求总是发到同一提供者。
注意:比较难理解的是LeastActive 最少活跃数,理论上最少活跃数应该是在服务提供者端进行统计的,服务提供者统计有多少个请求正在执行中。但是Dubbo却选择在消费端进行统计最少活跃数
,为什么能在消费端进行统计?逻辑如下:
- 消费者会缓存所调用服务的所有提供者,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0
- 消费者在调用次服务时,如果负载均衡策略是leastactive
- 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机选出某一个服务提供者后,假设位p2,Dubbo就会对p2.active+1
- 然后真正发出请求调用该服务
- 消费端收到响应结果后,对p2.active-1
- 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能,下次调用会再次判断最小的active,这就解释了为什么服务提供者越慢,接收的请求就越少!因为它的active值大!
配置方式
- Provider端配置:生产者通过在暴露服务的@Servic注解上进行配置:
@Service(loadbalance = "roundrobin")
,配置时需要注意负载均衡方式均为小写! - Consumer端配置:消费端通过
@Reference(loadbalance = "leastactive ")
如果Provider和Consumer都配置,则以Consumer端配置的为准!
2.3 服务超时
在服务提供者(服务端)和服务消费者上都可以配置服务超时时间,这两者是不一样的。
@Service(version = "timeout", timeout = 4000) //服务提供者端超时时间
@Reference(version = "timeout", timeout = 3000,retries = 1) //服务消费者端超时时间
消费者调用一个服务,分为三步:
- 消费者发送请求(网络传输)
- 服务端执行服务
- 服务端返回响应(网络传输)
如果在服务端和消费端只在其中一方配置了timeout
那么没有歧义,表示消费端调用服务的超时时间,消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常,但,服务端不会抛异常,服务端在执行服务后,会检查执行该服务的时间,如果超过timeout,则会打印一个超时日志。服务会正常的执行完。
如果在服务端和消费端各配了一个timeout
那情况就比较复杂了,假设
- 服务执行为5s
- 消费端timeout=3s
- 服务端timeout=6s
那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。如果
配置服务端timeout=4s,那么由于服务执行为5s,所以服务端也会打印警告,标识服务端也超时了!
2.4 集群容错
一个服务提供多个实例(集群),集群容错是指:集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。如图:如果服务实例1调用失败,则会尝试调用服务实例2或者3,默认重试2次。
集群容错可以在@Service 和 @Reference注解上进行配置:如果两者都配置,以消费端为主!
@Service( cluster = "failfast") //服务端超集群容错
@Reference(cluster = "failfast") //消费端集群容错
Dubbo提供了六种集群容错方案:
Failover Cluster:失败自动切换
当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries=“2” 来设置重试次数(不含第一次)。Failfast Cluster:快速失败
只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录Failsafe Cluster:失败安全
出现异常时,不抛异常,直接忽略。通常用于写入审计日志等操作Failback Cluster:失败自动恢复
后台记录失败请求,定时重发。通常用于消息通知操作Forking Cluster:并行调用多个服务器
只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=“2” 来设置最大并行数Broadcast Cluster:广播调用所有提供者
逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息
2.5 服务降级
服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。集群容错和服务降级的区别在于:
- 集群容错是整个集群范围内的容错
- 服务降级是单个服务提供者的自身容错
服务降级可以在消费端的 @Reference注解上使用mock来指定降级方案
mock=force:return+null
表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。mock=fail:return+null
表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
//服务降级:如果调用失败返回123@Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")
更多服务降级方案可参考本地伪装:https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/
本地伪装其实也是对Mock的应用,便于服务端在客户端执行容错逻辑
2.6 本地存根
消费端通过Dubbo远程调用服务端,其业务实现基本都在服务端。但有些时候想在消费端也执行部分逻辑,比如:做 ThreadLocal 缓存(这个用处最大),提前验证参数,调用失败后伪造容错数据
等等,此时就需要在@Reference中带上 Stub,消费端生成服务的代理 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
//本地存根,开启stub
@Reference(stub = "true")
或者
@Reference(stub = "com.foo.DemoServiceStub") //指定stub对象
还需要自定义一个类实现DemoService接口,表示为DemoService做的本地存根,这个类是放在消费端的
public class DemoServiceStub implements DemoService {private final DemoService demoService;// 构造函数传入真正的远程代理对象public DemoServiceStub(DemoService demoService){this.demoService = demoService;}@Overridepublic String sayHello(String name) {// 此代码在客户端执行, 你可以在客户端做ThreadLocal本地缓存,或预先验证参数是否合法,等等try {return demoService.sayHello(name); // safe null} catch (Exception e) {// 你可以容错,可以做任何AOP拦截事项return "容错数据";}}
}
注意:实现类中必须有一个传入远程 DemoService 实例的构造函数
使用上述存根代码执行后,如果调用失败,则会执行DemoServiceStub中的容错方案,控制台打印”容错数据“!
2.7 参数回调
参数回调是指:当消费端调用服务成功后,希望服务端能够回调一下消费端的逻辑
既然是服务端回调消费端的逻辑,那么这个逻辑一定是存在消费端的!以DemoService服务为例
消费端调用:
@Reference(version = "callback")private DemoService demoService;//调用服务demoService.sayHello("aaa", "d1", new DemoServiceListenerImpl())
上述代码new DemoServiceListenerImpl()
中要包含着具体的回调逻辑
// 回调逻辑接口
public interface DemoServiceListener {void changed(String msg);
}
// 回调逻辑实现类
public class DemoServiceListenerImpl implements DemoServiceListener {@Overridepublic void changed(String msg) {System.out.println("被回调了:"+msg);}
}
服务端回调
// DemoService接口
public interface DemoService {// 回调方法default String sayHello(String name, String key, DemoServiceListener listener) {return null;};
}
// @Method注明了是sayHello()中索引为2的参数参与了回调,以及最大同时支持3个回调,上述代码只有一个,如果写4个就报错
@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
public class CallBackDemoService implements DemoService {@Overridepublic String sayHello(String name, String key, DemoServiceListener callback) {callback.changed(); //代理对象直接回调消费端的changed方法return ""; // 正常访问}
}
在服务端回调时,需要注意:
sayHello()
方法中的DemoServiceListener为代理对象,并不是消费端传过来的DemoServiceListenerImpl对象- 需要在
@Service
中使用@Method
注明是哪个方法中哪个参数参与了回调,以及最大同时支持几个回调
结果:
在消费端打印的是changed的内容,但这个方法是在服务端被执行的
2.8 异步调用
上文所讲的内容都是依托于同步调用的,Dubbo也提供了异步调用方式,异步调用与同步调用的求别在于:
- 服务端需要使用
CompletableFuture.supplyAsync
开启一个线程执行任务 - 客户端需要使用
CompletableFuture.whenComplete
监听异步线程执行完毕
消费端代码示例:
@Reference(version = "async")private DemoService demoService;public static void main(String[] args) throws IOException {ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class);DemoService demoService = context.getBean(DemoService.class);// 调用直接返回CompletableFutureCompletableFuture<String> future = demoService.sayHelloAsync("异步调用"); // 5//这个方法只有等异步线程执行结束才会调用future.whenComplete((v, t) -> {if (t != null) {t.printStackTrace();} else {System.out.println("Response: " + v);}});System.out.println("结束了");}
服务端代码示例
public interface DemoService {// 同步调用方法String sayHello(String name);// 异步调用方法default CompletableFuture<String> sayHelloAsync(String name) {return null;};}
@Service(version = "async")
public class AsyncDemoService implements DemoService {//同步调用@Overridepublic String sayHello(String name) {System.out.println("sayhello方法 " + name);return name;}// 主要关注这个异步调用@Overridepublic CompletableFuture<String> sayHelloAsync(String name) {System.out.println("执行了异步服务" + name);//相当于在异步线程里执行sayHello方法!return CompletableFuture.supplyAsync(() -> {return sayHello(name);});}
}
执行结果如下:
消费端:
服务端:
可以看到他们之间打印的顺序也是异步的体现!
2.9 泛化调用、泛化服务
泛化调用: 在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,就可以不用依赖该服务的接口,直接利用GenericService接口来进行服务调用。泛化调用可以用来做服务测试。
@EnableAutoConfiguration
public class GenericDubboConsumerDemo {//调用DemoService服务,并不需要注入DemoService,也不需要引入依赖@Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true)private GenericService genericService;public static void main(String[] args) throws IOException {ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);GenericService genericService = (GenericService) context.getBean("demoService");Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});System.out.println(result);}
}
泛化服务: 可以不实现具体的某个接口,而是实现GenericService
接口,并在@Service
上标明接口名即可,在调用直接注入DemoService就可以使用!
@Service(interfaceName = "com.tuling.DemoService", version = "generic")
public class GenericDemoService implements GenericService {@Overridepublic Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {System.out.println("执行了generic服务");return "执行的方法是" + s;}
}
3. dubbo的REST协议
dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)、REST(文本序列化 + http协议)等等的支持。
当我们用Dubbo提供了一个服务后,如果消费者没有使用Dubbo也想调用服务,那么这个时候就可以让我们的服务支持REST协议,这样消费者就可以通过REST形式调用我们的服务了。更多REST协议内容点击查看官网!
①:服务端配置文件修改协议为rest
dubbo.protocol.name=rest
②:服务端实现:使用@Path指定Rest风格的访问路径(注意:所有暴露的服务都必须加@Path)
@Service(version = "rest")
@Path("demo")
public class RestDemoService implements DemoService {@GET@Path("say")@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})@Overridepublic String sayHello(@QueryParam("name") String name) {System.out.println("执行了rest服务" + name);URL url = RpcContext.getContext().getUrl();return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问}}
这样就可以通过浏览器访问这个服务了,其他消费端也可以直接通过HttpCliet等非dubbo的形式去调用服务!
4. dubbo的控制台
5. dubbo的服务路由
经过服务路由可以配置黑名单、白名单、读写分离、隔离不同机房网段等等,这点在官网有很详细的解释 点击查看官网详情!!!
dubbo提供的标签路由还可以用来发布版本,什么是蓝绿发布、灰度发布?
Dubbo的负载均衡、集群容错、服务降级等机制详解相关推荐
- 融云发送自定义消息_数据源管理 | Kafka集群环境搭建,消息存储机制详解
一.Kafka集群环境 1.环境版本 版本:kafka2.11,zookeeper3.4 注意:这里zookeeper3.4也是基于集群模式部署. 2.解压重命名 tar -zxvf kafka_2. ...
- 数据源管理 | Kafka集群环境搭建,消息存储机制详解
本文源码:GitHub·点这里 || GitEE·点这里 一.Kafka集群环境 1.环境版本 版本:kafka2.11,zookeeper3.4 注意:这里zookeeper3.4也是基于集群模式部 ...
- LVS负载均衡集群服务搭建详解(一)
LVS概述 1.LVS:Linux Virtual Server 四层交换(路由):根据请求报文的目标IP和目标PORT将其转发至后端主机集群中的某台服务器(根据调度算法): 不能够实现应用层的负载均 ...
- LVS负载均衡集群服务搭建详解
一.LVS概述 1.LVS:Linux Virtual Server 四层交换(路由):根据请求报文的目标IP和目标PORT将其转发至后端主机集群中的某台服务器(根据调度算法): 不能够实现应用层的 ...
- Dubbo 源码分析 - 集群容错之 LoadBalance
1.简介 LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载"均摊"到不同的机器上.避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况.通 ...
- Dubbo 源码分析 - 集群容错之 Cluster
1.简介 为了避免单点故障,现在的应用至少会部署在两台服务器上.对于一些负载比较高的服务,会部署更多台服务器.这样,同一环境下的服务提供者数量会大于1.对于服务消费者来说,同一环境下出现了多个服务提供 ...
- 使用lvs搭建负载均衡集群
有时候,单台服务器的性能可能无法应付大规模的服务请求,且其一旦出现故障,就会造成用户在一段时间内无法访问.通过集群技术,可以在付出较低成本的情况下获得在性能.可靠性.灵活性方面的相对较高的收益. 集群 ...
- 集群(一)——LVS负载均衡集群
集群(一)--LVS负载均衡集群 一.企业群集应用 1.群集的含义 2.问题出现 3.解决办法 4.根据群集所针对的目标差异进行分类 ①.负载均衡群集 ②.高可用群集 ③.高性能运算群集 二.负载均衡 ...
- 超详细!一文带你了解 LVS 负载均衡集群!
作者 | JackTian 来源 | 杰哥的IT之旅(ID:Jake_Internet) 前言 如今,在各种互联网应用中,随着站点对硬件性能.响应速度.服务稳定性.数据可靠性等要求也越来越高,单台服务 ...
最新文章
- ORB-SLAM3在windows下的编译使用
- Commons BeanUtils包学习2
- dispatchTouchEvent onInterceptTouchEvent onTouchEvent区分
- 网站搜索功能怎么实现_电商网站上的搜索功能是如何实现的?
- miniui 查询_JQueryMiniUI按照时间进行查询的实现方法
- Solr部分更新MultiValued的Date日期字段时报错及解决方案:Invalid Date String:‘Mon Sep 14 01:48:38 CST 2015‘
- python3 datatime,python3处理时间和日期:datetime模块 – Python3教程
- PHP笔记(CSS篇)
- hudson--插件管理
- Stata进行logistic回归绘制列线图并做内部验证
- 冒烟测试 SMOKE Test
- 474922-22-0,DSPE-PEG2000-Mal,DSPE-PEG2k-Maleimide,
- 计算机网络题库——第3章数据链路层
- 世界之最VS谁是世界上最无聊的人
- 2017华为实习生招聘机考模拟题——0交换排序
- FPGA Altera Remote Update笔记
- 计算机毕业设计Java毕业论文答辩管理系统(源码+系统+mysql数据库+lw文档)
- Spring的下载及目录结构
- 为大数据定个小目标:从改变惯例开始
- (附源码)springboot中学成绩管理 毕业设计100854