Spring Cloud的Feign消费和Hystrix熔断
现如今,由于互联网的流行,很多特产都可以在网上订购,你可以在堆满积雪的冬北订购海南的椰子,海南的椰子就会采用很快的物流方式调送到堆满积地的东北,就相当于在本地实现了买椰子的举动,这种远程调用的方法称为Feign。如下图所示。
一、Feign的解释
Feign 主要是帮助我们方便进行rest api服务间的调用,其大体实现思路就我们通过标记注解在一个接口类上(注解上将包含要调用的接口信息),之后在调用时根据注解信息组装好请求信息,通过服务器获取生成真实的服务地址,最后将请求发送出去;之后将接收到的结果反序列化为相关的Java对象供我们直接使用。
Feign在实际应用中通过在启动类上标记 @EnableFeignClients 注解来开启feign的功能,服务启动后会扫描 @FeignClient 注解标记的接口,然后根据扫描的注解信息为每个接口类生成feign客户端请求,同时解析接口方法中的Spring MVC的相关注解,通过专门的注解解析器识别这些注解信息,以便后面可以正确的组装请求参数,使用Eureka 获取到请求服务的真实地址等信息,最后使用 http 相关组件进行执行调用。其大致流程图如下:
在Feign的通信过程中,可能会出现服务器荡机的状况,Hystrix起到了一定的熔断保护措施。
二、Hystrix的解释
Hystrix是一个用于处理分布式系统的延迟和容错开源库,在分布式系统中,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能保证在一个依赖出现问题时,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
hystrix提供了五种理论技术,如下:
1、降级:当调用出现异常或超时等无法返回正常数据时,返回一个合理的结果或实现fallback方法,针对客户端而言。
2、熔断:当失败率达到阈值自动触发降级,这里的熔断就是具有特定条件的降级,当出现熔断时在设定的时间内不在请求。熔断有自动恢复机制,举个例子说,当熔断器启动后,每隔10秒,尝试将新的请求发送给service,如果服务可正常执行并返回结果,则关闭熔断器,恢复服务。如果仍调用失败,则继续返回fallback,熔断器持续开启。
3、请求缓存:服务A调用服务B,如果在A中添加请求缓存,第一次请求后走缓存,不在访问微服务B,即使出现大量请求,不会对B产生高负荷。请求缓存可以使用spring cache实现。
4、请求合并:当服务A调用服务B时,设定在5毫秒内所有请求合并到一起,对于服务B的负荷就会减少。使用@HystrixCollapser。方法返回值必须为Future
5、隔离:隔离分为线程池隔离合信号量隔离。通过判断线程池或信号量是否满,超过容量的请求直接降级,从而达到限流。
Hystrix可以理解成电路中的保险丝,线路不通还有那么一层保护,如下图所示。
三、产品的eureka服务器的启动
1、点击File--->New---->Project.....,如下图。
2、在弹出的地话框中,左边点击Spring Initializr,表示Spring的初始程序,右边在默认的地方可能会初始化失败,需要选择自定义的地址:http://start.aliyun.com。如右图所示。
3、在接下来的弹出框中输入spring cloud项目名称的Group和artifactId。如下图所示。
4、点击Next进入下一步,左边先点击Web,右边点击Spring web可以建立Web应用程序。如下图。
5、继续在这个对话框中,左边点击Spring Cloud Discovery,右边点击Eureka Server,如下图所示。
6、然后点击Next进入到“下一步”,会出现对话框。如下图所示。
7、在出现的对话框中,点击Finish后完成项目的构建向导。
项目框架建立后,修改主程序中的主类,如下图所示。
8、在打开的主程序中,加入注解@EnableEurekaServer。如下图所示。
最终,eurekaserver的程序代码如下。
package com.myfirsteurekabalance.myfirsteurekabalance;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class MyfirsteurekabalanceApplication {
public static void main(String[] args) {
SpringApplication.run(MyfirsteurekabalanceApplication.class, args);
}
}
9、下面需要编辑resources下面的application.yml文件,在原来的resouces目录下只有application.properties,需要把application.properties改成application.yml。然后编辑该文件。
10、application.yml的文件内容如下。
# 应用名称
spring:
application:
name: myfirsteurekabalance
# 应用服务 WEB 访问端口
server:
port: 8819
eureka:
client:
register-with-eureka: true
fetch-registry: false
service-url:
defaultZone: http://localhost:8819/eureka
instance:
hostname: myprovider1
其文件内容的解释如下 。
四、产品的Feign客户端的搭建
1、同样点击File--->New----->Project....,如下图所示。
2、在打开的对话框中,左边继续点击Spring Initializr,表示Spring的初始程序,右边在默认的地方可能会初始化失败,需要选择自定义的地址:http://start.aliyun.com。如下图所示。
3、点击Next,然后在出现的对话框中输入项目名称。如下图所示。
4、在接下来出现的对话框中,左边选择Web,右边选择Spring Web。如下图所示。
5、继续在这个对话框中,左边选择Spring Cloud Discovery,右边选择Eureka Discovery Client。如下图所示。
6、然后点击左侧的Spring Cloud Routing,然后点右侧的OpenFeign。再点击Next。
7、然后点击下一步即Next,在出现的对话框中直接点击Finish完成项目建立的向导。如下图所示。
项目完成后,接下来设置pom.xml中需要用到的依赖包,具体依赖内容如下。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
这里面后加入的依赖是spring-cloud-starter-zipkin、lombok、spring-cloud-starter-netflix-hystrix。具体依赖的内容如下。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
<version>2.2.8.RELEASE</version>
</dependency>
8、这里是需要Feign的fallback,Fallback可以帮助我们在使用Feign去调用另外一个服务时,如果出现了问题,走服务降级,返回一个错误数据,避免功能因为一个服务出现问题,全部失效。需要首先定义feign包,然后产生feign的java包。这里feign的包中包括两个Feign消费端,一个是Product的Feign消费端,一个是Order的Feign消费端。结构如下。
对于ProductFeign需要使用FegnClient指定消费客户端,同时指定消费在eureka注册中心的服务器名称,并且指明如果服务出现问题的服务降级错误处理fallback。
注意,这里的feign是一个接口,代码如下。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.stereotype.Component;
@FeignClient(name="myprovider5",fallback= ProductFallBackMethod.class)
public interface ProductFeign {
@GetMapping("/product/lists")
public String lists();
}
对于OrderFeign的代码也是做这样的处理,代码如下。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.stereotype.Component;
@FeignClient(name="myprovider4",fallback= OrderFallBackMethod.class)
public interface OrderFeign {
@GetMapping("/order/lists")
public String lists();
}
9、每个feign消费端都指定了一个fallback类方法,所以需要分别定义ProductFallback和OrderFallback的类方法,可以把这两个类放在fallback包下面.结构如下图。
在ProductFallbackMethod方法中实现定义的Feign接口,并且重写ProductFeign接口中的lists方法.特别注意的是这个类必须使用@Component注解来说明,不然spring cloud找不到这个fallback方法。代码如下。
package com.example.mymicroservice1.fallback;
import com.example.mymicroservice1.feigns.ProductFeign;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ProductFallBackMethod implements ProductFeign {
@Override
public String lists() {
log.info("ProductFacllbackMethod调用异常");
return "ProductFacllbackMethod调用异常";
}
}
代码中的log只是输出日志文件, return是返回到网页中的显示数据,这里的log使用lombok的Slf4j注解来实现,实现后就会有log的声明。
同理实现OrderFallBackMethod方法,代码如下。
import com.example.mymicroservice1.feigns.OrderFeign;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class OrderFallBackMethod implements OrderFeign {
@Override
public String lists() {
log.info("OrderFallBackMethod调用异常");
return "OrderFallBackMethod调用异常";
}
}
实现了这个fallback类后,对这两个Feign的调用可以通过service来实现,首先可以建立services包,然后建立一个微服务接口,注意,这里是接口, queryProductList方法中未来会实现ProductFeign, queryOrderList方法中未来会实现OrderFeign。
将两个服务放在一个微服务service的接口Micro1Service代码如下。
package com.example.mymicroservice1.services;
public interface Micro1Service {
String queryProductList();
String queryOrderList();
}
接下来定义第一个微服务Micro1Service接口的实现类。services包的结构图如下。
这里建立一个impl包,在impl包下面建立Micro1Service的接口的实现类Micro1ServiceImpl.代码如下。
package com.example.mymicroservice1.services.impl;
import com.example.mymicroservice1.feigns.OrderFeign;
import com.example.mymicroservice1.feigns.ProductFeign;
import com.example.mymicroservice1.services.Micro1Service;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@Service
@Slf4j
public class Micro1ServiceImpl implements Micro1Service {
@Autowired
private ProductFeign productFeign;
@Autowired
private OrderFeign orderFeign;
@Override
public String queryProductList() {
String lists=productFeign.lists();
log.info("ProductFeign调用返回结果:{}",lists);
return lists;
}
@Override
public String queryOrderList() {
String lists=orderFeign.lists();
log.info("OrderFeign调用结果返回:{}",lists);
return lists;
}
}
注意,这里的Micro1ServiceImpl实现类需要实现接口Micro1Service,同时需要使用注解Service,这样Spring Cloud可以得到这样的Service.在Service代码中将ProductFeign和OrderFeign 注入到Service当中,再实现Micro1Service的两个方法。在两个方法中分别调用ProductFeign和OrderFeign的lists接口。最后返回lists()接口返回的数据。
Service代码设置成功后,设置Controller控制器的代码,在控制器实现ProductList商品列表的显示及OrderList订单列表的显示.同时需要RestController对于页面返回数据的显示,在控制器中第一个微服务Micro1Service也需要注入到Controller控制器中。
代码如下。
package com.example.mymicroservice1.controller;
import com.example.mymicroservice1.services.Micro1Service;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@Slf4j
@RestController
@RequestMapping("/micro1")
public class Micro1Controller {
@Autowired
private Micro1Service micro1service;
@RequestMapping("/productList")
public String productList(){
return micro1service.queryProductList();
}
@RequestMapping("/orderList")
public String orderList(){
return micro1service.queryOrderList();
}
}
因为这个项目中使用Feign消费端,所以需要在主启动类中加入@EnableFeignClients的注解.代码如下。
package com.example.mymicroservice1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class Mymicroservice1Application {
public static void main(String[] args) {
SpringApplication.run(Mymicroservice1Application.class, args);
}
}
最后的yml文件中需要设置客户端feign的启动,新版本的启动hystrix使用circuitbreaker,代码如下。
spring:
application:
name: myprovider2
zipkin:
base-url: http://localhost:9411/
sleuth:
sampler:
probability: 1.0
server:
port: 8830
eureka:
client:
service-url:
defaultZone: http://localhost:8819/eureka
instance:
hostname: myprovider2
feign:
circuitbreaker:
enabled: true
五、产品的Product消费端的产生.
1、同样点击File--->New----->Project....,如下图所示。
2、在打开的对话框中,左边继续点击Spring Initializr,表示Spring的初始程序,右边在默认的地方可能会初始化失败,需要选择自定义的地址:http://start.aliyun.com。如下图所示。
3、点击Next,然后在出现的对话框中输入项目名称。如下图所示。
4、在接下来出现的对话框中,左边选择Web,右边选择Spring Web。如下图所示。
5、继续在这个对话框中,左边选择Spring Cloud Discovery,右边选择Eureka Discovery Client。如下图所示。
6、然后点击下一步即Next,在出现的对话框中直接点击Finish完成项目建立的向导。如下图所示。
7、在Product的微服务消费端中需要定义一个Pojo与数据库中的product表相对应。建立pojo对应的包entity,然后在entity包中产生Pojo的类Product。结构如下图所示。
8、在Product商品类中定义商品需要的信息。
package com.example.mymicroservice2.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Product {
private Integer id;
private String productName;
private Double productPrice;
private String crop;
private Date createTime;
private Date modifyTime;
private String remark;
}
这里通过lombok的Data注解来实现类中的Getter和Setter方法。
根据mvc的原理,下面需要根据Product定义dao模型。首先建立dao包,然后来dao包下建立ProductDao接口文件,注意这里是一个接口,结构如下图。
在接口文件中,定义接口方法lists,其目的显示product表中的所有数据。同时将接口中的注解Mapper来实现与mybatis和配置文件进行对应。代码如下。
package com.example.mymicroservice2.dao;
import com.example.mymicroservice2.entity.Product;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductDao {
public List<Product> lists();
}
这个ProductDao接口需要对应于mybatis的一个ProductDao.xml的mapper文件,mapper文件是存放在resource目录下,结构截图如下。
mapper文件内容中定义resultMap对应于Product类的类中字段,并通过select方法来完成product表的查询语句SQL操作,ProdudctDao.xml具体代码内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mymicroservice2.dao.ProductDao">
<resultMap id="BaseResultMap" type="com.example.mymicroservice2.entity.Product">
<id column="id" jdbcType="INTEGER" property="id"></id>
<result column="product_name" jdbcType="VARCHAR" property="productName"></result>
<result column="product_price" jdbcType="NUMERIC" property="productPrice"></result>
<result column="crop" jdbcType="VARCHAR" property="crop"></result>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"></result>
<result column="modify_time" jdbcType="TIMESTAMP" property="modifyTime"></result>
<result column="remark" jdbcType="VARCHAR" property="remark"></result>
</resultMap>
<select id="lists" resultMap="BaseResultMap">
select * from product
</select>
</mapper>
注意mapper文件中resultMap中的column属性指向数据库中字段名,jdbcType指示数据库中的字段类型,property指示java bean中的类属性。
在<select>标签中指示select查询SQL语句查找product表中的所有数据。
有了dao层和pojo模型后,定义service层的接口,然后通过实现service层的接口实现接口的内部实现代码,结构图如下图。
ProductService接口方法中定义查询所有商品的接口lists,其代码如下。
package com.example.mymicroservice2.services;
import com.example.mymicroservice2.entity.Product;
import java.util.List;
public interface ProductService {
public List<Product> lists();
}
对这样的Service接口定义实现,需要使用implements关键字实现这个接口,重写其中的lists方法,同时将ProductDao作为dao层注到Service层中,ProductDao层的逻辑是通过mybatis的配置文件中SQL语句来实现的.ProductServceImpl的代码如下。
package com.example.mymicroservice2.services.impl;
import com.example.mymicroservice2.dao.ProductDao;
import com.example.mymicroservice2.entity.Product;
import com.example.mymicroservice2.services.ProductService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import java.util.List;
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Resource
private ProductDao productDao;
@Override
public List<Product> lists() {
List<Product> mylist=productDao.lists();
log.info("查询商品的结果:{}",JSON.toJSONString(mylist));
return mylist;
}
}
这里输出调用了alibaba的json模块把输出的字符串以json的形式输出。
Service接口实现后,就可以实现Controller控制器的逻辑.在Controller接口中需要注意前面Feign消费端定义的请求接口是”/product”中的”/lists”路由路径.因此这里的Controller也要实现这样的路径.代码如下。
package com.example.mymicroservice2.services.impl;
import com.example.mymicroservice2.dao.ProductDao;
import com.example.mymicroservice2.entity.Product;
import com.example.mymicroservice2.services.ProductService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import java.util.List;
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
@Resource
private ProductDao productDao;
@Override
public List<Product> lists() {
List<Product> mylist=productDao.lists();
log.info("查询商品的结果:{}",JSON.toJSONString(mylist));
return mylist;
}
}
代码中也需要指明返回前端页面的是json数据的字符串化,使用了alibaba的json模块来进行转换。
代码截图如下。
package com.example.mymicroservice2.controller;
import com.example.mymicroservice2.entity.Product;
import com.example.mymicroservice2.services.ProductService;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.alibaba.fastjson.JSON;
import java.util.List;
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/lists")
public String lists(){
List<Product> lists=productService.lists();
return JSON.toJSONString(lists);
}
}
客户端的yml文件中需要增加指明zipkin的路径,数据库的连接四个参数及mybatis的配置文件路径。具体内容如下。
spring:
application:
name: myprovider5
datasource:
url: jdbc:mysql://localhost:3306/testservice?characterEncoding=utf8
driver-class-name: com.mysql.jdbc.Driver
username: root
password: admin
zipkin:
base-url: http://localhost:9411/
sleuth:
sampler:
probability: 1.0
mybatis:
mapper-locations: classpath*:/mapper/*.xml
server:
port: 8065
eureka:
client:
service-url:
defaultZone: http://localhost:8819/eureka
instance:
hostname: myprovider5
六、环境测试
1、新版本的zipKin,先启动zipkin的server端,通过maven仓库下载exec包的zipkin的jar包。然后通过java来执行。执行方法如下。
java -jar zipkin-server-2.12.9-exec.jar
运行结果如下图所示。
2、启动eurekaserver端注册中心。
3、然后启动mymicro1server端的关于商品和订单消费的feign微服务。
4、再次启动mymicro2server端关于商品请求的与数据库关联的逻辑接口服务。
5、最后启动mymicro3server端关于订单请求的与数据库关联的逻辑接口程序。
zipkin启动后,可以通过网页web页面访问9411端口,即可看到zipkin页面。如下图所示。
7、urekaserver端,mymicro1server端,mymicro2server端三者都启动后,可以在浏览器中访问接口,就会返回数据信息。如下图所示。
注意:这里需要建立mysql数据库,数据库名与配置文件中的一致,并且在当前数据库中建立product表格和order表格,product表格中的字段与mapper文件中配置的resultMapper中的类型匹配需一致,并且向表中插入数据,表的结构和数据如下图.
8、当访问成功后,断开mymicro2server端,也就是断开商品列表的控制器端,最后访问结果如下 ,证明feign消费端的fallback有效。
9、访问成功后,刷新 zipkin的访问页面。可以看到服务名及服务的接口span名。点击查找时,如下图所示。
10、此时,可以点击zipkin页面左上角的Try lens UI按钮,然后可以看到图形化的链路跟踪。如下图。
看到的效果如下图。
七、注意点
1、新版的Spring cloud必须有新版的依赖,通过mvnrepository仓库查询最新版的hystrix的版本是2.2.10.RELEASE。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
2、新版的spring cloud启动hystrix需要使用circuitbreaker,设置其为true,代码如下.
feign:
circuitbreaker:
enabled:true
3、fallback方法中必须加入@Component注解,不然不能将fallback注册到方法中。
4、在主启动类中使用注解@EnableFeignClients.
5、在Feign的接口方法中,使用@FeignClient注解,在注解中使用能够ping通的子服务服务器名称,同时调用熔断的fallback方法,后面是fallback方法的类名。
6、测试时当子服务的服务器没有启动时,会显示fallback方法后面的类名中实现的方法。