案例接着 Hystrix案例
在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:
String baseUrl = "http://user-service/user/";
User user = this.restTemplate.getForObject(baseUrl + id, User.class)
需要4点:提供服务的地址http://user-service/user/ ,参数id,请求方式get,返回类型User
如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?这就是我们接下来要学的Feign的功能了。
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
在服务调用者user-consumer中操作
1)添加依赖
org.springframework.cloud
spring-cloud-starter-openfeign
2)修改启动类
@EnableFeignClients //开启feign功能
@SpringCloudApplication //点进去看看,抵三个注解
public class ConsumerApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplicationStarter.class);
}
}
3)编写UserClient接口
//去eureka拉取服务,使用负载均衡来挑选
//支持hystrix,但是默认是关闭的 写法比较麻烦可以不使用
@FeignClient("user-service")
public interface UserClient {
@GetMapping("user/{id}")
User findUserById(@PathVariable("id") Long id);
}
这与上面的四点刚好相对应
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称4)修改ConsumerController
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private UserClient userClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id) {
return userClient.findUserById(id);
}
}
RestTemplate的注册被我删除了。Feign中已经自动集成了Ribbon负载均衡,因此我们不需要自己定义RestTemplate了
补充: Feign默认也有对Hystix的集成,只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:
feign:
hystrix:
enabled: true #feign 开启熔断 默认关闭
ribbon:
ConnectionTimeout: 500 #Feign的负载均衡时长 抛出异常
ReadTimeout: 2000 #超过两秒没读取到数据 抛出异常
配置会比较麻烦,先要修改UserClient,添加fallBack属性
@FeignClient(value = "user-service",fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("user/{id}")
User findUserById(@PathVariable("id") Long id);
}
添加UserClient 的实现类,实现findUserById方法
@Component
public class UserClientFallback implements UserClient {
@Override
public User findUserById(Long id) {
User user = new User();
user.setUsername("用户未知");
return user;
}
}
既然能自己写Hystrix那就可以不使用Feign的Hystrix了。
主要功能还是简化远程调用,其他的小功能不再赘述。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
Zuul的官网
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
从图中能够看出Zuul具有的功能有,最基本的路由,权限校验,负载均衡,下面将一一来介绍与使用。
创建gateway模块
1)添加依赖 pom.xml内容如下
spring-cloud-demo
com.scu
1.0.0-SNAPSHOT
4.0.0
gateway
org.springframework.cloud
spring-cloud-starter-netflix-zuul
2)创建启动类
@SpringBootApplication
@EnableZuulProxy //启用zuul
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class);
}
}
3)添加配置文件 application.yml
server:
port: 10086 #端口号
spring:
application:
name: gateway #服务名称
zuul:
routes:
xixi: #路径id 随意写
path: /user-service/** #映射路径
url: http://127.0.0.1:8083 #映射路径对应的url地址
启动项目后,当页面访问http://localhost:10086/user-service/user/1,之后路由地址为http://127.0.0.1:8083/user/1,即:将符合path 规则的一切请求,都代理到 url参数指定的地址,本例中,我们将 /user-service/**开头的请求,代理到http://127.0.0.1:8083
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,然后进行动态路由才对!
1)添加eureka依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2)修改配置文件
server:
port: 10086
spring:
application:
name: gateway
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka #zuul也注册到eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
zuul:
routes:
xixi:
path: /user-service/**
serviceId: user-service #服务提供方Id zuul自动负载均衡
注意:似乎启动类上不加@EnableDiscoveryClient也行,但我还是加了,也不影响。
如果觉得上面写的麻烦, 那么可以进行简化,如下
zuul:
routes: #服务id: 映射路径 默认配置也是这样
user-service: /user-service/**
上面这种配置也是默认的配置,所以即使我们不配置,也能访问到。那么我们就来试试访问user-consumer这个微服务
正常访问,实际上我们也没有自己去配置。zuul会根据服务id来帮助我们提供这种配置。但是如果你希望忽略掉部分 服务id: 映射路径 那么该怎么做呢?另外希望加路由前缀怎么做呢?
zuul:
routes: #服务id 映射路径 默认配置也是这样
user-service: /user-service/**
prefix: /api #路由前缀
ignored-services: #忽略掉的服务
- consumer-service
注意:这里加了前缀 /api 由于忽略掉了consumer-service服务,再来访问下
Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行routing
:在路由请求时调用post
:在routing和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。景非常多:
我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。
package com.scu.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class LoginFilter extends ZuulFilter{
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;//pre
}
@Override
public int filterOrder() {
//至少要确保拿到了参数 5 - 1
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;//返回true 使用拦截器
}
@Override
public Object run() throws ZuulException {
//获取上下文
RequestContext ctx = RequestContext.getCurrentContext();
//获取request
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("access-token");
if(StringUtils.isBlank(token)){
//登录校验失败 拦截 默认是true
ctx.setSendZuulResponse(false);
//返回403状态码 禁止访问
ctx.setResponseStatusCode(HttpStatus.FORBIDDEN.value());
}
return null;
}
}
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
ribbon:
ConnectionTimeout: 500
ReadTimeout: 2000 #500*2+2000两个加起来*2不能超过timeoutInMillisecond
# MaxAutoRetriesNextServer: 0 #不重试
重启后页面访问,查看日志,注意到warn的那行
2019-04-09 20:46:38.014 WARN 15792 --- [io-10086-exec-1] o.s.c.n.z.f.r.s.AbstractRibbonCommand : The Hystrix timeout of 3000ms for the command user-service is set lower than the combination of the Ribbon read and connect timeout, 6000ms.
说的是hystrix的超时时间3000ms(针对user-service服务,实际上像上面那样写是针对所有服务的超时时间都是3000ms,如果是只针对user-service服务那么需要在之前补充user-service:后面不变),小于ribbon的读和连接超时时间6000毫秒
所以将hystrix的超时时间设置为6000ms吧,关于这个时间怎么计算的,查看AbstractRibbonCommand类的下方代码
int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout",
IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);
int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout",
IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT);
int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries",
IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer",
IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);