当网站流量小时,只需一个应用,将所有功能部署在一块。
问题:
缺点:
服务间的调用,服务间首先相互独立,其次相互调用。
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式调用是关键。
优点:
缺点:
SOA:面向服务的架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。
缺点:
微服务的特点:
如果公司都采用java技术性,那么使用dubbo作为服务架构是不错的。
如果技术栈多样化,那么SpringCloud搭建微服务是不二之选。
java代码间调用,主流客户端:
Spring提供了一个RestTemplate模板工具类,对于Http的客户端进行了封装,并实现了对象与json的序列化和反序列化。
RestTemplate并没有限定Http的客户端类型,而是进行了抽象。目前常用的3中都有支持:
先在项目中注册一个RestTemplate对象,可以在启动类位置注册:
package com.leyou.httpdemo;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class HttpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HttpDemoApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
//默认的HtppUrlConnection,将Template注册到Spring
return new RestTemplate();
}
}
在测试类中直接注入
package com.leyou.httpdemo;
import com.leyou.httpdemo.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Test
public void httpGet() {
User user = this.restTemplate.getForObject("http://localhost:8088/user/16", User.class);
System.out.println(user);
}
}
通过RestTemplate的getForObject()方法,传递url地址及实体类的字节码,RestTemplate会自动发起请求,接受相应,并且帮我们对相应结果进行反序列化。
注意:这个项目和要引用的项目接口要保证tomcat端口不同,否则会报错端口被占用。
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中,
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
Netflix
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
<mapper.starter.version>2.0.3</mapper.starter.version>
<mysql.version>5.1.32</mysql.version>
<pageHelper.starter.version>1.2.5</pageHelper.starter.version>
</properties>
<!-- dependency管理器中所有子工程都有该依赖-->
<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>
<!--通用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>
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<!-- Spring工程的web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
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>cn.itcast</groupId>
<artifactId>cloud-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>user-service</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
<mapper.starter.version>2.0.3</mapper.starter.version>
<mysql.version>5.1.32</mysql.version>
<pageHelper.starter.version>1.2.5</pageHelper.starter.version>
</properties>
<!-- dependency管理器中所有子工程都有该依赖-->
<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>
<!--通用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>
</project>
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>cloud-demo</artifactId>
<groupId>cn.itcast</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<!-- Spring工程的web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost/yum6
username: root
password: 123456
mybatis:
type-aliases-package: cn.itcast.user.pojo
创建cn.itcast.user.pojo包
包:cn.itcast
类:UserApplication
package cn.itcast;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("cn.itcast.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
包:cn.itcast.user.pojo
类:User
package cn.itcast.user.pojo;
import lombok.Data;
import tk.mybatis.mapper.annotation.KeySql;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Data
@Table(name="tb_user")
public class User {
//id
@Id
@KeySql(useGeneratedKeys = true)
private Long id;
//用户名
// @Column(name = "username")
private String username;
//密码
private String password;
private String phone;
private Date created;
}
包:cn.itcast.user.mapper
类:UserMapper
使用通用Mapper
package cn.itcast.user.mapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import tk.mybatis.mapper.common.Mapper;
public interface UserMapper extends Mapper<User> {
}
包:cn.itcast.user.service
类:UserService
package cn.itcast.user.service;
import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id){
return userMapper.selectByPrimaryKey(id);
}
}
包:cn.itcast.user.controller
类:UserController
package cn.itcast.user;
import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
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;
@RestController
@RequestMapping("user")
public class Controller {
@Autowired
private UserService userService;
//@PathVariable是spring3.0的一个新功能:接收请求路径中占位符的值
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id){
return userService.queryById(id);
}
}
http://localhost:8081/user/16访问成功!!
该项目作为调用者,不需要再访问数据库了,他是调用服务方user-service。所以在引入依赖时只需要引入web启动器就可以了。
加入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
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>cloud-demo</artifactId>
<groupId>cn.itcast</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-demo</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
包:cn.itcast
类:ConsumerApplication
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
//启动类
@SpringBootApplication
public class ConsumerApplication {
//需要调用,所以需要这个对象
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
因为不需要数据库了,所以实体类中不需要@Table、@Id等一些通用Mapper映射注解了。
package cn.itcast.consumer.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class User {
//id
private Long id;
//用户名
// @Column(name = "username")
private String username;
//密码
private String password;
private String phone;
private Date created;
}
包:cn.itcast.consumer.web
类:ConsumerController
package cn.itcast.consumer.web;
import cn.itcast.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;
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
String url = "http://localhost:8081/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
http://localhost:8080/consumer/17访问成功!!
服务的调用中地址写死了。若是发生服务迁移或地址被占用,就无法调用了。若是部署集群,写死了就只访问一个,无法访问其他的。
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
注:消费者是拉取服务列表,而不是注册中心向消费者推送服务列表。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
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>cloud-demo</artifactId>
<groupId>cn.itcast</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>
该依赖已经将Eureka服务的注册发现等所有功能都实现了。
因为前面user-service占用端口8081;
consumer-service占用端口8080;
所以想要启动该项目,需要为它配置端口,比如配置为10086。
server:
port: 10086
包:cn.itcast
类: EurekaServer
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class);
}
}
@EnableEurekaServer:启动Eureka的服务。
http://localhost:10086/
会出现以下问题:
启动时报错,可以忽略,它是可以进入注册中心。但是进入后也一直在报错。
注意:
作为一个注册中心,也不能挂,也应该搭建成集群。所以他即是一个服务器,但其实也是客户端。
原因:
此时没有人注册,所以会报错。
解决方案:
要设置自己注册自己。
在application.yml中添加如下配置:
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
再次启动:
访问http://localhost:10086/
问题:服务名未知
解决方案:在application.yml配置中添加如下:
spring:
application:
name: eureka-server
出现的不是ip
解决方法:
在application.yml中添加如下配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
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>cloud-demo</artifactId>
<groupId>cn.itcast</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<!-- Spring工程的web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
</dependencies>
</project>
在类上添加注解**@EnableDiscoveryClient**
为什么使用@EnableDiscoveryClient?
答:现在我们使用的注册中心是Eureka,但是springcloud内置的注册中心不止一种,它不仅支持Eureka,也支持zookeper等好几种。当添加注解是@@EnableEurekaClient的时候,将来只能注册到Eureka注册中心。但是用@EnableDiscoveryClient注解,更加通用,即注解的对应服务也是客服端,但是内部既能兼容Eureka,也能兼容Zookeper等。
package cn.itcast;
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("cn.itcast.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
完整的application.yml内容:
server:
port: 8081
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost/yum6
username: root
password: 123456
mybatis:
type-aliases-package: cn.itcast.user.pojo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
启动user-service
访问http://localhost:10086/
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
@EnableDiscoveryClient
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
//启动类
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
//需要调用,所以需要这个对象
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
将该项目改为8088
server:
port: 8088
spring:
application:
name: consumer-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
因为大家都是客户端,所以服务的提供者也有可能消费者,服务的消费者也有可能是提供者。
服务者可能会部署成集群,开启很多个tomcat,但是这些服务者的服务名都是一样的,所以在不同tomcat上部署的同一个服务就叫做服务的实例。
若pom.xml不存在下面依赖,就添加进去。
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
package cn.itcast.consumer.web;
import cn.itcast.consumer.pojo.User;
import com.netflix.appinfo.InstanceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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.util.List;
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
此处测试只有一个服务,所以直接instances.get(0)。但是以后需要些负载均衡算法。
http://localhost:8088/consumer/19访问成功!
EurekaServer也是可以是一个集群,形成高可用的Eureka中心。
说个Eureka Server之间会互相注册为服务。当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
如果有三个Eureka,则每一个Eureka Server都需要注册到其他几个Eureka服务中。
先修改eureka-server
复制上述eureka-server项目
此时复制的这个eureka称为eureka-server2由于无法更改,所以就需要改原先的eureka-server项目。
要注意:因为eureka-server的端口号配置过,为10086,此时变成为eureka-server2的端口,即10086。
所以eureka-server的端口需要改为10087。
总结理解:
eureka-server:
端口10087,向eureka-server2注册。
eureka-server2:
端口10086,向eureka-server注册
若是其中一台eureka挂了,要想不影响访问,需要修改user-service、consumer-service的application.yml配置文件。
改为:向两个eureka都注册。多个地址使用“,”隔开。
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
服务提供者在启动时,会检测属性配置中的:eureka.client.register-with-erueka=true参数是否正确。事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息。会把这些信息保存在一个双层Map结构中。
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”,这个我们成为服务的续约(renew)。
心跳续约默认时间是30s,每隔30s进行一次心跳。也可配置。
eureka:
instance:
#每隔30s发送一次心跳
lease-renewal-interval-in-seconds: 30
#最小过期时长,每隔30秒发一次心跳,如果隔了90s还没有发送心跳,就挂了
lease-expiration-duration-in-seconds: 90
当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存到本地,并且每隔30s会重新获取并更新数据。
eureka:
client:
#要不要拉取服务
fetch-registry: true
#拉取周期
registry-fetch-interval-seconds: 3
当服务进行正常关闭操作时,它会出发一个服务下线的Rest请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接收到请求之后,将该服务置为下线状态。
有时服务可能由于内存溢出或网络故障灯原因使得服务不能正常工作,而服务注册中心并未收到“服务下线”的请求,相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每个一段时间(默认为60s)将当前清单中超时(默认90s)没有续约的服务剔除,这个操作被称为失效剔除。
可以通过eureka.server.eviction-interval-time-in-ms参数对其进行修改,单位是毫秒。
配置到eureka中。
eureka:
server:
eviction-interval-timer-in-ms: 300000
当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于85%,在生产环境下,因为网络延迟等原因,心跳失败实例有可能超标,但是此时就把服务删除列表并不妥当,因为服务可能没有宕机,Eureka在这段时间内不会删除任何服务实例,直到网络恢复正常。
生产环境下这很有效,保证了大多数服务依然可用,不过可由可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。
可以通过下面配置关停自我保护。
eureka:
server:
enable-self-preservation: false
修改前:是只取第一个实例:
package cn.itcast.consumer.web;
import cn.itcast.consumer.pojo.User;
import com.netflix.appinfo.InstanceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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.util.List;
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
System.out.println(baseUrl);
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
修改后:应该变成随机、轮询、hash等负载算法
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
在RestTemplate方法上加注解@LoadBalanced
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.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
//启动类
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
//需要调用,所以需要这个对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
将来它会内置拦截器,用来拦截器RestTemplat请求。
package cn.itcast.consumer.web;
import cn.itcast.consumer.pojo.User;
import com.netflix.appinfo.InstanceInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
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.util.List;
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
//最终方案
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
/* 方法一:
@Autowired
private RibbonLoadBalancerClient client;
//修改后
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例,该方法帮我们实现了负载均衡,默认是轮询。
ServiceInstance instance = client.choose("user-service");
...
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
*/
/* 修改前
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
System.out.println(baseUrl);
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}*/
}
http://localhost:8088/consumer/17
访问成功!
负载均衡默认通过轮询规则,若不想使用轮询,可通过以下配置使用其他规则。
在consumer-service的application.yml中配置
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
#随机
格式:
服务名称。ribbon.NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
是一种保护机制。
也是Netflix公司的一款组件。
微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的使用链路。
例如,一次业务请求,需要调用A、B、C、D四个服务,这四个服务可能又调用其他服务。此时,若某个服务发生异常,请求阻塞,用户不会得到想用。而tomcat这个线程不会释放,于是越来越多的用户请求被阻塞,会慢慢占满tomcat可用连接,从而导致其他所有服务都不可用,形成雪崩效应。
Hystrix解决雪崩问题的手段有两个:
每个服务有独立的线程池,例如,为一个服务设置5个线程,当用户请求该服务,出现阻塞时,就只会阻塞他自己的这5个线程,不影响其他。即只占满自己的线程池,这就是线程隔离。
当访问的这个服务线程池占满时,也不会出现无线阻塞,会设置一个时间,在时间内没有得到相应,会返回一个响应到浏览器,该请求会释放,释放线程。这就是服务降级。
总结:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程访问服务,如果线程池已满,或者请求超时,则会进行降级处理。
服务降级虽然会导致请求失败,但不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其他服务没有响应。
触发服务降级的情况:
在消费方配置,在consumer-service中添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在启动类上添加注解
@EnableCircuitBreaker
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
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;
//启动类
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
//需要调用,所以需要这个对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
标准的服务上都要添加
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication这三个注解,因此springcloud提供给我们一个新的注解@SpringCloudApplication可以替代这三个注解。
package cn.itcast;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
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;
//启动类
//@EnableCircuitBreaker
//@EnableDiscoveryClient
//@SpringBootApplication
@SpringCloudApplication
public class ConsumerApplication {
//需要调用,所以需要这个对象
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
package cn.itcast.consumer.web;
import cn.itcast.consumer.pojo.User;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
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.util.List;
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
//集群+失败处理=最终方案
//失败容错的指令,即成功时使用该方法,失败时调用注解中参数指的方法。要求失败和成功时的两个方法返回值和参数完全一样
@HystrixCommand(fallbackMethod = "queryByIdFallback" )
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(@PathVariable("id") Long id){
return "不好意思,服务器太拥挤了!";
}
/*//集群最终方案
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}*/
/* 方法一:
@Autowired
private RibbonLoadBalancerClient client;
//修改后
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例,该方法帮我们实现了负载均衡,默认是轮询。
ServiceInstance instance = client.choose("user-service");
...
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
*/
/* 修改前
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
System.out.println(baseUrl);
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}*/
}
修改UserService模拟熔断环境。
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id){
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userMapper.selectByPrimaryKey(id);
}
}
由于上述失败是调用方法是为其中一个方法写的,但是以后有许多方法,总不能为每个方法都要写一个失败时调用方法,此时就要优化代码。
package cn.itcast.consumer.web;
import cn.itcast.consumer.pojo.User;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
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.util.List;
@RestController
@RequestMapping("consumer")
//该类中所有方法默认失败时调用方法
@DefaultProperties(defaultFallback = "@DefaultProperties(defaultFallback = queryByIdFallback)\n")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
//集群+失败处理=最终方案
//启用降级和线程隔离,一旦失败,会去找@DefaultProperties中配置的方法
@HystrixCommand
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(){
return "不好意思,服务器太拥挤了!";
}
/* //方法一:
//集群+失败处理
//失败容错的指令,即成功时使用该方法,失败时调用注解中参数指的方法。要求失败和成功时的两个方法返回值和参数完全一样
@HystrixCommand(fallbackMethod = "queryByIdFallback" )
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}*/
/*//最终方案
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}*/
/* 方法一:
@Autowired
private RibbonLoadBalancerClient client;
//修改后
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例,该方法帮我们实现了负载均衡,默认是轮询。
ServiceInstance instance = client.choose("user-service");
...
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
*/
/* 修改前
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
System.out.println(baseUrl);
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}*/
}
现在是把所有超时统一定义,超时时长1s。但是不同业务其超时时长应该不一样。对于速度慢的,应该单独为其设置超时时长。
单独配:
该注解方法用来单独配置在方法上。
@HystrixCommand(commandProperties = {
@HystrixProperty(name = “execution.isolation.thread.timeoutInMilliseconds”,value = “3000”)
package cn.itcast.consumer.web;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
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;
import java.util.List;
@RestController
@RequestMapping("consumer")
//该类中所有方法默认失败时调用方法
@DefaultProperties(defaultFallback = "queryByIdFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
//集群+失败处理=最终方案
//启用降级和线程隔离,一旦失败,会去找@DefaultProperties中配置的方法
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(){
return "不好意思,服务器太拥挤了!";
}
/* //方法一:
//集群+失败处理
//失败容错的指令,即成功时使用该方法,失败时调用注解中参数指的方法。要求失败和成功时的两个方法返回值和参数完全一样
@HystrixCommand(fallbackMethod = "queryByIdFallback" )
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}*/
/*//最终方案
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//user-service是服务id
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}*/
/* 方法一:
@Autowired
private RibbonLoadBalancerClient client;
//修改后
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例,该方法帮我们实现了负载均衡,默认是轮询。
ServiceInstance instance = client.choose("user-service");
...
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
*/
/* 修改前
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id){
//根据服务id获取实例
List instances = discoveryClient.getInstances("user-service");
// 因为只有一个UserService,因此我们直接get(0)获取
ServiceInstance instance = instances.get(0);
//从实例中取出ip和端口
String url = instance.getHost();
int port = instance.getPort();
String baseUrl = "http://"+instance.getHost() + ":" + instance.getPort()+"/user/"+id;
System.out.println(baseUrl);
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}*/
}
之前设置的是2s的睡眠时间,但在这里设置的是3s超时时长,睡眠时间没有超过2s,所以可以访问成功。
整理配置:
配置在application.yml文件上。
hystrix:
command:
#配置全局变量
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
#配置单个方法或服务的变量
# user-service:或者queryByIdFallback
# execution:
# isolation:
# thread:
# timeoutInMilliseconds: 3000
熔断其英文单词:Circuit Breaker
像家里的点了熔断器,如果点了发生短路能立刻熔断电路,避免发生灾难。在分布式系统应用这一模块之后,服务调用方可以自己进行判断某些服务反映慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
不同于电路熔断只能断不能自动重连,Hystrix可以实现弹性容错,当情况好转之后,可以自动重连。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功回到正常状态,否则连续断开。
熔断器的三个状态:
我们通过手动控制,不需要通过休眠时间。若访问id为偶数,则请求失败,奇数则成功。
把之前模拟的休眠代码删掉。
package cn.itcast.user.service;
import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id){
/*try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
return userMapper.selectByPrimaryKey(id);
}
}
模拟测试环境,并设置休眠时间为10s,当熔断器关闭后,访问次数设置为10次,超过50%失败则进入休眠时间。休眠时间一过,释放部分连接,若请求成功,则关闭熔断器,否则进入休眠时间。
package cn.itcast.consumer.web;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
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;
import java.util.List;
@RestController
@RequestMapping("consumer")
//该类中所有方法默认失败时调用方法
@DefaultProperties(defaultFallback = "queryByIdFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
//集群+失败处理=最终方案
//启用降级和线程隔离,一旦失败,会去找@DefaultProperties中配置的方法
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000"),
//熔断器关闭后,失败次数
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
//熔断器关闭后,休眠时间
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),
})
@GetMapping("{id}")
public String queryById(@PathVariable("id") Long id){
//模拟熔断环境测试,id为偶数即请求失败,为奇数就成功
if(id%2 == 0){
throw new RuntimeException("");
}
//user-service是服务id
String url = "http://user-service/user/"+id;
//返回的对象转成json字符串
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(){
return "不好意思,服务器太拥挤了!";
}
生产环境下,这些参数不需要调,按照默认的就可以了。
总结:该篇讲了服务间的访问和高可用。