一、架构的演变
- 集中式,所有的功能都集中在同一个项目中,当访问量和数据量不断的增大时,系统会不堪重负。
- SOA 式,面向服务的架构,他通过“拆”的方式采用垂直的和水平的两种手段把一个完整的系统分割成多个项目进行设计。垂直就是按照业务功能进行拆分,各个部分是平行的;水平是按照系统的调用层次进行拆分,各个部分由上而下进行逐层调用,比如 mvc 的设计模式。这种架构当服务拆的比较多时,服务之间的调用错综复杂,此时需要服务的注册及订阅机制,以及调度和监控。此时基于 dubbo 及 zookeeper 的分布式的项目就出现了。
- 微服务阶段。
- 每个服务所处理的业务足够小,架构师在最初设计时,首先就要想到怎样把一个项目拆得足够小。
- 开发团队的人员数量要足够少,一般三到四人。
- 开发用到的技术,可以是多种。
- 每个服务使用一个独立的数据库。
- 服务间的调用采用 http 协议,因为微服务对外必须提供 rest 风格的 api。
- 微服务必须要实现集中注册,它是对微服务进行统一管理的机制。
- 统一的配置管理。
- 总线机制,保证服务间的互相调用。
二、spring cloud 框架
- 它是在 spring boot 出现后,spring 为了提供对微服务系统的充分管理,利用 netflix 公司既有的产品,并把它与 spring boot 结合在一起进行统一的使用。所以使用 sping cloud 的前提必须是使用 spring boot。
三、搭建服务间访问的项目
-
关于 spring cloud 的版本。
- 不同 spring boot 的版本对应于不同的 spring cloud 的版本。
- spring cloud 的最新版本 2020.0.x aka Ilford 对应于 spring boot 2.4.xx,问题在于最新版本已停止对 eureka 以及其它所有组件的更新及维护,除了 eureka 外,其它的组件都被剔除。
- 原来的 spring cloud 的组件在现在各自都出现了替代品。
- 为了能够使用到所有的组件,spring boot 要降低版本,例如 2.1.7。
-
一个简单的示例:
-
package com.zhong.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** * @author 华韵流风 * @ClassName ComsumerApplication * @Date 2021/10/15 15:51 * @packageName com.zhong.comsumer * @Description TODO */ @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
package com.zhong.provider.controller; import com.zhong.provider.pojo.User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author 华韵流风 * @ClassName ProviderController * @Date 2021/10/15 15:54 * @packageName com.zhong.provider.controller * @Description TODO */ @RestController @RequestMapping("/provider") public class ProviderController { @GetMapping("/user") public User user(@RequestParam String id) { User user = new User(); user.setId(id); user.setName("张三"); return user; } }
-
package com.zhong.consumer.controller; import com.zhong.consumer.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; /** * @author 华韵流风 * @ClassName ProviderController * @Date 2021/10/15 15:54 * @packageName com.zhong.provider.controller * @Description TODO */ @RestController @RequestMapping("/consumer") public class ConsumerController { @Autowired private RestTemplate restTemplate; @GetMapping("/user/{id}") public User user(@PathVariable String id) { //访问provider服务,得到user数据 return restTemplate.getForObject("http://localhost:80/provider/user?id=" + id, User.class); } }
-
-
创建基于 eureka 的注册中心
-
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloudtest</artifactId> <groupId>com.zhong</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>server_eureka</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> </project>
-
package com.zhong.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; /** * @author 华韵流风 * @ClassName EurekaApplication * @Date 2021/10/15 16:54 * @packageName com.zhong.eureka * @Description TODO */ @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class); } }
-
server: port: 10086 spring: application: name: server-eureka eureka: client: service-url: defaultZone: http://localhost:10086/eureka
-
-
让服务提供方和消费方向注册中心注册:
-
server: port: 80 spring: application: name: server-provider eureka: client: service-url: defaultZone: http://localhost:10086/eureka
-
package com.zhong.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** * @author 华韵流风 * @ClassName ComsumerApplication * @Date 2021/10/15 15:51 * @packageName com.zhong.comsumer * @Description TODO */ @SpringBootApplication //@EnableEurekaClient @EnableDiscoveryClient//范围更大 public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
-
eureka 自带了 ribbon,ribbon 也是 spring cloud 中的一个组件,作用是实现负载均衡。
-
分别在两个端口启动 provider;
-
访问消费者服务,可以观察到负载均衡采用的是轮询的方式,另一个是随机的方式。在消费者的启动类注入 restTemplate 的方法上添加注解 @LoadBalanced。
-
package com.zhong.consumer.controller; import com.zhong.consumer.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @author 华韵流风 * @ClassName ProviderController * @Date 2021/10/15 15:54 * @packageName com.zhong.provider.controller * @Description TODO */ @RestController @RequestMapping("/consumer") public class ConsumerController { @Autowired private RestTemplate restTemplate; /*@Autowired private DiscoveryClient discoveryClient;*/ @GetMapping("/user/{id}") public User user(@PathVariable String id) { //访问provider服务,得到user数据 // return restTemplate.getForObject("http://localhost:80/provider/user?id=" + id, User.class); /*List<ServiceInstance> instances = discoveryClient.getInstances("server-provider"); String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort(); System.out.println(url); return restTemplate.getForObject(url + "/provider/user?id=" + id, User.class);*/ return restTemplate.getForObject("http://server-provider/provider/user?id=" + id, User.class); } }
-
-
搭建高可用的注册中心,eureka 的集群。
-
注册中心可以自己注册给自己,也可以注册给对方,在集群环境下,个服务器之间互相复制注册的信息,如果整个集群挂了,每个客户端都复制了注册中心的所有数据,此时整个项目仍然可以正常运行。
-
server: port: 10087(6) spring: application: name: server-eureka eureka: client: service-url: defaultZone: http://localhost:10086(7)/eureka
-
server: port: 8081 spring: application: name: server-consumer eureka: client: service-url: defaultZone: http://localhost:10086/eureka,http://localhost:10087/eureka
-
四、服务的容错和熔断
-
所谓容错就是当部分微服务不能提供正常服务时,整个系统仍然能够正常工作。
- 服务雪崩,当一个服务不能工作,其它调用它的服务也随之不能工作,直到大量的服务都停止。
-
解决雪崩的手段主要有两种,一种是服务降级,另一种就是熔断。
- 服务降级,也就是用另一个服务来替换当前的不能工作的服务,还有就是如果当前服务不是主要服务,就停止当前服务。比较典型的做法就是当访问降级的服务时,返回一条消息告诉对方该服务不能工作了。
- 服务熔断,当服务不能正常工作时,暂时把请求断开,持续一段时间后再尝试服务是否已正常,如果正常就恢复服务。有三个状态,一是 open(打开熔断),二是 close(关闭熔断,这是正常状态),三是 half open(半开放,尝试阶段)。
-
Hystrix(豪猪,有自我保护的功能),是 spring cloud 的组件之一,可以实现服务的降级和熔断。
-
其功能的实现在 consumer 上,当 provider 不能正常提供服务时,通过 consumer 来实现降级和熔断。
-
使用:
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
添加注解 @EnableCircuitBreaker。
-
注意 @SpringCloudApplication 具有 @SpringBootApplication,@EnableDiscoveryClient,@EnableCircuitBreaker 三个注解的功能。
-
在控制器的请求方法上添加 @HystrixCommand(fallbackMethod = “方法名”),该方法具有了降级和熔断功能。fallbackMethod 的属性值是本类中的一个方法名,要求方法的参数和返回值与原方法相同,当原方法访问的服务不能正常工作时,系统会转去执行该方法,这是服务降级的主要做法。在该方法中可以返回一条消息。
-
当 provider 的响应时间大于1s时,服务就降级了,这个1s是默认值。
-
-
实现服务的熔断,主要观察三个状态的转换。(有些配置项没有提示)
-
circuitBreaker: requestVolumeThreshold: 10 sleepWindowInMilliseconds: 10000 errorThresholdPercentage: 50
-
以上配置的作用,当连续10次请求中有超过50%的请求不能正常工作,进入熔断状态(open),10s后加入半开状态进行请求尝试(half open),如果尝试成功就进入 close (熔断关闭)。
-
-
五、Feign
-
原意是伪装,把写在代码中访问服务提供者的方法调用代码(不优雅),转换为使用接口的方式,利用接口的代理对象中的方法来发请求并接收结果。伪装体现在表面上是使用接口,实际上底层依然是用原来的方法。
-
依赖:
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在 Application(即启动类) 上添加注解 @EnableFeignClients。
-
创建接口(关键):
-
package com.zhong.consumer.client; import com.zhong.consumer.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; /** * @author 华韵流风 * @ClassName ProviderClient * @Date 2021/10/19 10:35 * @packageName com.zhong.consumer.client * @Description TODO */ @FeignClient(value = "server-provider") public interface ProviderClient { /** * 示例方法 * * @param id id * @return User */ @GetMapping("/provider/user") User user(@RequestParam String id); }
-
-
在需要发出请求的类中注入接口对象(使用这种方式不需要注入 RestTemplate 了):
-
package com.zhong.consumer.controller; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import com.zhong.consumer.client.ProviderClient; import com.zhong.consumer.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * @author 华韵流风 * @ClassName ProviderController * @Date 2021/10/15 15:54 * @packageName com.zhong.provider.controller * @Description TODO */ @RestController @RequestMapping("/consumer") public class ConsumerController { /*@Autowired private RestTemplate restTemplate;*/ @Autowired private ProviderClient providerClient; /*@Autowired private DiscoveryClient discoveryClient;*/ @GetMapping("/user/{id}") @HystrixCommand(fallbackMethod = "callbackUser") public User user(@PathVariable String id) { //访问provider服务,得到user数据 // return restTemplate.getForObject("http://localhost:80/provider/user?id=" + id, User.class); /*List<ServiceInstance> instances = discoveryClient.getInstances("server-provider"); String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort(); System.out.println(url); return restTemplate.getForObject(url + "/provider/user?id=" + id, User.class);*/ // return restTemplate.getForObject("http://server-provider/provider/user?id=" + id, User.class); if ("1".equals(id)) { throw new RuntimeException(); } return providerClient.user(id); } public User callbackUser(String id) { User user = new User(); user.setId(id); user.setName("该服务暂时不可用,请稍后访问……"); return user; } }
-
-
feign 内部集成了 hystrix,如果基于 feign 来使用 hystrix 必须在配置中添加:
-
feign: hystrix: enabled: true
-
-
如果使用 feign,restTemplate 会在底层使用,@LoadBalanced 是默认使用的。所以仍然具有负载均衡的功能。
-
六、zuul
- 它具有网关的含义,也就是微服务的网关。
- 在互联网中,网关是不同局域网间的连接桥梁,通过网关不同网段内的设备可以互相访问。在网关中可以定义一些访问规则。
- 微服务为什么需要网关
- 如果前端的请求直接访问微服务,每个微服务都有唯一的接口及主机,会造成前端访问接口的地址混乱。
- 对于前端应该要有一种统一的访问微服务的地址书写形式,另外,需要把权限的代码集中或提前到 openservice 的前面。
- 所有 openservice 都要解决跨域的问题,微服务的项目一定是前后端分离的。
- 网关内部提供了多个过滤器,不同的过滤器实现不同的功能。也提供了自定义的过滤器。我们可以利用它来完成权限相关的功能,这样权限的代码就提到了网关中,不在 openservice 中来书写。
-
网关的实现
-
创建网关微服务
-
添加依赖:
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
-
-
添加注解:@EnableZuulProxy。
-
添加配置,重点在于通过 eureka 拿到应用的名称及访问地址,对以上的名称及访问地址进行映射,外部的请求可以通过映射来决定访问哪个服务。
-
server: port: 10010 spring: application: name: server-gateway eureka: client: service-url: defaultZone: http://localhost:10086/eureka #zuul: # routes: # #映射的id,可以随意写,但要保证唯一性 # server-provider: # path: /server-provider/** # serviceId: server-provider # server-consumer: # path: /server-consumer/** # serviceId: server-consumer #简便写法 zuul: routes: server-provider: /server-provider/** server-consumer: /server-consumer/** #访问前缀 prefix: /web
-
带前缀的访问:<http://localhost:10010/web/server-provider/provider/user?id=2>
-
更省略的写法就是什么都不写,path 默认为 /应用名/**
-
-
-
网关过滤器:
-
网关内置了多种过滤器,它们都实现了 IZuulFilter 接口,它们各自起到不同的作用。
-
如果需要实现自定义的过滤器,也需要继承 ZullFilter 抽象类,重写相关方法。
-
package com.zhong.gateway.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.exception.ZuulException; import org.springframework.stereotype.Component; /** * @author 华韵流风 * @ClassName MyFilter * @Date 2021/10/19 15:06 * @packageName com.zhong.gateway.filter * @Description TODO */ @Component public class MyFilter extends ZuulFilter { @Override public String filterType() { //前置过滤 return "pre"; } @Override public int filterOrder() { //返回执行顺序,数字越小越先执行 return 0; } @Override public boolean shouldFilter() { //是否执行本过滤器 return true; } @Override public Object run() throws ZuulException { //实现过滤器功能的方法 System.out.println("filter run"); return null; } }
-
-
前端过滤例子:
-
package com.tensquare.web.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @author 华韵流风 * @ClassName WebFilter * @Date 2021/10/20 17:11 * @packageName com.tensquare.web.filter * @Description TODO */ @Component public class WebFilter extends ZuulFilter { @Override public String filterType() { // pre :可以在请求被路由之前调用 // route :在路由请求时候被调用 // post :在route和error过滤器之后被调用 // error :处理请求时发生错误时被调用 return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { System.out.println("zuul的过滤器已执行……"); //向header中添加鉴权令牌 RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); String authorization = request.getHeader("Authorization"); if (authorization != null) { requestContext.addZuulRequestHeader("Authorization", authorization); } return null; } }
-
server: port: 10020 spring: application: name: tensquare-web eureka: client: service-url: defaultZone: http://localhost:10086/eureka zuul: routes: tensquare-gathering: /gathering/** tensquare-article: /article/** tensquare-base: /base/** tensquare-friend: /friend/** tensquare-qa: /qa/** tensquare-recruit: /recruit/** tensquare-spit: /spit/** tensquare-user: /user/** tensquare-search: /search/** sensitive-headers: - Cookie,Set-Cookie,Authorization #集合写法之一
-
-
后端过滤例子
-
package com.tensquare.manager.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import com.tensquare.utils.JwtUtil; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * @author 华韵流风 * @ClassName WebFilter * @Date 2021/10/20 17:11 * @packageName com.tensquare.web.filter * @Description TODO */ @Component public class ManagerFilter extends ZuulFilter { @Autowired private JwtUtil jwtUtil; @Override public String filterType() { // pre :可以在请求被路由之前调用 // route :在路由请求时候被调用 // post :在route和error过滤器之后被调用 // error :处理请求时发生错误时被调用 return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { System.out.println("zuul的过滤器已执行……"); //向header中添加鉴权令牌 RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); if ("OPTIONS".equals(request.getMethod())) { return null; } String url = request.getRequestURL().toString(); if (url.indexOf("/admin/login") > 0) { System.out.println("登陆页面" + url); return null; } //获取头信息 String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Claims claims = jwtUtil.parseJWT(token); if (claims != null) { if ("admin".equals(claims.get("roles"))) { requestContext.addZuulRequestHeader("Authorization", authHeader); System.out.println("token 验证通过,添加了头信息" + authHeader); return null; } } } //终止运行 requestContext.setSendZuulResponse(false); //http状态码 requestContext.setResponseStatusCode(401); requestContext.setResponseBody("无权访问"); requestContext.getResponse().setContentType("text/html;charset=UTF‐8"); return null; } }
-
server: port: 10010 spring: application: name: tensquare-manager eureka: client: service-url: defaultZone: http://localhost:10086/eureka zuul: routes: tensquare-gathering: /gathering/** tensquare-article: /article/** tensquare-base: /base/** tensquare-friend: /friend/** tensquare-qa: /qa/** tensquare-recruit: /recruit/** tensquare-spit: /spit/** tensquare-user: /user/** sensitive-headers: [Cookie,Authorization,Set-Cookie] jwt: config: ttl: 3600000 key: zhong
-
-
七、监控
-
在 hystrix 组件中提供了针对熔断功能的监控仪表盘。
-
监控对象必须是使用了 hystrix 的微服务。
-
必须创建一个独立的监控微服务。
-
实现步骤:
-
添加依赖:
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
-
配置:
-
management: endpoints: web: exposure: include: hystrix.stream
-
-
注解:@EnableHystrixDashboard
-
观察结果:
- <http://localhost:8081/actuator/hystrix.stream>
- <http://localhost:8081/consumer/user/2>
-
八、集中配置组件 SpringCloudConfig
-
gitee
-
创建配置中心微服务
-
文件命名规则:{application}-{profile}.yml或{application}-{profile}.properties
-
依赖:
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
-
-
注解:@EnableConfigServer
-
配置:
-
server: port: 12000 spring: application: name: server-config cloud: config: server: git: uri: https://gitee.com/hylf/tensquare.git username: #私人仓库需要账号密码 password: ********
-
-
消费端:
-
依赖:
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
-
-
把原来的配置名改为 bootstrap.yml,配置内容为:
-
spring: cloud: config: name: base profile: dev label: master uri: http://127.0.0.1:12000
-
-