springcloud

服务调用方式

常见的远程调用方式有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)引入依赖

<dependencies>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
       </dependency>
   </dependencies>

(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

<!--eureka客户端-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-eureka-client</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

添加@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:

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
@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<ServiceInstance> 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<ServiceInstance> 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)引依赖

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

(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去做。
引依赖,加注解,写接口

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

在启动类上,添加注解,开启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这个网关,然后再由网关来实现鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。
快速入门:
引依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>

加注解

@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中拉取服务。
引依赖

      <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 一、简介 Eureka 是 Netflix 开发的服务发现框架,本身是一个基于 REST 的服务,主要用于定位运行...
    Gojo99阅读 452评论 0 0
  • 服务治理之Spring Cloud Eureka 1.服务治理。可以说是微服务架构中最为核心和基础的模块,主要用来...
    LUOERD阅读 375评论 0 1
  • 内部类的分类 内部类,顾名思义就是在-个类的内部声明一个类。内部类主要分为: ■静态内部类. ■匿名内部类 ■成员...
    一见你就抱阅读 180评论 0 0
  • 墙角的电线杆 叽叽喳喳三两只的麻雀 嘲笑着窗下垫脚的男孩 空地的小秋千 天真无邪两三对的孩童 随风扬起骄傲的头 街...
    喬霂之阅读 236评论 0 4
  • 我是一个不容易被感动的人,但在读关家良一的《跑步教我的王者风范》时却几次泪湿眼眶,每一次都是在他坚持的时候,在他反...
    勤行乐道阅读 847评论 0 1