前言

为什么采用微服务架构?

随着需求的增加,单体架构(单个系统)已经不足以支撑庞大的业务功能,故演变为将业务进行拆分,根据不同的职责划分模块,由多个服务来完成,每个服务称微服务,多个微服务之间通过协议标准通信,共同完成整个项目的功能实现,这样也减轻了当个服务的压力。

个人理解,微服务架构为SOA(服务化架构)的一种具体演变跟产物。

\

初步构建

项目搭建

创建3个服务

微服务间实体类公用 微服务公共实体类_微服务

1.用于存放公共实体的服务API

2.充当消费者的服务consumer

3.充当提供者的服务provider


三者间的联系:服务提供者负责查询数据库,服务消费者负责调用提供者获取数据,其中存放公共实体的服务能保证一个实体不需要在多个服务中多次创建

 将实体服务当作依赖,将坐标添加至消费者,提供者两个服务中

<dependency>
    <groupId>com.example</groupId>
    <artifactId>springboot-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

各个服务配置

api服务配置

配置文件

server:
  port: 8000
spring:
  application:
    name: springboot-api

创建实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Dept implements Serializable {
    private String id;
    private String dname;
    private String dbSource;
}

消费者服务配置

配置文件

server:
  port: 8001
spring:
  application:
    name: consumer

创建一个接口(后面补充)

@RestController
@RequestMapping("/consumer")
public class DeptConsumerController {

}

提供者服务配置

导入依赖

<dependencies>
        <!--拿到实体类 配置api module-->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>springboot-api</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

配置文件

server:
  port: 8002
spring:
  application:
    name: springcloud-provider-dept-8001
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:postgresql://127.0.0.1:5432/DB01?stringtype=unspecified
    username: postgres
    password: 123456
    driver-class-name: org.postgresql.Driver
mybatis:
  type-aliases-package: com.example.springbootapi.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

controller

@RestController
@RequestMapping("/dept")
public class DeptController {

    @Resource
    private DeptService deptService;

    @GetMapping
    public List<Dept> getDept(){
        return this.deptService.getList();
    }
}

service

@Service
public class DeptService {

    @Resource
    private DeptMapper deptMapper;

    public List<Dept> getList(){
        return this.deptMapper.getDept();
    }
}

mapper

@Mapper
public interface DeptMapper {
    List<Dept> getDept();
}

DeptMapper.xml

<select id="getDept" resultType="com.example.springbootapi.pojo.Dept">
    SELECT * FROM dept
</select>

数据库对应的表

微服务间实体类公用 微服务公共实体类_微服务_02

 启动服务访问接口http://127.0.0.1:8002/dept,请求到数据则搭建成功

[{"id":"1","dname":"研发部","dbSource":"DB01"},{"id":"2","dname":"测试部","dbSource":"DB01"}]

至此三个简单服务搭建完毕

通过RestTemplate实现跨服务调用

不同服务不同职责,通常是通过消费者服务来获取数据的(直接面向用户的服务),而提供者连接数据库提供数据,调用消费者服务的接口去调用提供者的接口获取数据,所以需要跨服务调用接口

对消费者服务的DeptConsumerController进行补充

@RestController
@RequestMapping("/consumer")
public class DeptConsumerController {

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/list")
    public List<Dept> getList(){
        return restTemplate.getForObject("http://127.0.0.1:8002/dept",List.class);
    }

}

先启动服务提供者,再启动消费者与api服务

此时若消费者服务启动失败,报错如下

A component required a bean of type 'org.springframework.web.client.RestTemplate' that could not be...

Spring Boot 1.3默认提供RestTemplate的Bean,1.4版本后不再提供该实例,故需要自己创建RestTemplate的配置将其注入至spring容器中。

