avatar

SpringClound(上)

系统架构演变

随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此也不断的演 进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有在Google 带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安逸得过且过?

集中式架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。

优点

系统开发速度快

维护成本低

适用于并发要求较低的系统

缺点

代码耦合度高,后期维护困难

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

无法水平扩展

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

垂直拆分

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

优点

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

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

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

缺点

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

分布式服务

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心, 使前端应用能更快速的响应多变的市场需求。

优点

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

缺点

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

面向服务架构(SOA)

SOA架构代表面向与服务架构,俗称服务化,通俗的理解为面向与业务逻辑层开发,将共同的业务逻辑抽取出来形成一个服务,提供给其他服务接口进行调用,服务与服务之间调用使用rpc远程技术。

SOA(Service Oriented Architecture)面向服务的架构:它是一种设计方法,其中包含多个服务, 服务之间通过相 互依赖终提供一系列的功能。一个服务 通常以独立的形式存在与操作系统进程中。各个服务之间 通过网络调用。 SOA结构图:

ESB(企业服务总线),简单 来说 ESB 就是一根管道,用来连接各个服务节点。为了集 成不同系统,不同协 议的服务,ESB 做了消息的转化解释和路由工作,让不同的服务互联互通。

SOA缺点

每个供应商提供的ESB产品有偏差,自身实现较为复杂;应用服务粒度较大,ESB集成整合所有服务和协 议、数据转换使得运维、测试部署困难。所有服务都通过一个通路通信,直接降低了通信速度。

SOA架构特点

1.SOA架构中通常使用XML方式实现通讯,在高并发情况下XML比较冗余会带来极大的影响,所以最后微服务架构中采用JSON替代xml方式

2.SOA架构的底层实现通过WebService和ESB(xml与中间件混合物),Web Service技术是SOA服务化的一种实现方式,WebService底层采用soap协议进行通讯,soap协议就是Http或者是Https通道传输XML数据实现的协议。

微服务架构

微服务架构是使用一套小服务来开发单个应用的方式或途径,每个服务基于单一业务能力构建,运行在自己的进程 中,并使用轻量级机制通信,通常是HTTP API,并能够通过自动化部署机制来独立部署。这些服务可以使用不同的 编程语言实现,以及不同数据存储技术,并保持低限度的集中式管理。

API Gateway网关是一个服务器,是系统的唯一入口。为每个客户端提供一个定制的API。API网关核心是,所 有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。如它还可以具有其它职 责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。通常,网关提供RESTful/HTTP的 方式访问服务。而服务端通过服务注册中心进行服务注册和管理。

微服务的特点

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责

  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。

  • 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务接口API。并不关心服务的技术实现,做到与平台 和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。

  • 自治:自治是说服务间互相独立,互不干扰

    • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
    • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
    • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口
    • 数据库分离:每个服务都使用自己的数据源
    • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服 务都是独立的组件,可复用,可替换,降低耦合,易维护

微服务架构与SOA都是对系统进行拆分;微服务架构基于SOA思想,可以把微服务当做去除了ESB的SOA。ESB是 SOA架构中的中心总线,设计图形应该是星形的,而微服务是去中心化的分布式软件架构。两者比较类似,但其实也 有一些差别:

功能 SOA 微服务
组件大小 大块业务逻辑 单独任务或小块业务逻辑
耦合 通常松耦合 总是松耦合
管理 着重中央管理 着重分散管理
目标 保应用能够交互操作 易维护、易扩展、更轻量级的交互

服务调用方式

RPC和HTTP

无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢?

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

  • RPC:Remote Produce Call远程过程调用,RPC基于Socket,工作在会话层。自定义数据格式,速度快,效 率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
  • Http:http其实是一种网络传输协议,基于TCP,工作在应用层,规定了数据传输的格式。现在客户端浏览器 与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的 提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
    现在热门的Rest风格,就可以通过http协议来实现。

区别

RPC的机制是根据语言的API(language API)来定义的,而不是根据基于网络的应用来定义的。
如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。 相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么Spring Cloud搭建微服务是不二之选。在我们的项 目中,会选择Spring Cloud套件,因此会使用Http方式来实现服务间调用。

Http客户端工具

既然微服务选择了Http,那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很多的http客户 端工具,能够帮助我们做这些事情,例如:HttpClient OKHttp URLConnection

不过这些不同的客户端,API各不相同。而Spring也有对http的客户端进行封装,提供了工具类叫RestTemplate

Spring的RestTemplate

Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和 反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持: HttpClient OkHttp JDK原生的URLConnection(默认的)

下文会讲解RestTemplate的使用。

为什么学习SpringCloud

微服务是一种架构方式,终肯定需要技术架构去实施。

微服务的实现方式很多,但是火的莫过于Spring Cloud了。为什么?

  • 后台硬:作为Spring家族的一员,有整个Spring全家桶靠山,背景十分强大。
  • 技术强:Spring作为Java领域的前辈,可以说是功力深厚。有强力的技术团队支撑,一般人还真比不了
  • 群众基础好:可以说大多数程序员的成长都伴随着Spring框架,试问:现在有几家公司开发不用Spring? Spring Cloud与Spring的各个框架无缝整合,对大家来说一切都是熟悉的配方,熟悉的味道。
  • 使用方便:相信大家都体会到了SpringBoot给我们开发带来的便利,而Spring Cloud完全支持Spring Boot的开 发,用很少的配置就能完成微服务框架的搭建

SpringCloud出现,对微服务技术提供了非常大的帮助,因为SpringCloud 提供了一套完整的微服务解决方案,不像其他框架只是解决了微服务中某个问题。

  • 服务治理: 阿里巴巴开源的Dubbo和当当网在其基础上扩展的Dubbox、Eureka、Apache 的Consul等

  • 分布式配置中心: 百度的disconf、Netfix的Archaius、360的QConf、SpringCloud、携程的阿波罗等。

  • 分布式任务:xxl-job、elastic-job、springcloud的task等。

  • 服务跟踪:京东的hyra、springcloud的sleuth等

SpringCloud简介

简介

Spring Cloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/

Spring擅长的就是集成,把世界上好的框架拿过来,集成到自己的项目中。

  • Spring Cloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由, 负载均衡,熔断器,控制总线,集群状态等功能;

  • 协调分布式环境中各个系统,为各类服务提供模板性配置。

  • 其主要 涉及的组件包括:Eureka:注册中心 Zuul、Gateway:服务网关 Ribbon:负载均衡 Feign:服务调用 Hystrix或Resilience4j:熔断器

以上只是其中一部分,架构图:

版本

Spring Cloud不是一个组件,而是许多组件的集合;它的版本命名比较特殊,是以A到Z的为首字母的一些单词(其 实是伦敦地铁站的名字)组成:

我们在项目中,使用新稳定的Greenwich版本(格林威治)

服务治理Eureka

什么是服务治理

在传统rpc远程调用中,服务与服务依赖关系,管理比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

服务注册与发现

在服务注册与发现中,有一个注册中心,当服务器启动的时候,会把当前自己服务器的信息 比如 服务地址通讯地址等以别名方式注册到注册中心上。

另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,让后在实现本地rpc调用远程。

原理图

Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址 ,里面存储每个服务的名字和对应IP

提供者:启动后向Eureka注册自己信息(把自己的地址,应用名,提供什么服务告诉注册中心)

消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新 。通俗点就是获取注册中心里的服务提供者的名字和IP

心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态

搭建注册中心

相关配置参考后续相关参数和配置说明章节内容

创建父maven工程

微服务中需要同时创建多个项目,为了方便课堂演示,先创建一个父工程,然后后续的工程都以这个工程为父,实现 maven的聚合。这样可以在一个窗口看到所有工程,方便讲解。在实际开发中,每个微服务可独立一个工程。

src目录下不编写代码,可以将该目录删除

依赖如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR1</spring-cloud.version>
<mapper.starter.version>2.1.5</mapper.starter.version>
<mysql.version>5.1.46</mysql.version>
</properties>

<dependencyManagement>
<dependencies>
<!--spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--通用mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${mapper.starter.version}</version>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

搭建注册中心

第一步:新建一个maven工程,名字叫eureka-server,导入如下依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

第二步:修改application.yml配置文件

配置tomcat端口,应用的名字,eureka

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8011
spring:
application:
name: eureka-server # 应用名称,服务提供者,消费者,注册中心都要有名字
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:${server.port}
register-with-eureka: false # 是否将自己注册到注册中心(不需要,注册中心只需要注册服务提供者即可)
fetch-registry: false # 是否重注册中心拉取数据

第三步:添加启动类

这里需要在启动类上添加@EnableEurekaServer注解,表示自己是注册中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.itcast;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;


@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class,args);
}
}

第四步:访问注册中心

http://localhost:8011

搭建服务提供者

