#首先我们要了解什么是单体应用简单来说就是:
所有功能放在一个 war/jar 文件内。
单体应用缺点:
1. 复杂性高:所有功能和代码混杂在一起,容易有 bug 隐患。
2. 技术债务高:导致后续开发很难修改和优化。
3. 部署频率低:每次都是全量部署,耗时长,风险大,迭代速度跟不上市场需求。
4. 可靠性差:任何一个功能出错,都会影响整个程序。
5. 扩展能力受限:不能根据业务模块进行伸缩扩展,操纵资源浪费。
6. 技术创新难:技术选型固定,很难引入新的框架或技术。
#接下来我们引入一种新技术来解决这种问题:
微服务(mico service):以开发一组小型服务(程序)的方式来开发一个独立的应用。
cluster(集群):多台服务器做相同的事情 。
distributed(分布式):多台服务器做不相同的事情,组合到一起是一个完整的程序。
微服务程序就是典型的分布式程序。
但是运用这种技术同时又会出现各种问题接下来我用SpringBoot项目给大家一一解答:
1. 拆分后的各个服务(service)之间如何互相发现:点击查看具体解决方案 发现:获取其他服务的位置(ip)
我们可以引入“注册中心”这一概念,引入注册中心,统一管理各个服务的位置信息:
注册:各个服务启动时候主动向注册中心汇报自身位置信息
发现:根据服务名从注册中心获取服务位置信息
2. 拆分后的各个服务(service)之间如何通信:点击查看方式ribbon具体解决方案点击查看方式feign具体解决方案
可以使用 RPC 技术解决:
RPC(Remote Procedure Call)是远程过程调用的缩写形式,可以实现在 A 程序内调用 B 程序中的方法。
如下所示,我们常用的就是restful形式的
gRPC:google
dubbe:alibaba
restful:spring cloud
把 B 程序中的方法制作为请求,A 向 B 发请求获取数据
具体框架:ribbon 和 feign
3. 如何阻止雪崩效应
雪崩:某个服务宕机导致调用链路上的所有服务宕机
使用熔断技术在服务宕机后断开其他服务和它的联系
使用降级技术替代从宕机服务器获取的数据
4. 如何让客户端访问时设置统一的入口解决方案
引入网关服务,通过管理请求地址和处理服务的对应关系
5.配置中心的引入
配置中心 Spring Cloud:为开发者提供了一系列快速构建微服务程序的工具。
使用 springboot 开发单个服务,
服务之间的协调通信使用 spring cloud 完成。
Spring Cloud 主要是整合了 Netflix 提供的一系列微服务解决方案,我们从这种讲起。
Spring Cloud Alibaba 主要是整合阿里云提供的一系列微服务解决方案。
使用 maven 聚合工程来把多个独立的微服务(maven项目)整合到一个 maven 项目中,方便开发和管理。
项目具体实现:
- 创建简单的 maven 项目
- 删除无用的 src 目录
- 在 pom 文件中管理所有子项目都会使用的依赖
<?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">
<modelVersion>4.0.0</modelVersion>
<!--springboot中添加提供相关的Maven默认依赖,让常用的包依赖可以省去version标签-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.laozhang</groupId>
<artifactId>spring-cloud-test</artifactId>
<version>1.0-SNAPSHOT</version>
<!--修改打包方式,项目的打包类型有:pom、jar、war。packaging默认是jar类型-->
<packaging>pom</packaging>
<dependencies>
<!--springboot的web工程场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--springboot测试场景启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<!--默认使用Java8,可添加以下配置修改版本-->
<java.version>1.8</java.version>
<!--springcloud版本号-->
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>
<!--一个依赖管理器,对cloud的依赖进行管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!--编译插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4. 新建 Module 开发每个服务
4.1.1eureka-server(注册中心)
注册中心
一定,一定要检查这个地方,确保新建的Model在总项目的目录下。
在注册中心的pom文件中增加注册中心的场景启动器:
<?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>spring-cloud-test</artifactId>
<groupId>com.laozhang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<!--服务注册中心场景启动器,这里暂时加一个就行-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
4.1.2 创建 application 配置文件进行配置
在application.yml中
spring:
profiles:
active: dev #配置使用dev环境
在application-dev.yml中
spring:
application:
name: eureka-server #程序名 = 服务名
server:
port: 8761 #端口号
eureka:
instance:
hostname: localhost #域名,在哪台服务器上运行就写那台的, eureka-server
client:
register-with-eureka: false #禁止在eureka-server注册自己,注册中心没必要再注册自己
fetch-registry: false #禁止在eureka-server注册自己,注册中心没必要再注册自己
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka #在注册中心注册自己的地址
4.1.3 编写 springboot 启动类 EurekaServerApplication
4.1.4 为启动类增加 @EnableEurekaServer 注解
package com.laozhang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @author zhangfan
* @date 2019/10/15
*/
@SpringBootApplication
@EnableEurekaServer //注册中心启动类添加
public class EureakServerApplication {
public static void main(String[] args) {
SpringApplication.run(EureakServerApplication.class,args);
}
}
4.1.5 启动服务,访问注册中心的监控页面
http://localhost:87614.2 user(用户服务)
4.2.1 引入依赖
<?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>spring-cloud-test</artifactId>
<groupId>com.laozhang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user</artifactId>
<!--添加单个服务(每个功能节点)的client场景启动器-->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
4.2.2创建 application 配置文件进行配置,上边已经列举过,接下来我只展示dev中的配置:
spring:
application:
name: user #程序名 = 服务名
server:
port: 8901 #端口号
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #在注册中心注册自己的请求地址
4.2.3 编写 springboot 启动类 UserApplication
4.2.4为启动类增加 @EnableEurekaClient 注解
package com.laozhang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author zhangfan
* @date 2019/10/15
*/
@SpringBootApplication
@EnableEurekaClient //单个服务启动类添加的注解
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class,args);
}
}
4.2.5 创建 UserController 并编写 findAll 和 findById 方法
package com.laozhang.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangfan
* @date 2019/10/15
*/
@RestController
public class UserController {
private String[] users = new String[]{"张三","李四","王五"};
@GetMapping("/users")
public String[] findAll(){
return users;
}
@GetMapping("/users/{id}")
public String findById(@PathVariable("id") Integer id){
return id > 0 && id < 3 ? users[id] : "id错误";
}
}
接下来再次访问这个网站,如图,已经有了一个服务
访问如下网址,便有了数据
4.3仿照上边的User,再编写一个food(菜品服务)
4.3.1引入依赖,引入的依赖和User的完全相同
4.3.2创建 application 配置文件进行配置,我只展示dev
spring:
application:
name: food #程序名 = 服务名
server:
port: 8902 #端口号
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #在注册中心注册自己的请求地址
4.3.3编写 springboot 启动类 FoodApplication
4.3.4为启动类增加 @EnableEurekaClient 注解
4.3.5编写controller代码,添加如下方法:
package com.laozhang.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangfan
* @date 2019/10/15
*/
@RestController
public class FoodController {
private String[] users = new String[]{"水果","张","三"};
@GetMapping("/foods")
public String[] findAll(){
return users;
}
@GetMapping("/foods/{id}")
public String findById(@PathVariable("id") Integer id){
return id > 0 && id < 3 ? users[id] : "id错误";
}
}
再次启动第三个food服务,访问:http://localhost:8761/
访问如下网址便有了数据
4.4zuul(网关/统一入口)
网关
4.4.1添加依赖
<?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>spring-cloud-test</artifactId>
<groupId>com.laozhang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul</artifactId>
<dependencies>
<!--添加单个服务器的场景启动器-->
<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>
</dependencies>
</project>
4.4.2创建 application 配置文件进行配置
spring:
application:
name: zuul #端口名=服务名
server:
port: 8900 #设置统一的访问端口
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #在注册中心注册自己的请求地址
zuul:
routes: #配置路由(转发给哪个服务处理)信息
user: #自定义key,一般和要配置的服务名保持一致
path: /user-api/** #客户端发送的请求地址
serviceId: user #转发给哪个服务进行处理
food:
path: /food-api/** #food 的就转发给food处理
serviceId: food #转发给哪个服务进行处理
4.4.3编写 springboot 启动类 ZuulApplication
4.4.4 为启动类增加 @EnableEurekaClient 和 @EnableZuulProxy 注解
package com.laozhang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* @author zhangfan
* @date 2019/10/15
*/
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
再次启动访问http://localhost:8761/会发现又多了一个
这时我们就可以用一个端口8900访问user和food了,如图
5
.
使用 ribbon(简单) 实现 user 调用 food 方法
user 服务中的 /users/{id}/foods 请求需要返回指定用户点过的菜品
查询用户点过的菜品方法在 food 服务中,叫做 findByUserId()
food 的操作:
1. 把 findByUserId 方法变成请求
1. 在 FoodController 中注入 FoodService
2. 新增 /foods/users/{id} 请求和对应的处理方法,
在处理方法中调用 findByUserId
user 处理 /users/{id}/foods 请求的时候需要
调用 food 的 findByUserId() 获取数据
food的service实现类
package com.laozhang.service.impl;
import com.laozhang.service.FoodService;
import org.springframework.stereotype.Service;
/**
* @author zhangfan
* @date 2019/10/15
*/
@Service
public class FoodServiceImpl implements FoodService {
private String[][] orders = new String[][]{
{"汉堡","薯条"},{"炸鸡","可乐"},{"鸡翅","雪碧","薯条"}
};
@Override
public String[] findUserById(Integer id) {
return id >= 0 & id < 3 ? orders[id] : new String[]{};
}
}
在food的controller层添加
@GetMapping("/foods/users/{id}")
public String[] findByUserId(@PathVariable("id") Integer id){
System.out.println("2");
return foodService.findUserById(id);
}
user 的操作:
1. ribbon 是 eureka-client 依赖内置的
2. 通过自定义类配置 ribbon
package com.laozhang.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* @author zhangfan
* @date 2019/10/15
*/
@Configuration
public class MyRibbonConfiguration {
@Bean //把方法返回的对象注入spring容器中,然后在其他类就可以使用@Autowired注入使用
@LoadBalanced //启用负载均衡,必须启用,平均分配给集群中的节点,减轻节点的压力
public RestTemplate restTemplate(){
//RestTemplate类对象用于在user中向food发送请求
return new RestTemplateBuilder().build();
}
}
- 在请求处理方法中向 food 发送 /foods/users/{id} 请求
1. 注入 RestTemplate 对象
2. 使用 RestTemplate 对象发送请求获取返回值
getForObject/postForObject
第一个参数:请求地址
使用服务名代替 host 和 port
运行的时候 ribbon 会拿着服务名去注册中心查询 host 和 prot 后进行替换
第二个参数:返回值类
@GetMapping("/users/{id}/foods")
public String[] foods(@PathVariable("id") Integer id){
System.out.println("1");
//调用food中的方法,获取返回值,返回给前端
//运行的时候ribbon会拿着food去注册中心查询ip和prot,替换以后发送请求
String[] orders = restTemplate.getForObject("http://food/foods/users/" + id, String[].class);
System.out.println("3");
return orders;
}
重启user和food然后访问http://localhost:8900/user-api/users/2/foods
5
.
使用 feign(专业)实现 food 调用 user
关于feign调用的超时问题
food 服务中 /foods/{id}/users 请求需要返回菜品的主人
查询菜品主人的方法在 user 服务中,叫做 findByFoodId()
food 处理 /foods/{id}/users 请求的时候需要
调用 user 的 findByFoodId() 获取数据
user 的操作:
- 把 findByFoodId 方法变成请求
- 在 UserController 中注入 UserService
- 新增 /users/foods/{id} 请求和对应的处理方法,
在处理方法中调用 findByFoodId
service的实现类
package com.laozhang.service.impl;
import com.laozhang.service.UserService;
import org.springframework.stereotype.Service;
/**
* @author zhangfan
* @date 2019/10/15
*/
@Service
public class UserServiceImpl implements UserService {
private String[] users = new String[]{"张忘","里找","王老五"};
@Override
public String findByFoodId(Integer id) {
return id >= 0 & id < 3 ? users[id] : "主人不存在";
}
}
controller中增加
@GetMapping("/users/foods/{id}")
public String findByFoodId(@PathVariable("id") Integer id){
String byFoodId = userService.findByFoodId(id);
return byFoodId;
}
food 的操作:
1. 引入依赖
再次添加
<!--专业版的-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 为启动类增加 @EnableFeignClients 注解
- 创建 UserService 接口
- 为接口添加 @FeignClient(name = “user”) 注解
- 以方法加注解的形式声明需要发送的请求
package com.laozhang.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author zhangfan
* @date 2019/10/15
*/
@FeignClient(name = "user") //说明使用food向user服务发请求
public interface UserService {
/**
* 向user服务的/users/foods/{id}地址中发送一个请求
* @param id
* @return
*/
@GetMapping("/users/foods/{id}") //向user中发送的具体请求
String findByFoodId(@PathVariable("id") Integer id);
}
@GetMapping("/users/foods/{id}")
String findByFoodId(@PathVariable(“id”) Integer id);
- 在 FoodController 中注入 UserService 后调用方法完成功能
@GetMapping("/foods/{id}/users")
public String findUserById(@PathVariable("id") Integer id){
return userService.findByFoodId(id);
}
*
使用feign很耗费资源,一旦超时就会报异常,通常在zuul的dev中添加配置
ribbon:
ReadTimeout: 100000 #配置超时时间
SocketTimeout: 100000 #配置超时时间
完成如上步骤之后输入网址进行测试
熔断和降级
*
*
熔断和降级这两个要放在一起说:只熔断不降级的话用户还是没有得到数据,熔断是降级的前提。
- 引入依赖,再我们food里添加
<!--*******熔断场景启动器********-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 在 application-dev.yml 中启用 feign 中的 hysitrix
feign:
hystrix:
enabled: true #启用hystrix
- 为启动类增加 @EnableHystrix 或 @EnableCircuitBreaker 注解
- 实现 UserService 接口,增加 @Component 注解,返回默认数据
package com.laozhang.service.impl;
import com.laozhang.service.UserService;
import org.springframework.stereotype.Component;
/**
* @author zhangfan
* @date 2019/10/16
*/
@Component
public class UserServiceImpl implements UserService {
@Override
public String findByFoodId(Integer id) {
return "主人不存在";
}
}
- 修改 UserService 中的 @FeignClient 注解,添加降级数据
@FeignClient(name = "user",fallback = UserServiceImpl.class) //说明使用feign向user服务发请求,fallback触发降级后去呢找数据
6.配置 zuul 服务中的网关超时时间(application-dev.yml 中 ):
ribbon:
ReadTimeout: 60000 #配置超时时间
SocketTimeout: 60000
如图不存在时已经返回默认数据
配置中心(gitee):
*
通常把配置文件放在 git 上进行统一管理 配置文件命名格式:服务名-dev/prod/test.properties/yml/json 比如:
创建一个服务器config-server:读取 git 上的配置文件
- 引入依赖
<dependencies>
<!--单个服务器场景启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--配置中心的场景启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
- 创建 application 进行配置
spring:
application:
name: config-server #程序名 = 服务名
cloud:
config:
server:
git:
uri: https://gitee.com/保密的啦/spring-cloud-01.git #git上配置文件项目地址
search-paths: config #git上配置文件所在目录
username: 保密@qq.com #gitee账号,创建gitee项目为私有的需要填写账号密码,否则不用
password: 保密 #gitee密码
server:
port: 8800 #端口号
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #在注册中心注册自己的请求地址
- 创建启动类
- 为启动类增加 @EnableEurekaClient 和 @EnableConfigServer 注解
package com.laozhang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author zhangfan
* @date 2019/10/16
*/
@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class,args);
}
}
接下来就是配置config-client:从 config-server 上读取对应的配置信息
替换 user 中 UserController 中的 users 内容为 git 上的配置文件中数据
user的controller中users改变为:
- 添加依赖在user的pom文件中增加
<!--读取配置服务的配置-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 重命名配置文件为 bootstrap
- 为 UserController 增加 @RefreshScope 注解
- 使用 @Value("${users}") 注解读取配置文件数据,赋值给 users 数组
@Value("${users}")
private String[] users;
- 修改 git 上的配置文件
在user的dev中添加:
cloud:
config:
discovery:
enabled: true
service-id: config-server #配置中心,服务的名字
profile: dev #使用dev环境的配置文件
management:
endpoints:
web:
exposure:
include: refresh,health #配置gitee用
6.使用 POSTMAN 发送 POST 请求:
http://localhost:8901/actuator/refresh 进行配置文件的热更新