@Configuration
public class RestTempalteConfig {
    //@LoadBalanced 负载均衡
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

测试

调用接口

127.0.0.1:8001/consumer/list成功拿到数据

[{"id":"1","dname":"研发部","dbSource":"DB01"},{"id":"2","dname":"测试部","dbSource":"DB01"}]

至此跨服务调用接口成功

服务注册与发现

当服务越来越多,就需要所有服务进行统一管理,这样能灵活处理服务端口,地址改变带来的问题

服务注册中心Eureka

能自动注册,发现服务,对服务状态,信息等进行统一管理,当需要获得其他服务信息时,只需要向Eureka查询即可

Eureka服务搭建

新建服务

微服务间实体类公用 微服务公共实体类_spring cloud_03

该服务pom新增依赖

<parent>
    <groupId>com.example</groupId>
    <artifactId>springcloud-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>

<dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

父项目pom中新增spring cloud依赖

注意:springboot与springcloud版本是有互相对应的,请选择合适的版本,否则不兼容会报错

此处将父项目中的springboot版本修改为2.3.3.RELEASE

<groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.3.3.RELEASE</version>

   <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

启动类新增注解@EnableEurekaServer开启注册中心

yml配置文件

server:
  port: 8888
spring:
  application:
    name: eureka-server
eureka:
  client:
    #不获取服务端 自己为注册中心
    fetch-registry: false
    #不将自己注册到注册中心
    register-with-eureka: false
    #将eureka服务指向自己
    service-url:
      defaultZone: http://localhost:8888/eureka

启动服务,访问127.0.0.1:8888,至此注册中心服务搭建成功

微服务间实体类公用 微服务公共实体类_spring_04

将服务注册至Eureka

 3个服务均添加依赖

注意此处用的是spring-cloud-starter-netflix-eureka-client,而eureka服务用的是server,当然此处用server也可

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

yml配置文件添加配置

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka

全部启动后,3个服务成功注册至注册中心

微服务间实体类公用 微服务公共实体类_微服务间实体类公用_05

服务发现

yml配置文件修改3个服务的名字依旧为provider,consumer,api,eurekaserver

spring:
  application:
    name: provider

将消费者服务中使用ip跨服务调用直接改为服务名,修改后如下

http://127.0.0.1:8002/dept -> http://provider/dept

@RestController
@RequestMapping("/consumer")
public class DeptConsumerController {

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/list")
    public List<Dept> getList(){
        return restTemplate.getForObject("http://provider/dept",List.class);
    }

}

负载均衡作用

同i一服务可创建多个实例,当其中一个挂了,还可以访问其他实例(新增一个服务提供者实例)

微服务间实体类公用 微服务公共实体类_微服务间实体类公用_06