服务提供者的功能:提供一个接口从数据库根据ID查询用户信息,并将自己注册到注册中心。

在父工程下新建子maven工程,名字叫user-service

第一步引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

第二步编写配置文件和导入数据

  • 主要配置tomcat端口号,数据库连接池,mybatis自动扫描pojo实体类,eureka注册中心的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9001
spring:
datasource:
url: jdbc:mysql://localhost:3306/springcloud
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
application:
name: user-service
mybatis:
type-aliases-package: cn.itcast.user.pojo
eureka:
client:
service-url:
defaultZone: http://localhost:8011/eureka
  • 导入sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE DATABASE springcloud;
USE springcloud;
CREATE TABLE `user` (
`id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(100) DEFAULT NULL COMMENT '用户名',
`password` VARCHAR(100) DEFAULT NULL COMMENT '密码',
`name` VARCHAR(100) DEFAULT NULL COMMENT '姓名',
`age` INT(10) DEFAULT NULL COMMENT '年龄',
`sex` TINYINT(1) DEFAULT NULL COMMENT '性别,1男性,2女性',
`birthday` DATE DEFAULT NULL COMMENT '出生日期',
`note` VARCHAR(255) DEFAULT NULL COMMENT '备注',
`created` DATETIME DEFAULT NULL COMMENT '创建时间',
`updated` DATETIME DEFAULT NULL COMMENT '更新时间'
) ENGINE=INNODB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES ('1', 'zhangsan', '123456', '张三', '30', '1', '1964-08-08', '张三同学在学Java', '2014-09-19 16:56:04', '2014-09-21 11:24:59');
INSERT INTO `user` VALUES ('2', 'lisi', '123456', '李四', '21', '2', '1995-01-01', '李四同学在传智学Java', '2014-09-19 16:56:04', '2014-09-19 16:56:04');
INSERT INTO `user` VALUES ('3', 'wangwu', '123456', '王五', '22', '2', '1994-01-01', '王五同学在学php', '2014-09-19 16:56:04', '2014-09-19 16:56:04');
INSERT INTO `user` VALUES ('4', 'zhangliu', '123456', '张六', '20', '1', '1996-09-01', '张六同学在传智播客学Java', '2014-09-19 16:56:04', '2014-09-19 16:56:04');
INSERT INTO `user` VALUES ('5', 'lina', '123456', '李娜', '28', '1', '1988-01-01', '李娜同学在传智播客学Java', '2014-09-19 16:56:04', '2014-09-19 16:56:04');
INSERT INTO `user` VALUES ('6', 'lilei', '123456', '李雷', '23', '1', '1993-08-08', '李雷同学在传智播客学Java', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('7', 'hanmeimei', '123456', '韩梅梅', '24', '2', '1992-08-08', '韩梅梅同学在传智播客学php', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('8', 'itcast', '123456', '传智播客', '21', '2', '2008-07-08', '传智播客搞IT教育', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('9', 'heima', '123456', '黑马', '18', '2', '2012-08-08', '黑马是传智播客高端品牌', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('10', 'linus', '123456', '林纳斯', '45', '2', '1971-08-08', '林纳斯搞了linux又搞git', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('11', 'leijun', '123456', '雷布斯', '33', '2', '1983-08-08', '小爱同学;are you ok', '2014-09-20 11:41:15', '2014-09-20 11:41:15');
INSERT INTO `user` VALUES ('12', 'madaye', '123456', '马大爷', '46', '2', '1980-08-08', '马大爷花呗可以不还吗', '2014-09-20 11:41:15', '2014-09-20 11:41:15');

第三步编写pojo实体类

在cn.itcast.user.pojo 包下创建User实体类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package cn.itcast.user.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
private Long id;
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;//1男2女
private Date birthday;//出生日期
private Date created;//创建时间
private Date updated;//更新时间
private String note;//备注
}

第四步编写UserMapper接口或者UserDao接口

1
2
3
4
5
6
7
package cn.itcast.user.mapper;

import cn.itcast.user.pojo.User;
import tk.mybatis.mapper.common.Mapper;

public interface UserMapper extends Mapper<User> {
}

第五步编写UserService

  • 接口
1
2
3
4
5
6
7
package cn.itcast.user.service;

import cn.itcast.user.pojo.User;

public interface UserService {
User findById(Long id);
}
  • 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.itcast.user.service.impl;

import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;

@Override
public User findById(Long id) {
return userMapper.selectByPrimaryKey(id);
}
}

第六步编写controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;

@GetMapping("/{id}")
public User findById(@PathVariable Long id){
return userService.findById(id);
}
}

第七步编写启动类

注意:启动类一定要能扫描到其他类,其他类都处于user包下,所以启动类我创建到user包下,启动时要将自己注册到注册中心,所以要加@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.itcast.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import tk.mybatis.spring.annotation.MapperScan;

@EnableDiscoveryClient
@SpringBootApplication
@MapperScan(value = {"cn.itcast.user.mapper"})
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class,args);
}
}

第八步启动项目并访问http://localhost:9001/user/2

搭建服务消费者

在父工程下新建子maven工程,名字叫user-provider

第一步引入依赖

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

第二步编写配置文件和导入数据

  • 主要配置tomcat端口号,eureka信息
1
2
3
4
5
6
7
8
9
server:
port: 8080
spring:
application:
name: user-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:8011/eureka

第三步编写pojo实体类

在cn.itcast.user.pojo 包下创建User实体类,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.itcast.user.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;//1男2女
private Date birthday;//出生日期
private Date created;//创建时间
private Date updated;//更新时间
private String note;//备注
}

第四步编写controller

控制器直接访问注册中心中服务提供者的应用名即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cn.itcast.user.controller;

import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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;

import java.net.URI;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;

@GetMapping("/{id}")
public User findById(@PathVariable Long id)throws Exception{
User user = restTemplate.getForObject(new URI("http://user-service/user/" + id), User.class);
return user;
}
}

第五步编写启动类

注意:启动类一定要能扫描到其他类,其他类都处于user包下,所以启动类我创建到user包下,启动类上要加@EnableDiscoveryClient注解,表示要从注册中心拉取数据。在restTemplate()方法上除了加@Bean注解外,这里还加上了@LoadBalanced注解,表示使用restTemplate底层使用Ribbon客户端负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.itcast.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class,args);
}

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

第八步启动项目并访问http://localhost:8080/user/2

搭建高可用注册中心

在微服务中,注册中心非常核心,可以实现服务治理,如果一旦注册出现故障的时候,可能会导致整个微服务无法访问,在这时候就需要对注册中心实现高可用集群模式。

Eureka高可用原理

Eureka高可用实际上将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组相互注册的服务注册中心,从而实现服务清单的互相同步,达到高可用效果。

1
2
3
4
eureka:
client:
register-with-eureka: true # 是否将自己注册到注册中心
fetch-registry: true # 是否从注册中心拉取数据

Eureka集群环境服务端配置

注意:所有的注册中心应用名要保持一致

Eureka注册中心1配置如下

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8011
spring:
application:
name: eureka-server # 应用名称,注册中心名字要一致
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8012/eureka
register-with-eureka: true # 是否将自己注册到注册中心
fetch-registry: true # 是否重注册中心拉取数据

Eureka注册中心2配置如下

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8012
spring:
application:
name: eureka-server # 应用名称,注册中心名字要一致
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8011/eureka
register-with-eureka: true # 是否将自己注册到注册中心
fetch-registry: true # 是否重注册中心拉取数据

访问其中的任意一个注册中心,效果如下

Eureka集群客户端配置

消费者或者生产者客户端在向注册中心注册的时候,要把自己注册到所有的注册中心中

在defaultZone中配置配置多个注册中心,使用逗号分隔

1
2
3
4
eureka:
client:
service-url:
defaultZone: http://localhost:8011/eureka,http://localhost:8012/eureka

相关参数和配置说明

将服务注册到注册中心

eureka.client.register-with-eureka=true

1
2
3
eureka:
client:
register-with-eureka: true

服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否正确,事实上 默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。

第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性

第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081

值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群

使用IP注册

  • 默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可以在 user-service 中添加配置如下:
1
2
3
4
eureka:
instance:
ip-address: 127.0.0.1 # ip地址
prefer-ip-address: true # 更倾向于使用ip,而不是host名

服务续约

在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我 还活着”。这个我们称为服务的续约(renew);

有两个重要参数可以修改服务续约的行为;可以在 user-service 中添加如下配置项:

1
2
3
4
eureka:  
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30

lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒

lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每隔30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳, EurekaServer就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer-in-ms设定的时间)从服务列表 中移除,这两个值在生产环境不要修改,默认即可。

获取服务列表

当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从Eureka Server服务的列表拉取只读备份,然后缓存在本地。并且每隔30秒会重新拉取并更新数据。可以在 consumer-demo 项目中通过下面的参数来修改:

1
2
3
4
eureka:
client:
fetch-registry: true # 是否从注册中心拉取数据
registry-fetch-interval-seconds: 30

失效剔除和自我保护

如下的配置都是在Eureka Server服务端进行:

服务下线

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线 了”。服务中心接受到请求之后,将该服务置为下线状态。

失效剔除

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

可以通过 eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒。

1
2
3
eureka:  
server:  
eviction-interval-timer-in-ms: 5000

自我保护

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

默认情况下,EurekaClient会定时向EurekaServer端发送心跳,如果EurekaServer在一定时间内没有收到EurekaClient发送的心跳,便会把该实例从注册服务列表中剔除(默认是90秒),但是在短时间内丢失大量的实例心跳,这时候EurekaServer会开启自我保护机制,Eureka不会踢出该服务。

当服务未按时进行心跳续约时,Eureka会统计服务实例近15分钟心跳续约的 比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务 剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生 产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服 务的失败容错。
可以通过下面的配置来关停自我保护:

1
2
3
eureka:  
server:  
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)

负载均衡Ribbon

Nginx负载均衡

nginx是客户端所有请求统一交给nginx,由nginx进行实现负载均衡请求转发,属于服务器端负载均衡。

既请求有nginx服务器端进行转发。

Ribbon 负载均衡

实际环境中,往往某个服务通常会实现集群。此时获取的服务列表中就会有多个,到底该访问哪一个 呢?

一般这种情况下就需要编写负载均衡算法,在多个实例列表中进行选择。 Ribbon是从eureka注册中心服务器端上获取服务注册信息列表,缓存到本地,让后在本地实现轮训负载均衡策略。

既在客户端实现负载均衡。

使用方式

Eureka已经集成了Ribbon负载均衡组件。只需要启用即可。我们需要在SpringBoot启动类在注入RestTemplate这个Bean的时候再通过@LoadBalanced指定底层使用Ribbon即可

第一步启用Ribbon负载均衡组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.itcast.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(UserConsumerApplication.class,args);
}

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

第二步使用,直接通过服务名调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cn.itcast.user.controller;

import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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;

import java.net.URI;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;

@GetMapping("/{id}")
public User findById(@PathVariable Long id)throws Exception{
User user = restTemplate.getForObject("http://user-service/user/" + id, User.class);
return user;
}
}

修改负载均衡策略

Ribbon默认的负载均衡策略是轮询。SpringBoot也帮提供了修改负载均衡规则的配置入口在consumerdemo的配置文件中添加如下,就变成随机的了

注意:Ribbon是客户端负载均衡,所以下面代码要配置在客户端。表示访问user-service服务的时候使用随机策略

格式:{服务名称}.ribbon.NFLoadBalancerRuleClassName

1
2
3
user-server:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

熔断器Hystrix

Hystrix 在英文里面的意思是 豪猪,它的logo 看下面的图是一头豪猪,它在微服务系统中是一款提供保护机制的组 件,和eureka一样也是由netflix公司开发。 主页:https://github.com/Netflix/Hystrix/

那么Hystrix的作用是什么呢?具体要保护什么呢? Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。

雪崩问题

在微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:

如图,一次业务请求,需要调用A、P、H、I四个服务,这四个服务又可能调用其它服务。
如果此时,某个服务出现异常:

例如:微服务I 发生异常,请求阻塞,用户请求就不会得到响应,则tomcat的这个线程不会释放,于是越来越多的 用户请求到来,越来越多的线程会阻塞:

服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪 崩效应。

这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就 会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。 此时如果有很多个车型都需要这个 零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩

雪崩问题解决方案之服务降级

在高并发情况下,防止用户一直等待,使用服务降级方式,返回一个友好的提示直接给客户端,不会去处理请求,调用fallback,比如秒杀时返回当前人数过多,请稍后重试,目的是为了用户体验

雪崩问题解决方案之服务熔断

服务熔断的目的是为了保护服务(类似保险丝),在高并发的情况下,如果请求达到了一定的极限(可以自己设置阀值),如果流量超过了设置的阀值,自动开启保护服务功能,使用服务降级方式返回一个友好的提示。服务熔断机制和服务降级一起使用

雪崩问题解决方案之服务隔离

油两种,线程池隔离和信号量隔离

文章作者: 微信:hao_yongliang
文章链接: https://haoyongliang.gitee.io/2020/03/23/SpringCloud/SpringCloud(%E4%B8%8A)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 郝永亮的主页
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论