前一阵子一直在忙着做项目,最近有空,把项目中用到的SpringBoot微服务相关架构再梳理一遍。
项目资源:https://github.com/sunroyi/SpringCloud
主要分为以下几步:
(1)搭建SpringBootService,这里是各个微服务的业务逻辑。(这里搭建了2个Service,用来测试熔断)
(2)搭建SpringBootEureka,用来发现服务。
(3)搭建SpringBootConfig,用于动态维护配置文件。
(4)搭建SpringBootConsumer,这个不是必须的,只有当一个更大的业务需要调用其他多个微服务Service时才需要搭建。
可以通过Ribbon+RestTemplate,Feign两种方式进行微服务之间的调用。可以加入Hystrix进行熔断处理。
(5)搭建SpringBootZuul,用于路由控制。可以加入Hystrix用于熔断处理。
(6)使用Jenkins进行代码的自动化部署。
------------------------------------------------------------------------------------------------------------------
在项目搭建之前,需要先了解Spring Cloud中Hystrix、Ribbon以及Feign它们三者之间在处理微服务调用超时从而触发熔断降级的关系。这里我借用一下别人的描述:https://www.jianshu.com/p/31dfb595170c
Ribbon:
它的作用是负载均衡,会帮你在每次请求时选择一台机器,均匀的把请求分发到各个机器上Ribbon的负载均衡默认使用的最经典的Round Robin轮询算法。这是啥?简单来说,就是如果订单服务对库存服务发起10次请求,那就先让你请求第1台机器、然后是第2台机器、第3台机器、第4台机器、第5台机器,接着再来—个循环,第1台机器、第2台机器。。。以此类推。
Feign:
Feign客户端是一个web声明式http远程调用工具,提供了接口和注解方式进行调用(用来调用其他服务)
Feign的一个关键机制就是使用了动态代理。
1. 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
2. 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
3. Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
4. 最后针对这个地址,发起请求、解析响应
Hystrix:
在微服务架构里,一个系统会有很多的服务。以本文的业务场景为例:订单服务在一个业务流程里需要调用三个服务。现在假设订单服务自己最多只有100个线程可以处理请求,然后呢,积分服务不幸的挂了,每次订单服务调用积分服务的时候,都会卡住几秒钟,然后抛出—个超时异常。
咱们一起来分析一下,这样会导致什么问题?
1. 如果系统处于高并发的场景下,大量请求涌过来的时候,订单服务的100个线程都会卡在请求积分服务这块。导致订单服务没有一个线程可以处理请求
2. 然后就会导致别人请求订单服务的时候,发现订单服务也挂了,不响应任何请求了
上面这个,就是微服务架构中恐怖的服务雪崩问题
这么多服务互相调用,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。比如积分服务挂了,会导致订单服务的线程全部卡在请求积分服务这里,没有一个线程可以工作,瞬间导致订单服务也挂了,别人请求订单服务全部会卡住,无法响应。
Hystrix是隔离、熔断以及降级的一个框架。啥意思呢?说白了,Hystrix会搞很多个小小的线程池,比如订单服务请求库存服务是一个线程池,请求仓储服务是一个线程池,请求积分服务是一个线程池。每个线程池里的线程就仅仅用于请求那个服务。
打个比方:现在很不幸,积分服务挂了,会咋样?
当然会导致订单服务里的那个用来调用积分服务的线程都卡死不能工作了啊!但是由于订单服务调用库存服务、仓储服务的这两个线程池都是正常工作的,所以这两个服务不会受到任何影响。
这个时候如果别人请求订单服务,订单服务还是可以正常调用库存服务扣减库存,调用仓储服务通知发货。只不过调用积分服务的时候,每次都会报错。但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。
------------------------------------------------------------------------------------------------------------------
我们知道在Spring Cloud微服务体系下,微服务之间的互相调用可以通过Feign进行声明式调用,在这个服务调用过程中Feign会通过Ribbon从服务注册中心获取目标微服务的服务器地址列表,之后在网络请求的过程中Ribbon就会将请求以负载均衡的方式打到微服务的不同实例上,从而实现Spring Cloud微服务架构中最为关键的功能即服务发现及客户端负载均衡调用。
另一方面微服务在互相调用的过程中,为了防止某个微服务的故障消耗掉整个系统所有微服务的连接资源,所以在实施微服务调用的过程中我们会要求在调用方实施针对被调用微服务的熔断逻辑。而要实现这个逻辑场景在Spring Cloud微服务框架下我们是通过Hystrix这个框架来实现的。
调用方会针对被调用微服务设置调用超时时间,一旦超时就会进入熔断逻辑,而这个故障指标信息也会返回给Hystrix组件,Hystrix组件会根据熔断情况判断被调微服务的故障情况从而打开熔断器,之后所有针对该微服务的请求就会直接进入熔断逻辑,直到被调微服务故障恢复,Hystrix断路器关闭为止。
------------------------------------------------------------------------------------------------------------------
一. SpringBootService的搭建
1. 创建SpringBoot项目
1.1. New Project -> Maven -> Next
1.2. 输入GroupId,ArtifactId -> Next -> Finish
2. SpringBoot项目的相关配置
2.1. 在pom.xml中加入SpringBoot相关的Jar包
<?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.sun</groupId>
<artifactId>springbootService</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.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>Edgware.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<!-- 添加这个依赖之后就可以创建一个web应用程序。starter poms部分可以引入所有需要在实际项目中使用的依赖。
spring-boot-starter-web依赖包含所有的spring-core, spring-web, spring-webmvc,嵌入的Tomcat server和其他web应用相关的库。 -->
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.7</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<!--Spring Cloud Config 客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<!--Spring Boot Actuator,感应服务端变化-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2. 在resources中添加application.properties,这个文件用来进行 项目的相关配置
spring.application.name=springbootService
server.port=6001
3. 项目结构和代码
3.1. 项目目录结构
其中,common目录下是一些共通代码,我这里直接拿过来用了。
sun下面的controller提供对外接口,entity定义实体类。
ComsumerClientApplication在sun目录下,是SpringBoot项目的启动文件。
3.2. 相关Java代码
(1)ComsumerClientApplication.java
package sun;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan
@EnableDiscoveryClient
@RefreshScope //开启配置更新功能
public class ConsumerClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerClientApplication.class, args);
}
}
(2)ServiceController.java
/**
* Copyright © 2012-2014 <a href="https://github.com/thinkgem/jeesite">JeeSite</a> All rights reserved.
*/
package sun.controller;
import common.entity.RestfulResult;
import common.utils.CommUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.entity.ServiceInfo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController // 重要,如果用Controller会404
@RequestMapping(value = "service")
public class ServiceController {
@RequestMapping(value = "hello")
public void login(HttpServletRequest request, HttpServletResponse response,
@RequestBody ServiceInfo serviceInfo) {
RestfulResult restfulResult = new RestfulResult();
try {
restfulResult.setData("Service1:Welcome " + serviceInfo.getName() + "!");
} catch (Exception e) {
e.printStackTrace();
}
CommUtils.printDataJason(response, restfulResult);
}
@RequestMapping(value = "rest")
public String rest(@RequestBody ServiceInfo serviceInfo){
return "Service1:Welcome " + serviceInfo.getName() + " !";
}
}
(3)ServiceInfo.java
package sun.entity;
public class ServiceInfo {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
(4)RestfulResult.java
package common.entity;
public class RestfulResult {
private String result = "Success";
private String message;
private Object data; // 返回数据
private int cntPage; // page数
private long cntData; // 返回数据总数
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public int getCntPage() {
return cntPage;
}
public void setCntPage(int cntPage) {
this.cntPage = cntPage;
}
public long getCntData() {
return cntData;
}
public void setCntData(long cntData) {
this.cntData = cntData;
}
}
(5)CommonUtils.java
package common.utils;
import javax.servlet.http.HttpServletResponse;
public class CommUtils {
// JSON格式化
public static String printDataJason(HttpServletResponse response,
Object item) {
try {
JsonUtils.renderString(response, item);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 随机生成6位随机验证码
*
*/
public static String createRandomVcode(int len) {
// 验证码
String vcode = "";
for (int i = 0; i < len; i++) {
vcode = vcode + (int) (Math.random() * 9);
}
return vcode;
}
}
(6)JsonUtils.java
package common.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JsonUtils {
public JsonUtils() {
}
public static String renderString(HttpServletResponse response, Object object) {
return renderString(response, JsonMapper.toJsonString(object), "application/json");
}
public static String renderString(HttpServletResponse response, String string, String type) {
try {
response.setContentType(type);
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
return null;
} catch (IOException var4) {
return null;
}
}
}
(7)JsonMapper.java
package common.utils;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser.Feature;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.TimeZone;
public class JsonMapper extends ObjectMapper {
private static final long serialVersionUID = 1L;
private static Logger logger = LoggerFactory.getLogger(JsonMapper.class);
private static JsonMapper mapper;
public JsonMapper() {
this(Include.NON_EMPTY);
}
public JsonMapper(Include include) {
if (include != null) {
this.setSerializationInclusion(include);
}
this.enableSimple();
this.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.getSerializerProvider().setNullValueSerializer(new JsonSerializer<Object>() {
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString("");
}
});
this.registerModule((new SimpleModule()).addSerializer(String.class, new JsonSerializer<String>() {
public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString(S