目录
1.微服务简介
API Gateway 网关是一个服务器,是系统的唯一入口。网关提供 RESTful/HTTP 的方式访问服务。而服务端通过服务注册中心进行服务注册和管理。
微服务特点:
- 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
- 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供REST的接口即可。
- 自治:自治是说服务间互相独立,互不干扰
- 团队独立:每个服务都是一个独立的开发团队。
- 技术独立:因为是面向服务,提供REST接口,使用什么技术没有别人干涉
- 前后端分离:采用前后端分离开发,提供统一REST接口,后端不用再为PC、移动段开发不同接口
- 数据库分离:每个服务都使用自己的数据源
HTTP
2.SpringCloud简介
- Eureka:注册中心
- Zuul:服务网关
- Ribbon:负载均衡
- Hystrix:断路器
- Feign:服务调用
- ......
3.微服务模拟
3.1父工程创建
创建maven工程
pom.xml
<?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>
<groupId>com.vv</groupId>
<artifactId>parent</artifactId>
<!--聚合父工程-->
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>11</java.version>
<!--greenwich版本clound对应spring boot 2.1.x-->
<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>
<!--springcloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--tkmybatis-->
<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>
</project>
3.2服务提供者
<?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>parent</artifactId>
<groupId>com.vv</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
server:
#配置端口号
port: 9091
#配置数据库
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url:jdbc:mysql://localhost:3306/test
username: root
password:
导入数据库
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_name` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`name` varchar(50) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` int(11) DEFAULT NULL,
`birthday` date DEFAULT NULL,
`created` date DEFAULT NULL,
`updated` date DEFAULT NULL,
`note` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
/*Data for the table `tb_user` */
insert into `tb_user`(`id`,`user_name`,`password`,`name`,`age`,`sex`,`birthday`,`created`,`updated`,`note`) values
(1,'zhangsan','1','张三a',18,1,'2019-02-27','2019-02-27','2019-02-27','在学习Java...'),
(2,'lisi','1','李四ab',18,1,'2019-02-27','2019-02-27','2019-02-27','在学习Java...'),
(3,'wangwu','1','王五abc',18,1,'2019-02-27','2019-02-27','2019-02-27','在学习Java...'),
(4,'fanbingbing','1','范冰冰aa',18,2,'2019-02-27','2019-02-27','2019-02-27','在学习Java...'),
(5,'guodegang','1','郭德纲bb',18,1,'2019-02-27','2019-02-27','2019-02-27','在学习Java...'),
(6,NULL,NULL,'周星驰cc',18,NULL,'2020-06-01','2020-06-01',NULL,NULL);
编写实体类
@Data
@Table(name = "tb_user")
public class User{
// id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 用户名
private String userName;
// 密码
private String password;
// 姓名
private String name;
// 年龄
private Integer age;
// 性别,1男性,2女性
private Integer sex;
// 出生日期
private Date birthday;
// 创建时间
private Date created;
// 更新时间
private Date updated;
// 备注
private String note;
}
public interface UserMapper extends Mapper<User> {
}
@Service
public class UserService {
private UserMapper mapper;
@Autowired
public void setMapper(UserMapper mapper) {
this.mapper = mapper;
}
public User selectById(Long id){
return mapper.selectByPrimaryKey(id);
}
}
这里可以设置一下,省略掉这个报错
controller(提供REST风格web 服务,根据id查询用户 )
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService service;
@GetMapping("/{id}")
public User queryById(@PathVariable Long id){
return service.selectById(id);
}
}
代码结构:
启动测试:http://localhost:9091/user/1
3.3服务调用者
pom.xml
<?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>parent</artifactId>
<groupId>com.vv</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>comsumer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
@SpringBootApplication
public class ComsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ComsumerApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping("/consumer/{id}")
public User select(@PathVariable Long id){
String url = "http://localhost:9091/user/" + id;
//通过调用user-service的服务得到个json格式的对象然后反序列化成一个对象
return restTemplate.getForObject(url, User.class);
}
}
项目结构
启动两个项目测试,因为我们没有配置端口,那么默认就是8080
访问:http://localhost:8080/consumer/1
3.4存在问题
存在问题:
- 在consumer中,我们把url地址硬编码到了代码中,不便后期维护
- consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
- consumer不清楚user-service的状态,服务宕机也不知道
- user-service只有1台服务,不具备高可用性
- 即便user-service形成集群,consumer还需自己实现负载均衡
4.Eureka注册中心
在刚才的案例中,user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要记录服务提供者的地址。将来地址出现变更,还需要及时更新。在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。不仅开发困难,将来测试、发布上线都会非常麻烦。
4.1Eureka
4.2案例
4.2.1EurekaServer
<?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>parent</artifactId>
<groupId>com.vv</groupId>
<version>1.0.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>
<!--jdk版本11会报错,下面添加jaxb-api-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</project>
server:
port: 10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
defaultZone: HTTP://127.0.0.1:10086/eureka
# 不注册自己
register-with-eureka: false
# 不拉取服务
fetch-registry: false
4.2.2注册服务
<!--Eureka客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件:
server:
#配置端口号
port: 9091
#配置数据库
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password:
#指定服务名称,将来会作为应用的id使用(eureka)
application:
name: user-service
#不用指定register-with-eureka和fetch-registry,因为默认是true
#配eureka地址
eureka:
client:
service-url:
defaultZone: HTTP://127.0.0.1:10086/eureka
重启项目访问Eureka监控页面
4.2.3发现服务
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
启动类添加注解
配置文件
spring:
application:
name: consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@RequestMapping("/consumer/{id}")
public String select(@PathVariable Long id){
String url = "http://localhost:9091/user/" + id;
//通过调用user-service的服务得到个json格式的对象然后反序列化成一个对象
return restTemplate.getForObject(url, String.class);
}
/**
* 以前是吧url写死,现在使用eureka
*/
@Autowired
private DiscoveryClient discoveryClient;
@RequestMapping("/consumer2/{id}")
public String select2(@PathVariable Long id){
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = serviceInstances.get(0);
String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/user/" + id;
return restTemplate.getForObject(url, String.class);
}
}
测试:http://localhost:8080/consumer2/1
4.3Eureka进阶
4.3.1Eureka核心角色
- 服务注册中心 :Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的eureka-server
- 服务提供者 :提供服务的应用,可以是Spring Boot应用,也可以是其它任意技术实现,只要对外提供的是REST风格服务即可。本例中就是我们实现的user-service
- 服务消费者 :消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的consumer
4.3.2高可用Eureka
假设要搭建两条EurekaServer的集群,端口分别为:10086和10087
server:
port: ${port:10086} #如果port参数不存在,则使用10086
spring:
application:
name: eureka-server
eureka:
client:
service-url:
# eureka 服务地址,如果是集群的话;需要指定其它集群eureka地址
defaultZone: ${defaultZone:HTTP://127.0.0.1:10086/eureka}
# 不注册自己
# register-with-eureka: false
# 不拉取服务
# fetch-registry: false
所谓的高可用注册中心,其实就是把EurekaServer自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。
- 删除了register-with-eureka=false和fetch-registry=false两个配置。因为默认值是true,这样就会吧自己注册到注册中心了。
- 把service-url的值改成了另外一台EurekaServer的地址,而不是自己
然后同时启动两个端口
4.3.3配置客户端和服务端
服务注册
- 第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性,user-service
- 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:user-service:8081
- 值则是服务的实例对象,也就是说一个服务,这样可以同时启动多个不同实例,形成集群。
默认注册时使用的是主机名或者localhost,如果想用ip进行注册,可在 user-service 中添加配置
服务续约
生产环境中,我们不需要修改这个值。
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%,当EurekaServer节点在短时间内丢失过多客户端(可能发生了网络分区故障)。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式
5.负载均衡Ribbon
5.1Ribbon实现负载均衡
然后调用http://localhost:8080/consumer2/1
5.2负载均衡策略
6.Hystrix断路器
6.1雪崩问题
- 线程隔离
- 服务降级
6.2线程隔离&服务降级
6.2.1原理
线程隔离示意图
服务降级:优先保证核心服务
- 线程池已满
- 程序运行异常
- 服务熔断触发服务降级
- 请求超时
6.2.2案例
在consumer中添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在启动类上添加注解
(熔断的降级逻辑方法必须跟正常逻辑方法保证相同的参数列表和返回值声明)
当select方法不能正常执行的时候,就执行fallback方法
当我把user-service关闭时
6.2.3默认fallback
6.2.4超时设置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000
6.3服务熔断
在服务熔断中使用熔断器(断路器),服务调用方可以自己进行判断哪些服务反应慢或存在大量超时,可以针对这些服务进行主动熔断,防止整个系统被拖垮。
Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可以自动重连。通过断路的方式,将后续请求直接拒绝,一段时间(默认5秒)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打开,拒绝请求的服务。
- Closed:关闭状态(断路器关闭),所有请求都正常访问。
- Open:打开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进行休眠计时
熔断案例:
为了能够精确控制失败和成功,我们在consumer的controller中加入一段逻辑
当我们连续访问id=1的数据时,就会触发熔断,断路器打开,一切请求被降级处理。
此时访问id为2的请求,会发现返回的也是失败,而且失败时间很短,只有20毫秒左右;因进入半开状态之后2是可以的。
不过默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们通过配置修改熔断策略:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 #服务降级超时时间
circuitBreaker:
requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20
sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
7.Feign
7.1简介
之前获取url虽然动态的获取到了user-service,但需要编写类似的大量重复代码,格式基本相同,无非参数不一样。
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
Feign整合了ribbon+restTemplate
7.1案例
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
创建Feign客户端
创建新的controller
在启动类添加注解,启动feign
启动测试:http://localhost:8080/cf/1
7.2超时控制
默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
7.3Feign打印日志
Feign支持的日志级别:
步骤
1、添加配置日志Bean
2、配置文件
7.4服务降级
Feign也集成了Hystrix,但默认是关闭的,需要在配置文件中打开
修改consumer的配置文件
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
Feign中的fallback不同于Ribbon中
1、首先要创建fallback处理类,实现上面的UserFeignClient接口
2、然后再UserClient中指定fallback类
测试: