SPRING CLOUD微服务DEMO-上篇
Posted qianbixin
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SPRING CLOUD微服务DEMO-上篇相关的知识,希望对你有一定的参考价值。
目录
1. 微服务架构
系统架构的演变从单应用,到根据每个模块垂直拆分,到分布式服务,SOA,到目前进化成了微服务形态。
微服务的特点
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
- 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
- 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
- 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护
2. 远程调用方式
2.1 RPC/RMI
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型
2.2 Http
Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议。也可以用来进行远程服务调用。缺点是消息封装臃肿。
2.3 如何选择
既然两种方式都可以实现远程调用,我们该如何选择呢?
- 速度来看,RPC要比http更快,虽然底层都是TCP,但是http协议的信息往往比较臃肿,不过可以采用gzip压缩。
- 难度来看,RPC实现较为复杂,http相对比较简单
- 灵活性来看,http更胜一筹,因为它不关心实现细节,跨平台、跨语言。RPC方式需要在API层面进行封装,限制了开发的语言环境。
因此,两者都有不同的使用场景:
- 如果对效率要求更高,并且开发过程使用统一的技术栈,那么用RPC还是不错的。
- 如果需要更加灵活,跨语言、跨平台,显然http更合适
微服务,更加强调的是独立、自治、灵活。而RPC方式的限制较多,因此微服务框架中,一般都会采用基于Http的Rest风格服务。
3. Http客户端工具
可以使用一些流行的开源Http客户端工具请求Rest接口
- HttpClient
- OKHttp
- URLConnection
3.1 RestTemplate
使用Http客户端工具请求Rest接口,得到数据后反序列化成对象。这样做比较麻烦,Spring提供了一个RestTemplate
模版工具类,对Http客户端进行了封装(默认使用URLConnection),实现了对象和Json的序列化和反序列化,方便得很。下面搭建一个Spring Boot工程,同时演示RestTemplate如何使用。
4. Spring Boot 搭建项目
微服务的一个重要特点是每个服务都是一个可以独立运行的项目,要是按照以前的SSM的搭建方式,无法做到服务的快速搭建和部署。于是Spring Boot应运而生,它的理念就是约定大于配置,你选好模块,自动帮你搭建好环境,非常便捷。
接下来用Spring Boot搭建两个简单的用户微服务:user-service
和user-consume
,实现的功能是:user-consume使用RestTemplate调用user-service服务。
具体参考这篇笔记(子文章)SPRING BOOT搭建两个微服务模块
5. Spring Cloud简介
Spring Cloud是实现微服务架构的技术。它集成了Netflix公司的一些微服务组件。(如果你不知道这家公司,那你肯定不爱看美剧。)
Netflix微服务架构图
- Eureka:注册中心
- Zuul:服务网关
- Ribbon:负载均衡
- Feign:服务调用
- Hystix:熔断器
6. 微服务场景模拟
在第四节我们已经搭建好了user-service
和user-consume
两个微服务。
consume使用RestTemplate调用service提供的rest接口,获得json数据,反序列化成User对象返回到前台。其中存在着一些问题:
- consume中调用的rest接口的地址是硬编码的,不方便维护。
- 如果service的rest接口变更或关闭了,consume并不知情,最终什么都得不到。
- service只有1台,一旦宕机,整个服务就不可用了。如果扩展多台,那consume又要自己考虑负载均衡。
接下来介绍的几个组件就是为解决这些问题而生的。
7. Eureka注册中心
7.1 简介
Eureka负责微服务的管理。
如果一个项目有数十个微服务,调用者想要自己找到一个适合的,可用的微服务是很麻烦的一件事。
就比如你想要坐车出门,自己上街拦出租车就很麻烦,要么司机拒载,要么车里已经有人了,要么干脆就没有车来...后来就出现了滴滴,做为一个网约车的“注册中心”,可以为你分配离你最近的空闲出租车。
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”
机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
7.2 原理图
- EurekaServer:注册中心,可以是多个Eureka的集群,对外暴露自己的地址。
- 服务提供者:启动后在注册中心中注册,成功后定期使用http方法向注册中心发送心跳包,表明自己还活着。
- 客户端消费者:向注册中心订阅服务。注册中心会向消费者发送合适的服务提供者列表,并且定期更新。需要使用某个服务时,可以从列表中找到并调用。
7.3 搭建注册中心
搭建过程和第4节说的一样,注意选择Eureka模块即可。
7.3.1 代码和配置文件
- 启动类
@EnableEurekaServer
注解表示这是一个注册中心
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
- 配置文件
现在我们只启动一个eureka作为注册中心。
这是Spring官方文档中的Standalone Eureka Server
的建议配置。
注意eureka下的client配置,这个配置意思是eureka应用作为一个客户端,对注册中心的动作。
其中service-url的配置必须要填写,内容是注册中心的地址,如果有多个,逗号隔开。
defaultZone路径后面必须加上/eureka
后缀,别问我为啥。
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
register-with-eureka: false # 是否注册自己的信息到EurekaServer,默认是true
fetch-registry: false # 是否拉取服务列表,默认是true
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
访问http://localhost:10086即可看到注册中心的内容,eureka只有一个,服务列表为空:
7.4 将user-service注册到eureka
7.4.1 添加依赖
eureka客户端依赖
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Spring Cloud的依赖,注意我这里的版本是Greenwich.SR1
<!-- SpringCloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Spring的仓库地址 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
7.4.2 开启EurekaClient功能
添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient // 开启EurekaClient功能
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
7.4.3 配置eureka客户端属性
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/XXXXX?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: XXXXX
password: XXXXX
hikari:
maximum-pool-size: 20
minimum-idle: 10
application:
name: user-service # 应用名称
mybatis:
type-aliases-package: com.vplus.demo.userservice.pojo
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true # 当调用getHostname获取实例的hostname时,返回ip而不是host名称
ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
7.4.4 效果
7.5 user-consume从eureka中获取服务
7.5.1 添加依赖
同上7.4.1
7.5.2 开启EurekaClient功能
同上7.4.2
7.5.3 配置eureka客户端属性
server:
port: 8082
spring:
application:
name: user-consume # 应用名称
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
7.5.4 修改UserService
- 之前是调用UserDao,UserDao使用
RestTemplate
请求远程接口得到数据。 - 现在我们在service层,从eureka中拉取服务列表,得到接口地址后请求数据。
@Service
public class UserService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;// Eureka客户端,可以获取到服务实例信息
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
// String baseUrl = "http://localhost:8081/user/";
// 根据服务名称,获取服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
// 获取ip和端口信息
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/";
ids.forEach(id -> {
// 我们测试多次查询,
users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
// 每次间隔500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
return users;
}
}
7.6 eureka集群
7.6.1 搭建集群
Eureka可以集群搭建形成高可用的注册中心。多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
我们要两个Eureka,端口号分别是10086、10087
可以使用Idea的启动器复制功能复制eureka的启动器。
将应用A的启动器复制出一个A2,A启动后,修改配置再启动A2,这样我们就可以得到两个配置不同的应用了。
先把原来的eureka配置信息改为
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10087/eureka
启动EurekaApplication,启动起来后再将配置改为
server:
port: 10087 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
再启动EurekaApplication2,此时两个注册中心就形成了集群。
两个eureka相互注册,10086的defaultZone的url地址的端口号为10087,注意这一点。
再将两个客户端的Eureka相关配置改为
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
访问http://localhost:10086和http://localhost:10087都可以看到效果:
7.7 Eureka相关配置
7.7.1 服务提供者
服务提供者主要做两个动作:服务注册
和服务续约
- 服务注册的对应配置,true表示服务启动后会向注册中心发送注册请求,默认就是开着的。
eureka
client
register-with-erueka: true
- 服务续约的对应配置,这些都是默认值
eureka:
instance:
#服务失效时间,默认值90秒
lease-expiration-duration-in-seconds: 90
#服务续约(renew)的间隔,默认为30秒
lease-renewal-interval-in-seconds: 30
默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,注意,这是服务提供者告诉注册中心:我每30秒续约一次,90秒没有续约,就表示我失效了,是配置在服务提供者上的,不是配置在注册中心上的。
我实验的时候,关闭一个服务,eureka几乎是瞬间就知道服务down了
试了好几次都是这样,可能我的关闭动作出发了什么东西吧,时间有限,先不管这个问题,留个坑将来看。
后来我注意到服务关闭后会输出一句:Unregistering ...,推测服务正常关闭时会自己通知注册中心。
- 实例ID名的修改
在页面上,服务提供者实例ID显示为:localhost:user-service:8081,
格式为:${hostname} + ${spring.application.name} + ${server.port}
,对应配置为
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
修改后启动,变成了
7.7.2 服务消费者
消费者需要拉取服务列表,拉取时间间隔默认为30秒1次,对应配置为,如果是开发环境可适当缩小方便开发
eureka:
client:
registry-fetch-interval-seconds: 30
7.7.3 失效剔除和自我保护
失效剔除:每个一段时间剔除掉失效的服务,默认为60s,为了方便开发设置为1s。
服务器怎么知道失效了?看看上面服务提供者的配置,注意这几个配置的联系。
自我保护:生产环境中,由于网络延迟等原因,失效服务并不一定是真正失效了。如果被标记为失效的服务太多,超过了85%,此时eureka会把这些服务保护起来,先不剔除,保证大多数服务还可用。在开发中将自我保护模式关掉,方便开发。
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(默认为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间为1s(默认为60s)
8. Robbin负载均衡
8.1 开启两个user-service
具体操作参照7.6.1,这里设置两个service的端口为8080和8081
8.2 开启负载均衡
注意,是在调用端即consume上开启负载均衡,Eureka中已经集成了Ribbon,无需引入新的依赖,只需要在调用端的RestTemplate
的注册Bean方法上添加注解:@LoadBalanced
即可。这个方法位于UserConsumerApplication里。
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
}
具体的调用方式也需要修改,将UserService中的queryUserByIds方法修改成这样。
public List<User> queryUserByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
// 地址直接写服务名称即可
String baseUrl = "http://user-service/user/";
ids.forEach(id -> {
// 我们测试多次查询,
users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
// 每次间隔500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
return users;
}
}
8.3 默认负载均衡策略分析
在org.springframework.cloud.client.loadbalancer
包里面有一个LoadBalancerInterceptor
,它就是实现负载均衡的拦截器。
跟踪源码,找到了RibbonLoadBalancerClient
,用consume多次请求接口,断点调试
execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
方法:
就是轮询。
8.4 修改负载均衡策略
一个配置即可修改,有多种配置规则,这里使用随机规则。
注意格式,是以服务名称开头的。
server:
port: 8082
spring:
application:
name: user-consume # 应用名称
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
###################################负载均衡配置###############################################
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
8.5 重试机制
如果有一台服务突然挂掉了,而eureka还来不及将其清除出服务列表,或者消费者拉取的服务列表还有缓存,一旦请求到这台挂掉的服务就会报错。虽然多次请求后结果也能出来,但体验非常不好。
Ribbon的重试机制就是解决这个问题的。
引入依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
开启Spring Cloud的重试机制并配置
server:
port: 8082
spring:
application:
name: user-consume # 应用名称
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
registry-fetch-interval-seconds: 5
instance:
prefer-ip-address: true # 当其它服务获取地址时提供ip而不是hostname
ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
user-service:
ribbon:
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
就好了。
这篇文章参考的是黑马培训的《乐优商城》
以上是关于SPRING CLOUD微服务DEMO-上篇的主要内容,如果未能解决你的问题,请参考以下文章
Spring Cloud中如何保证各个微服务之间调用的安全性(下篇)
Spring Cloud Alibaba微服务介绍 及 Nacos注册中心实战
Spring Cloud与Docker微服务架构实战 PDF
干货分享微服务spring-cloud(4.负载均衡ribbon与熔断器hystrix)