服务调用方式
常见的远程调用方式有RPC和HTTP
RPC:Remote Produce Call远程过程调用,类似的还有RMI。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表。
HTTP:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
RestTemplate的使用
跨服务调用:restTemplate的getForObject(地址,结果)
@Autowired
private RestTemplate restTemplate;
@Test
public void httpGet(){
User user = restTemplate.getForObject("http://localhost:80/user/2", User.class);
System.out.println("user = " + user);
}
初始SpringCloud
dependencyManagement和dependencies区别:dependencies:自动引入声明在dependencies里的所有依赖,并默认被所有的子项目继承。如果项目中不写依赖项,则会从父项目继承(属性全部继承)声明在父项目dependencies里的依赖项。dependencyManagement里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要的依赖。
可以给大家看下我搭好的微服务调用场景
user-service对外提供了查询用户的接口;consumer-demo通过RestTemplate访问http://localhost:8081/user/{id}接口,查询用户数据。
哈哈哈哈,yang嗯嗯迈出了万里长征第一步。
但是这个远程服务调用案例存在问题:在consumer中,把url地址硬编码到了代码中,不方便后期维护;consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效;consumer不清楚user-service的状态,服务宕机也不知道;user-service只有一台服务,不具备高可用性;即便user-service形成集群,consumer还需自己实现负载均衡。
注册中心原理图如下
心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态。
(1)引入依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
(2)在启动类上加@EnableEurekaServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class);
}
}
(3)改端口
server:
port: 10086
设置应用名;将自己注册到自己
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
OK,此时eureka-server启动起来了。
user-service服务要注册到eureka-server,改造user-service
org.springframework.cloud
spring-cloud-netflix-eureka-client
2.0.1.RELEASE
添加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("enen.user.mapper")
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class);
}
}
要知道注册中心的位置及给应用命名
server:
port: 8081
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/eesy
username: root
password: 12345678
mybatis:
type-aliases-package: enen.user.pojo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
ok ,接下来改造服务的调用方,同样引依赖,加注解,加配置。
server:
port: 8088
spring:
application:
name: consumer-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
接下来看下高可用的Eureka
server:
port: 10087
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
user-service向注册中心写入时
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
Eureka客户端:
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册:
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上就是true。如果值为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中。
服务续约:
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还或者”。这个称为服务的续约(renew):有两个重要的参数可以修改服务续约的行为:
eureka:
instance:
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds服务续约的间隔,默认30秒。lease-expiration-duration-in-seconds服务失效时间,默认90秒。也就是说,默认情况下每隔30秒服务就会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
获取服务列表:
当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从EurekaServer服务的列表只读备份,然后缓存在本地,并且每隔30秒会重新获取更新数据。
失效剔除:有时我们的服务可能由于内存溢出或网络故障等原因使服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时服务,默认每隔一段时间(默认60秒)将当前清单中超时(默认90秒)没有续约的服务剔除。可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒。
负载均衡Ribbon:
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
// private DiscoveryClient discoveryClient;
private RibbonLoadBalancerClient client;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
//根据服务id获取实例
// List instances = discoveryClient.getInstances("user-service");
//从实例中取出ip和端口
// ServiceInstance ins = instances.get(0);
//使用负载均衡(默认轮询)
ServiceInstance ins = client.choose("user-service");
String url = "http://"+ins.getHost()+":"+ins.getPort()+"/user/" + id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
第二种方式:
在启动类上加注解@LoadBalanced
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}
}
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
// private DiscoveryClient discoveryClient;
// private RibbonLoadBalancerClient client;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
//根据服务id获取实例
// List instances = discoveryClient.getInstances("user-service");
//从实例中取出ip和端口
// ServiceInstance ins = instances.get(0);
//使用负载均衡(默认轮询)
// ServiceInstance ins = client.choose("user-service");
// String url = "http://"+ins.getHost()+":"+ins.getPort()+"/user/" + id;
String url = "http://user-service/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
采用随机而不是轮询
user-service:
ribbon:
NFLoadBalancerRulerClassName: com.netflix.loadbalancer.RandomRule
Hystrix:
解决雪崩问题的手段有两个:线程隔离和服务熔断。
线程隔离,服务降级
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者超时,则会进行降级处理。
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
触发Hystrix服务降级的情况:线程池已满;请求超时。
实践服务的消费方进行降级处理:
(1)引依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
(2)开启熔断,加注解@EnableCircuitBreaker
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args){
SpringApplication.run(ConsumerApplication.class, args);
}
}
@SpringCloudApplication=@EnableCircuitBreaker+@EnableDiscoveryClient+@SpringBootApplication
(3)编写降级逻辑
@GetMapping("{id}")
//成功和失败的方法返回值和参数列表必须一样
@HystrixCommand(fallbackMethod = "queryByIdFallback")
public String queryById(@PathVariable("id") int id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(@PathVariable("id") int id){
return "不好意思,服务器太拥挤了!";
}
通用的fallback
@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
//成功和失败的方法返回值和参数列表必须一样
//@HystrixCommand(fallbackMethod = "queryByIdFallback")
@HystrixCommand
public String queryById(@PathVariable("id") int id){
String url = "http://user-service/user/"+id;
String user = restTemplate.getForObject(url, String.class);
return user;
}
public String queryByIdFallback(int id){
return "不好意思,服务器太拥挤了!";
}
public String defaultFallback(){
return "不好意思,服务器太拥挤了!";
}
}
自定义超时时长配置
@HystrixCommand(commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "2000")
})
feign
feign可以把Rest请求进行隐藏,伪装成类似SpringMVC的controller一样,你不用再自己拼接url,拼接参数等操作,一切都交给feign去做。
引依赖,加注解,写接口
org.springframework.cloud
spring-cloud-starter-openfeign
在启动类上,添加注解,开启feign功能
@EnableFeignClient
@FeignClient("user-service")
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") int id);
}
在远程调用时,直接注入UserClient
@Autowird
private UserClient userClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") int id){
return userClient.queryById(id);
}
feign开启熔断:
在application.yml中开启hystrix
feign:
hystrix:
enabled: true
feign中的Fallback配置不像Ribbon中那样简单了。
zuul
不管是来自客户端的请求,还是服务内部调用,一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
快速入门:
引依赖
org.springframework.cloud
spring-cloud-starter-netflix-zuul
加注解
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
配置
server:
port: 10010
zuul:
routes:
hehe:
path: /user-service/**
url: http://127.0.0.1:8081
访问http://127.0.0.1:10010/user-service/user/1
ok了。
这里地址是固定的不好,需要从eureka中拉取服务。
引依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
最终配置为:
server:
port: 10010
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
routes:
hehe:
path: /user-service/**
serviceId: user-service
http://127.0.0.1:10010/user-service
成功映射到/user-service/**,然后转发至serviceId: user-service这个服务,从eureka中去查找到8081,然后访问8081。
OK,以上就是面向服务的路由。
简化配置方案:
服务ID:服务路径
zuul:
routes:
user-service: /user-service/**
默认情况下,一切服务的映射路径就是服务名本身。例如服务名为:user-service,则默认的映射路径就是:/user-service/**。也就是说,刚才的映射规则不用配置也是ok的。
如果想禁用某个路由规则,可以:
server:
port: 10010
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
spring:
application:
name: gateway
zuul:
routes:
user-service: /user/**
ignored-services:
-consumer-service
过滤器
zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的。
filterType()//过滤器类型;filterOrder()//过滤器顺序(数字越小优先级越高);shouldFilter()//要不要过滤;run()//过滤逻辑
过滤器执行生命周期
正常流程:请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会达到post过滤器,而后返回响应。
异常流程:整个过程中,pre或routing过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给post过滤器,最后返回给用户;如果是error过滤器自己出现异常,最终也会进入post过滤器,而后返回;如果是post过滤器出现异常,会跳转到error过滤器,但是与pre和touting不同的是请求不会再到达post过滤器了。
自定义一个过滤器,模拟登陆校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行。
@Component//自动加入到spring中
public class LoginFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER-1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
//获取请求上下文
RequestContext ctx = RequestContext.getCurrentContext();
// 获取request
HttpServletRequest request = ctx.getRequest();
//获取请求参数access-token
String token = request.getParameter("access-token");
//判断是否存在
if(StringUtils.isBlank(token)){
//不存在,未登录,则拦截
ctx.setSendZuulResponse(false);
//返回403
ctx.setResponseStatusCode(HttpStatus.SC_FORBIDDEN);
}
return null;
}
}
负载均衡和熔断:
zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制,但是所有的超时策略都是走的默认值,比如熔断超时时间只有1s,很容易就触发了。因此建议手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
ribbon:
ConnectionTimeout: 500
ReadTimeOut: 2000
ribbon的超时时长,真实值是(read+connect)*2,必须小于hystrix时长。
zuul的高可用:
启动多个Zuul服务,自动注册到Eureka,形成集群。如果是服务内部访问,你访问Zuul,自动负载均衡,没问题。但是,Zuul更多是外部访问,PC端、移动端等。他们无法通过Eureka进行负载均衡,那么该怎么办?此时,使用其他的服务网关,来对Zuul进行代理,比如Nginx。