 对应eureka服务列表,可以看到提供者服务有两个实例

微服务间实体类公用 微服务公共实体类_spring cloud_07

此时调用消费者接口/consumer/list,restTemplate会根据效用的策略(默认为轮询,及你一下,我一下)去调用两个不同端口的实例

微服务间实体类公用 微服务公共实体类_java_08

返回结果变更为XML问题

此处返回结果可能存在问题,变为XML格式

原因:整合eureka依赖时,带入jackson-dataformat-xml依赖导致

解决:

(注意一定要先重启服务提供者,在启动服务消费者)

1.每个请求注解上增加(consumer服务的controller添加)

@GetMapping(value = "/list",produces = { "application/json;charset=UTF-8" })

2.引入eureka依赖是排除上述依赖(consumer服务的pom文件添加)

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.fasterxml.jackson.dataformat</groupId>
                    <artifactId>jackson-dataformat-xml</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

eureka集群搭建

问题:避免单个服务中心崩溃,导致整个系统服务访问失效

方案:创建多个eureka服务实例,每个实例注册至另一个形成环形

如:eureka1-》eureka2-》eureka3-》eureka1

如:eureka1-》eureka2 -》eureka1

两个实例配置文件例子

注意有用用的是本地测试,所以需要更改主机名字为eureka1/eureka2,配置文件中不能直接使用localhost

修改hosts文件中将eureka1,eureka2解析到127.0.0.1

新增:

127.0.0.1  eureka1

127.0.0.1  eureka2


hosts文件位置:

Mac /etc/hosts

Windows C:\Windows\system32\drivers\etc\hosts

server:
  port: 8888
spring:
  application:
    name: eurekaserver
eureka:
  instance:
  	#主机名改为eureka1
    hostname: eureka1
  client:
    #不获取服务端 自己为注册中心
    fetch-registry: false
    #不将自己注册到注册中心(此时要注释掉该配置)
    #register-with-eureka: false
    #将eureka服务指向另一个服务中心
    service-url:
      defaultZone: http://eureka1:8889/eureka
server:
  port: 8889
spring:
  application:
    name: eurekaserver
eureka:
  instance:
  	#主机名改为eureka2
    hostname: eureka2
  client:
    #不获取服务端 自己为注册中心
    fetch-registry: false
    #不将自己注册到注册中心(此时要注释掉该配置)
    #register-with-eureka: false
    #将eureka服务指向另一个服务中心
    service-url:
      defaultZone: http://eureka1:8888/eureka

其他服务都注册至两个服务中心

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka,http://localhost:8889/eureka

负载均衡LoadBalancer

2020年后,LoadBalancer代替Ribbon进行负载均衡,添加注解@LoadBalanced,LoadBalancerInterceptor会对服务调用请求进行拦截

LoadBalancer提供了两种负载均衡策略

RandomLoadBalancer(随机)

RoundRobinLoadBalancer (轮询)默认

* 修改为随机分配策略

创建配置类

public class LoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory){
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

修改RestTempalteConfig

@Configuration
@LoadBalancerClient(value = "provider", configuration = LoadBalancerConfig.class)
//指定provider服务,表示调用次服务时,采用此种策略,configuration指定采用定义的配置类
public class RestTempalteConfig {
    //@LoadBalanced 负载均衡
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

OpenFeign跨服务调用

OpenFeign同RestTemplate,为HTTP客户端请求工具,相较于后者,更为方便

消费者服务pom文件导入依赖(因为只有消费者服务设计跨服务调用)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

启动类添加注解@EnableFeignClients开启

消费者服务中,创建需要调用的服务的客户端接口

通过@FeignClient("服务名")设置为该服务在此服务中的客户端

@FeignClient("provider") //服务名
public interface DeptClient {

    //接口路径,入参,返回类型与提供者服务需要调用的接口一致
    @RequestMapping("/dept")
    List<Dept> getDept();
}

修改原RestTemplate的调用方式

@RestController
@RequestMapping("/consumer")
public class DeptConsumerController {

//    @Resource
//    private RestTemplate restTemplate;
    @Resource
    DeptClient deptClient;

    @GetMapping(value = "/list")
    public ResponseEntity getList(){
        //List<Dept> list = restTemplate.getForObject("http://provider/dept",List.class);
        List<Dept> list = deptClient.getDept();
        return new ResponseEntity(list, HttpStatus.OK);
    }
}

启动各个服务测试,请求http://127.0.0.1:8001/consumer/list

运行正常得到结果

[{"id":"1","dname":"研发部","dbSource":"DB01"},{"id":"2","dname":"测试部","dbSource":"DB01"}]

熔断器Hystrix(已过时,学习使用)

场景:连续的服务调用中,当最后一个服务崩溃,导致所有服务崩溃

服务降级

如果当接口调用失败(服务未开或者服务崩溃情况),返回其他预设的数据

在消费者服务进行整合

导入依赖

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

启动类添加注解@EnableHystrix开启

在方法调用中,添加备选处理方法(@HystrixCommand(fallbackMethod = "方法")指定服务调用异常后调用的方法)

@RestController
@RequestMapping("/consumer")
public class DeptConsumerController {

//    @Resource
//    private RestTemplate restTemplate;
    @Resource
    DeptClient deptClient;

    @GetMapping(value = "/list")
    @HystrixCommand(fallbackMethod = "afterSituation")
    public ResponseEntity getList(){
        //List<Dept> list = restTemplate.getForObject("http://provider/dept",List.class);
        List<Dept> list = deptClient.getDept();
        return new ResponseEntity(list, HttpStatus.OK);
    }

    //getList()调用失败后 将默认调用该方法进行返回
    ResponseEntity afterSituation(){
        return new ResponseEntity("provider服务未启动", HttpStatus.OK);
    }
}

启动eureka服务,消费者服务,不启动提供者服务调用http://127.0.0.1:8001/consumer/list

返回结果为

provider服务未启动

服务熔断

熔断机制是用于应对服务雪崩的微服务保护机制,如上,当请求正常方法时,发现跨服务调用失败后,及调用备用方法,之后一段时间将会持续调用备用方法而不去调用原方法,当一段时间后,重新尝试调用原方法,如果失败则继续同上,如果其他服务正常,调用成功,则熔断器会关闭

通过OpenFeign实现服务降级

消费者服务中,直接实现客户端接口,编写方法则为补救措施,去掉DeptConsumerController中上述新增的@HystrixCommand(fallbackMethod = "afterSituation")

@Component
public class DeptClientAfterSituation implements DeptClient {
    @Override
    public List<Dept> getDept() {
        return Collections.emptyList();
    }
}

客户端接口中指定降级策略@FeignClient(value = "provider",fallback = DeptClientAfterSituation.class)

@FeignClient(value = "provider",fallback = DeptClientAfterSituation.class) //服务名
public interface DeptClient {

    //接口路径,入参,返回类型与提供者服务需要调用的接口一致
    @RequestMapping("/dept")
    List<Dept> getDept();
}

yml配置文件新增配置,开启OpenFeign熔断机制

feign:
  hystrix:
    enabled: true

重启测试 得到结果[]

重启provider服务,过一会儿后,请求得到数据

[{"id":"1","dname":"研发部","dbSource":"DB01"},{"id":"2","dname":"测试部","dbSource":"DB01"}]

可考虑整合hystrix-dashboard来搭建监控界面

路由网关Gateway

之前使用的Zuul网关也已经停更


为什么要用网关?

不需要所有服务都向外暴露,通过路由机制起到保护作用。用户请求经过路由转发至各个服务,同服务多个实例间也可实现负载均衡功能

网关服务创建

创建新的网关服务并导入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

yml配置文件

server:
  port: 9000
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka
spring:
  application:
    name: gateway

单机学习eureka+gateway存在的问题

问题:学习到网关后,才体现出单机学习的问题,因为每个服务注册到服务中心,都默认将主机名作为实例名注册如下图,这样网关分配路由时,会用 DESKTOP-6LI1316当作服务名,最后导致拼接后的地址为 DESKTOP-6LI1316:8001/consumer/list

导致请求失败 

微服务间实体类公用 微服务公共实体类_spring_09

 解决:

方法1:更改主机名,但该方式比较麻烦,且为了方便学习故不考虑

方法2:每个实例注册时,采用ip注册到服务中心的方式,如消费之实例添加eureka.instance.prefer-ip-address=true(需要注册的服务添加,本次学习中在消费者服务,网关服务添加即可,服务中心无需添加)

eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka

 注册后点击实例名发现访问的是ip

172.XX.XX.XX:8001/actuator/info 而不是 desktop-6li1316:8001/actuator/info

路由的配置

动态路由配置(通过服务名来调用),gateway服务yml文件中加上配置

spring:
  application:
    name: gateway
  cloud:
    gateway:
      # 路由配置列表  - 表示列表中的一块路由配置
      routes:
        - id: consumer-service   # 本路由名称
          uri: lb://consumer  # 路由的地址,lb-负载均衡,http-正常转发
          predicates: # 路由规则,通过断言来实现分配 更多规则见官网 此处以Path为例
            - Path=/con/**  # 访问该路径,都会被转发到上述指定服务
          filters: #过滤器 一些规则
            - StripPrefix=1 #表示转发后拼接路径去掉第一个con/

此处必须加上filters.StripPrefix=1的配置,原因为当访问/con/consumer/list,本应该转发至consumer/consumer/list,但是此处依旧会带上/con/变为/con/consumer/consumer/list导致访问路径不存在404,故需要去掉/con/

启动后通过网关访问

http://127.0.0.1:9000/con/consumer/list

得到数据,且与http://127.0.0.1:8001/consumer/list

获取到相同数据

[{"id":"1","dname":"研发部","dbSource":"DB01"},{"id":"2","dname":"测试部","dbSource":"DB01"}]

到此 网关转发测试成功,通过网关就不需要直接访问每个具体服务的接口,各个微服务在内网中互相调用,对外只暴露网关,起到了保护作用

路由过滤器

对http请求与响应进行修改,如上述使用filters.StripPrefix=1的使用(更多配置见官网)

spring:
  application:
    name: gateway
  cloud:
    gateway:
      # 路由配置列表  - 表示列表中的一块路由配置
      routes:
        - id: consumer-service   # 本路由名称
          uri: lb://consumer  # 路由的地址,lb-负载均衡,http-正常转发
          predicates: # 路由规则,通过断言来实现分配 更多规则见官网 此处以Path为例
            - Path=/con/**  # 访问该路径,都会被转发到上述指定服务
          filters: #过滤器 一些规则
            - StripPrefix=1 #表示转发后拼接路径去掉第一个con/
            - AddRequestHeader=Test, 你好 #向头部加入数据Test=你好

GlobalFilter自定义过滤器

网关服务新增过滤器

@Component
public class MyGatewayFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    //获取ServerHttpRequest对象,非HttpServletRequest
    ServerHttpRequest request = exchange.getRequest();
    //request.getQueryParams()获取携带请求参数
    List<String> value = request.getQueryParams().get("test");
    if(value != null && value.contains("你好")) {
        //传递至下一级进行过滤
        return chain.filter(exchange);
    }else {
        //返回响应,不再继续
        return exchange.getResponse().setComplete();
    }
}
}

*Config配置中心

(一般不这么用,简单了解)

当一个服务创建多个实例,如上述学习,一个实例配置一个yml文件,如果一个地方配置修改,需要所有配置文件修改,当达到一定数量就很麻烦,故考虑一个配置中心用于统一配置管理

官方文档Spring Cloud Config

创建配置中心服务(服务端配置)

导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
  	<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

启动类添加@EnableConfigServer

配置文件

server:
  port: 9001
spring:
  application:
    name: configserver
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka

需要结合Github或者本地git创建存放配置文件仓库(此处不深入学习)

仓库中配置文件名字格式: 服务名-环境.yml(如consumer-dev.yml)

yml添加本地仓库配置

spring:
  cloud:
    config:
      server:
        git:
          #本地仓库,远程仓库直接写地址 http://git...
          uri: file://${user.home}/Desktop/config-repo
          # 默认分支设定为你自己本地或是远程分支的名称
          default-label: main

通过 http://localhost:9001/Git分支/服务名-环境.yml

如:127.0.0.1:9001/main/consumer-dev.yml

客户端配置

其他服务对于配置中心服务均为客户端

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

本地创建bootstrap.yml,删除原application.yml,也可以保留,但是bootstrap优先级更高,先读取,后读取application

spring:
  cloud:
    config:
      #文件名
      name: bookservice
      #配置服务器的地址
      uri: http://localhost:9001
      # 环境
      profile: dev
      # git分支
      label: main

这样服务端也能通过配置中心配置拿到配置文件

微服务CAP原则

Consistency一致性:保证同一时刻,所有节点拿到的数据都是最新值

Availability可用性:非故障节点每个请求得到相应(借助熔断器等)

Partition tolerance分区容错性:节点突然不连通,整个网络划分几块区域,数据散落情况需要得到容忍

一般来说,三者不能保证同时成立,最多保证两者成立

方案:

AC:某个节点更新后,结果通知所有节点保证一致性,速度提升保证可用性

CP:某个节点最新数据发送至其他节点,保证一致性,等所有节点都得到数据后进行响应,保证了容错性,但此时可用性相对无法保证

AP:基于CP,放弃一些些节点的数据同步,不等待所有节点得到数据就进行响应,此时放弃了统一性,保证可用性和容错性(eureka服务注册为例,某节点失效自动切换其他节点,只要一个服务中心实例正常运行,则保证了可用性,但不保证数据为最新数据)

到此SpringCloud传统知识及基础入门完毕,如果需要进一步学习可参考第二部分