精华内容
下载资源
问答
  • 目录 注:主要只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。 一、配置中心的由来及选择 (一)配置中心由来 ...二、Spring Cloud Config概述及基本实现方法介绍 三、Spring Clo...

    目录

    注:主要只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

    一、配置中心的由来及选择

    (一)配置中心由来

    (二)配置中心要求具备的功能

    (三)配置中心基本流转图和支撑体系分析                       ​

    (四)多种配置中心的选择与对比方案

    二、Spring Cloud Config概述及基本实现方法介绍

    三、Spring Cloud Config结合Git实现配置中心方案

    (一)Git版基本工作原理(未加Spring Cloud Bus热刷新)

    (二)Git多种配置信息讲解

    (三)基本的手动刷新和结合Spring Cloud Bus热刷新

    四、Spring Cloud Config结合关系型数据库MYSQL实现配置中心方案

    (一)基本实现原理

    (二)基本要求讲解

    五、Spring Cloud Config结合非关系性数据库MongoDB实现配置中心方案

    (一)基本实现原理

    (二)基本要求讲解

    六、Spring Cloud Config使用技能及功能扩展

    (一)基本实用技能:本地参数覆盖远程参数

    (二)客户端自动刷新实现

    (三)客户端回退功能实现

    (四)客户端安全认证机制JWT实现

    七、Spring Cloud Config实现客户端及服务端高可用方案

    (一)客户端高可用原理及方案

    (二)服务端高可用原理及方案

    八、Spring Cloud Config与Apollo配置使用实现界面化操作

    (一)Apollo基本概述及基本功能介绍

    (二)Apollo总体架构模块分析

    (三)Apollo客户端设计与运行环境介绍

    参考书籍、文献和资料:


    注:主要只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

    一、配置中心的由来及选择

    (一)配置中心由来

    互联网时代下的分布式系统,应用部署在N台服务器上或在云化环境以多实例呈现,如果一个实例一个实例(或一台服务器一台服务器)进行修改配置和重启,一是维护成本极高,二是不现实,配置中心的思想便应运而生。配置中心用作集中管理不同环境和不同集群配置,以及在修改配置后实时动态推送到应用动态更新。

    配置中心管理作为微服务六大实现技术之一,从模型上来分析,包括4大分类、4个核心需求和2个维度分析,在分布式下配置中心在实现上需要满足3大需求:高效获取、实时感知、分布式访问。基本理论在之前博客中已经讲解过,具体见以下博客链接:https://blog.csdn.net/xiaofeng10330111/article/details/85682513

    (二)配置中心要求具备的功能

    配置中心应该剧本具备以下基本功能,具体如图:

    这里只展示了基本的实现功能,从功能特性、技术路线、可用性和易用性方面还有更多的功能要求,具体可以见(四)以及对应的配置中心实现方案对比。

    (三)配置中心基本流转图和支撑体系分析

    具体可如图所示:

            

    (四)多种配置中心的选择与对比方案

    具体对比分类 具体对比项 重要程度 Spring Cloud Config Netflix Archaius Ctrip Apollo DisConf
    功能特性 静态配置管理 基于file 支持 支持
    动态配置管理 支持 支持 支持 支持
    统一管理 无,需要git、数据库等 支持 支持
    多维度管理 无,需要git、数据库等 支持 支持
    变更管理 无,需要git、数据库等
    本地配置缓存 支持 支持
    配置更新策略
    配置锁 支持 不支持 不支持 不支持
    配置校验
    配置生效时间 重启生效,手动刷新 手动刷新生效 实时 实时
    配置更新推送 需要手动触发 需要手动触发 支持 支持
    配置定时拉取 支持 配置更新目前依赖事件驱动,client重启或server推送操作
    用户权限管理 无,需要git、数据库等 支持 支持
    授权、审核、审计 无,需要git、数据库等 界面直接提供发布历史和回滚按钮 操作记录有赖数据库,但无查询接口
    配置版本管理 git 支持 无,需要git、数据库等
    配置合规检测 不支持 不支持 支持(还需完善)  
    实例配置监控 需要结合Spring Admin 不支持 支持 支持,可以查看每个配置在哪台机器上加载
    灰度发布 不支持 不支持 支持 不支持部分更新
    告警通知 不支持 不支持 支持邮件方式告警 支持邮件方式告警
    统计报表 不支持 不支持 不支持 不支持
    依赖关系 不支持 不支持 不支持 不支持
    技术路线 支持Spring Boot 原生支持 支持 与Spring Boot无关
    支持Spring Config 原生支持 支持 与Spring Cloud无关
    客户端支持 java java java、.net java
    业务系统入侵性 入侵性弱 入侵性弱 入侵性弱 入侵性弱,支持注解和xml
    可依赖组件        
    可用性 单点故障(SPOF) 支持HA部署 支持HA部署 支持HA部署 支持HA部署,高可用由ZK提供
    多数据中心部署 支持 支持 支持 支持
    配置获取性能 unkown unkown unkown unkown
    易用性 配置界面 无,需要git、数据库等操作 统一界面 统一界面

    结论:

    • 从整体上来看的话,携程的Apollo性能及各方面相对于其他配置中心而言是最好的,因为其支持pring Boot和Spring Config,所以在微服务架构中建议最好采用Apollo来作为配置中心;
    • Spring Cloud Config相对Apollo性能与支持没有很全面,但是可以采用一定的开源技术及现有技术进行改善加之原生支持pring Boot和Spring Config,在微服务架构中也是强烈建议采用的方案;
    • 其他两种方案在微服务架构中不建议采用,在此不做分析。

    我们主要对Spring Cloud Config进行分析和理解,Apollo做辅助分析,关于Apollo的相关知识有时间在进行学习和分享。

    二、Spring Cloud Config概述及基本实现方法介绍

    Spring Cloud Config是Spring Cloud微服务体系中的配置中心,是一个集中化外部配置的分布式系统,由服务端和客户端组成,其不依赖于注册中心,是一个独立的配置中心,支持多种存储配置信息形式,目前主要有jdbc、value、native、svn、git,其中默认是git。重点讨论功能有如下两个方面:

    • 将程序中配置的各种功能开关、参数配置、服务器地址------>修改后实时生效
    • 灰度发布、分环境、分集群管理配置--------->全面集中化管理

    因为采用native方式必然决定了每次配置完相关文件后一定要重启Spring Cloud Config,所以一般我们不会采用此方案,在实际操作中我们主要的实现方案有以下四种:Spring Cloud Config结合Git实现配置中心方案+Spring Cloud Config结合关系型数据库实现配置中心方案+Spring Cloud Config结合非关系型数据库实现配置中心方案+Spring Cloud Config与Apollo配置结合实现界面化配置中心方案。

    三、Spring Cloud Config结合Git实现配置中心方案

    (一)Git版基本工作原理(未加Spring Cloud Bus热刷新)

    配置客户端启动时会向服务器发起请求,服务端接收到客户端的请求后,根据配置的仓库地址将Git上的文件克隆到本地的一个临时目录中,这个目录是一个Git的本地仓库目录,然后服务端再读取本地文件返回给客户端。这样做的好处是,当Git服务器故障或者网络请求异常时,保证服务端仍然可以正常工作。

    在实际实现上,服务端需要配置好git的uri地址信息以及search-paths信息,并在对应Git中与之相对应,客户端在bootstrap文件中按照其内容和具体文件名进行配置label、uri、name、profile等消息即可,但是在基本实现上如果修改文件后依旧需要重启来解决此问题,所以需要进行手动刷新或结合Spring Cloud Bus进行热刷新。

    (二)Git多种配置信息讲解

    关于Git配置信息而言,其主要有以下几种方式:

    1.本地仓库

    Spring Cloud Config默认使用Git,对Git的配置也最简单,Config Server可用uri、username、password这三个参数就可以读取配置了,通过Git的版本控制可以使Config Server适应特殊的场景。

    测试时我们也可以使用本地仓库的方式,使用file://前缀,那么uri的配置就可以写作

    spring:
      cloud: 
        config: 
          server: 
            git: 
              uri: file://${user.home}/config-repo     #注意:Windows系统需要使用file:///前缀
                                                       #     ${user.home}代表当前用户的家目录

    2.占位符配置URI

    Spring Cloud Config Server支持占位符的使用,支持{application}{profile}{label}这些占位符对Git的uri配置,通过占位符使用应用名称来区分应用对应的仓库然后进行使用。这里需要注意仓库名称和仓库下面的配置文件名称一致才可以,因为配置了spring.cloud.config.name默认占位符匹配的是spring.application.name。

    spring:
      cloud: 
        config: 
          server: 
            git: 
              uri: https://github.com/hellxz/SpringCloudlearn/config-repo/{application}
                   #此时spring.application.name的值会填充到这个uri中,从而达到动态获取不同位置的配置
    

    3.匹配并配置多个仓库

    Spring Cloud Config Server除了使用{应用名}/{环境名}来匹配配置仓库外,还支持通过带有通配符的表达式来匹配。

    当有多个匹配规则的时候,可以用逗号分隔多个{应用名}/{环境名}配置规则。以官方文档例子举例:

    spring:
      cloud:
        config:
          server:
            git:
              uri: https://github.com/spring-cloud-samples/config-repo   #默认的仓库
              #注意:配置多个仓库时,Config Server 在启动时会直接克隆第一个仓库的配置库,其他配置库只有请求时才会clone到本地
              repos:
                simple: https://github.com/simple/config-repo
                special:
                  pattern: special*/dev*,*special*/dev*
                  uri: https://github.com/special/config-repo
                local:
                  pattern: local*
                  uri: file:/home/configsvc/config-repo
                test: 
                  pattern: 
                    - '*/development'
                    - '*/staging'
                  uri: https://github.com/development/config-repo

    如果{应用名}/{环境名}不能匹配到仓库,那么就在默认的uri下去查找配置文件。

    上边的例子中,

    • simple 仓库自动匹配到 simple/*
    • special 仓库的pattern,第一个是应用名以special开头,环境名以dev开头;第二个是应用名包含special,环境名以dev开头;多个匹配到同一uri的pattern用逗号分割
    • local 仓库的的pattern也会自动补全为local*/*
    • test仓库中的 pattern 是以通配符开始的,需要使用单引号

    4.子目录存储

    通过spring.cloud.config.server.git.searchPaths来定位到Git仓库的子目录中,相当于在uri后加上searchPaths的目录。

    searchPaths参数的配置也支持使用{应用名}、{环境名}、{分支名}占位符,比如spring.cloud.config.server.git.searchPaths={应用名},通过这样的配置,我们能让每一个应用匹配到自己的目录中。如下举例:

    spring:
      cloud:
        config:
          server:
            git:
              uri: https://github.com/spring-cloud-samples/config-repo
              searchPaths: '{application}'

    5.访问权限

    使用Git仓库的时候,使用HTTP认证需要使用username和password属性来配置账户,具体如下:

    (还可以使用SSH认证,Config Server本地的.ssh文件或使用私钥等进行配置,如:http://cloud.spring.io/spring-cloud-static/Finchley.RELEASE/single/spring-cloud.html#_git_ssh_configuration_using_properties)

    spring:
      cloud:
        config:
          server:
            git:
              uri: https://github.com/spring-cloud-samples/config-repo
              username: trolley
              password: strongpassword

    (三)基本的手动刷新和结合Spring Cloud Bus热刷新

    为了避免重启项目才能获取最新的配置信息,可以进一步优化做到手动刷新和结合Spring Cloud Bus进行热刷新,其基本要求一般不改动服务端相关配置和代码,但是需要各客户端增加断点访问依赖和安全依赖,这样就可以对外通过访问客户端刷新断点uri来进行刷新操作,手动刷新就是直接访问断点的方式,虽然简单,但是如果每次都要手动刷新的话,面对微服务很多的情况下,就会因为操作人员忘记或者遗漏的情况,从而造成服务出错。在生产实践中,我们往往要求结合Spring Cloud Bus进行热刷新。

    结合Spring Cloud Bus进行热刷新的操作如上图所示,用户更新配置信息时,检查到Git Hook变化,触发Hook配置地址的调用,Config Server接收到请求并发布消息,Bus将消息发送到Config Client,当Config Client接收到消息后重新发送请求加载配置消息。

    四、Spring Cloud Config结合关系型数据库MYSQL实现配置中心方案

    (一)基本实现原理

    Spring Cloud Config是一个独立的配置中心,支持多种存储配置信息形式,其中包括jdbc方式,其基本原理图如下: 

    git有它天然的优势,比如多版本管理、分支管理、提交审核策略等等,但是如果相对其中存储的数据做细粒度的权限控制,就力不从心了。我们可以将持久化从git迁移到MySQL上,这样的好处就是,可以针对配置中心,方便开发出一些对外接口,例如一些用户可配置的动态改更新的参数,同时,由于是数据库方式,当让可以自己在此基础上实现视图化和刷新机制,整体上显得更加优雅。

    (二)基本要求讲解

    具体实现上,需要Config Server端增加服务中心jar包、-配置中心jar包、连接msql数据库相关jar包这三个必须的jar包,在配置文件中需要增加对应的服务名称、连接配置信息、mysql 属性配置、指定注册中心地址等,具体我们要分析的是连接配置信息:

    #连接配置信息
     spring:
      application:
       name: config-server-jdbc
      profiles:
       active: jdbc
      cloud:
       config:
        server:
         default-label: dev
         jdbc:
          sql: SELECT akey , avalue FROM config_server where APPLICATION=? and APROFILE=? and LABEL=?

    连接配置信息中:

    • spring.profiles.active=jdbc ,自动实现JdbcEnvironmentRepository。
    • sql语句自定义,否则会默认为“SELECT KEY, VALUE from PROPERTIES where APPLICATION=? and PROFILE=? and LABEL=?”,具体可以参考 JdbcEnvironmentRepository 实现。非必须,这里由于采用mysql数据源,keyvalue是保留关键词,原生的实现语句会报错,所以需要重写一下这句查询语句(如果存储的表结构设计不同于上面准备的内容,也可以通过这个属性的配置来修改配置的获取逻辑)
    • 数据库建表为config_server,由于key,value和profile是mysql关键字,所以都在最前面加了a。当然表名字段名都可以自定义。
    • {application} 对应客户端的"spring.application.name"属性;
    • {aprofile} 对应客户端的 "spring.profiles.active"属性(逗号分隔的列表); 
    • {label} 对应服务端属性,这个属性能标示一组配置文件的版本.
    • 只要 select出来是两个字段 ,框架会 自动包装到environment的map<key,value> 。

    然后便是启动类等注解配置和数据库相关操作与构建,相关代码具体见后面博客及github。

    注意,JDBC存储的使用思路,具体使用实际上还有很多可以优化的空间,比如:索引的优化、查询语句的优化;如果还需要进一步定制管理,对于表结构的优化也是很有必要的。

    五、Spring Cloud Config结合非关系性数据库MongoDB实现配置中心方案

    (一)基本实现原理

    Spring Cloud Config作为独立的配置中心,支持多种存储配置信息形式,但是没有提供MongoDB的方式,但是目前已经有相关孵化器,其基本原理图如下:

    其基本原理与关系型数据库原理相类似,相关代码具体见后面博客及github。

    (二)基本要求讲解

    具体实现上,需要Config Server端增加服务中心jar包、-配置中心jar包、连接mongo数据库相关jar包这三个必须的jar包,在配置文件中需要增加对应的服务名称、连接配置信息、mongo属性配置、指定注册中心地址等,相关代码具体见后面博客及github。

    六、Spring Cloud Config使用技能及功能扩展

    (一)基本实用技能:本地参数覆盖远程参数

    主要是指使用本地的参数覆盖远程的参数,这在开发的时候经常会用到,主要配合内容如下:

    spring:
      cloud:
        config:
          allowOverride: true
          overrideNone: true
          overrideSystemProperties: false

    这三个属性的意思是:

    • spring.cloud.config.allowOverride:如果想要远程配置优先级高,那么allowOverride设置为false;如果想要本地配置优先级高那么allowOverride设置为true,默认为true;
    • spring.cloud.config.overrideNone:overrideNone为true时本地配置优先级高,包括系统环境变量、本地配置文件等等,默认为false;
    • spring.cloud.config.overrideSystemProperties:只有系统环境变量或者系统属性才能覆盖远程配置文件的配置,本地配置文件中配置优先级低于远程配置,默认为true。

    (二)客户端自动刷新实现

    在一些应用上面,不需要在服务端批量推送的时候,客户端本身需要获取变化参数的情况,这个时候需要使用客户端自动刷新来完成该功能。具体实现上,可以单独在二方包中增加一个用于自动刷新的功能,引入spring-cloud-config-client和spring-cloud-autoconfigure,并且增加自动配置类(增加间隔刷新时间),在该类中主要注入端点类,通过定时任务和刷新时间,进行配置请求刷新,添加配置后,我们将二方包引入到实际的客户端应用中。

    在客户端中需要引入spring-cloud-config-client、spring-boot-starter-security和我们刚刚新做的二方包,并且在对应的配置文件中增加spring.cloud.config.refreshInterval内容,写一个相关的控制器便可以开始测试了,相关代码具体见后面博客及github。

    (三)客户端回退功能实现

    客户端可以匹配回退机制,主要用于以下两种场景,应用回退手段来处理案例:

    • 因为一定原因出现了网络中断的情况;
    • 配置服务因为一定的原因进行维护而关闭。

    当启用回退时,客户端适配器将“缓存”本地文件系统中的计算属性。要启用回退功能,只需要指定存储缓存的位置即可。

    在具体实现上,我们同样需要在二方包增加对应可以实现客户端回退功能的内容,引入spring-cloud-config-client和spring-security-rsa,增加自动配置类(包含回退本地配置文件所在的文件和名称、要回退配置文件的路径、用来创建本地回退文件的方法)和相关配置内容,

    (四)客户端安全认证机制JWT实现

    Spring Cloud Config客户端使用JWT身份验证方法代替标准的基本身份验证,这种方式需要对服务端和客户端都要改造,具体如下:

    • 客户端向服务端授权Rest Controller发送请求并且带上用户名和密码;
    • 服务端返回JET Token;
    • 客户端查询服务端的配置需要在Header中带上token令牌进行认证。

    在具体实现上,需要在二方包中引入jwt相关内容,并将该二方包作为基本包使用,在此二方包中pom中必须需要引入一下这三项内容spring-boot-autoconfigure、spring-boot-autoconfiguration-processor、spring-cloud-starter-config,创建Config配置类(引入标示username和password以及endpoint内容,同时增加初始化init并采用注解@PostConstruct,表明在Servelt构造函数和Init()方法之间执行具体容器加载),并进一步创建实体类LoginRequest(对应请求username和password)和实体类Token(对应生成的token)。

    将此二方包引入到客户端的pom中,同时引入spring-cloud-config-client,并且在对应bootstrap文件中添加用于参与安全认证所需要的username、password、endpoint(是一个http地址,config server的访问授权地址)。增加启动类和控制器进行测试。

    接下来就需要对服务端的代码进行分析和讨论了,首先,需要在pom中引入spring-cloud-config-server、jwt、gson、spring-boot-starter-security,创建JwtAuthenticationRequest类用于传递用户名和密码,创建JwtAuthenticationResponse实体类返回token信息,创建JwtUser用户认证信息实体类,创建JWT的token认证过滤器和JWT工具类(用于生成token和token验证),创建JWT认证端点类(在认证过程中,未能认证通过的直接返回401状态码),接着创建一个认证账号的验证类MemberServiceImpl及将数据封装为json返回客户端的WebAuthenticationDetailsSourceImpl,最后创建Config进行安全和过滤的自动配置类,增加控制器用于测试,相关代码具体见后面博客及github。相关具体类调用时序图如下:

     

    七、Spring Cloud Config实现客户端及服务端高可用方案

    (一)客户端高可用原理及方案

    客户端的高可用方式,从方案角度来看,主要还是用file的形式,和前面客户端的回退方案思路大致一样,客户端高可用主要是解决当服务端不可用的情况下,在客户端仍然可以正常启动。从客户端的角度出发,不是增加配置中心的高可用性,而是降低客户端对配置中心的依赖程度,从而提高整个分布式架构的健壮性。

    具体实现上,仍然需要创建一个二方包,在二方包中引入spring-cloud-config-client,并且配置属性加载类,创建配置类命名为ConfigSupportConfiguration(主要是用于判断远程加载信息是否可用,如果不能用则将读取加载本地配置文件启动),同时在二方包中增加配置文件spring.factories指明org.springframework.cloud.bootstrap.BootstrapConfiguration。

    将该二方包引入对应的客户端的pom中,同时引入spring-cloud-config-client,在bootstrap文件中增加backup开关并指明你备份的本地地址fallbackLocation,增加对应的启动类和控制器用于测试,相关代码具体见后面博客及github。

    (二)服务端高可用原理及方案

    服务端的高可用在生产环境中也一样重要,通过结合Eureka注册中心的方式来搭建Spring Cloud Config Server高可用,通过Ribbon的负载均衡选择一个Config Server进行连接来获取配置信息,具体流程见上图。

    具体实现上,Eureka相关开发与之前是一样的,不用做多余的代码工作;服务端的pom文件中需要引入spring-cloud-config-server和spring-cloud-starter-netflix-eureka-client,其他按照之前的开发即可;客户端的pom文件中需要引入spring-cloud-config-client和spring-cloud-starter-netflix-eureka-client,并且在bootstrap中不在配置spring.cloud.config.uri信息用于指定Server端地址,而是增加了三个新的配置,具体如下:

    • spring.cloud.config.discovery.enabled:开启Config服务发现支持;
    • spring.cloud.config.discovery.serviceId:指定Server端的name,也就是Server端spring.application.name;
    • euraka.client.service-url.defaultZone:只想注册中心的地址。

    接着,按照以往的流程测试高可用就可以了,相关代码具体见后面博客及github。

    八、Spring Cloud Config与Apollo配置使用实现界面化操作

    (一)Apollo基本概述及基本功能介绍

    1.Apollo简介

    Apollo(阿波罗)是携程框架部研发并开源的一款生产级的配置中心产品,它能够集中管理应用在不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

    Apollo目前在国内开发者社区比较热,在Github上有超过5k颗星,在国内众多互联网公司有落地案例,可以说Apollo是目前配置中心产品领域Number1的产品,其成熟度和企业级特性要远远强于Spring Cloud体系中的Spring Cloud Config产品。

    Apollo采用分布式微服务架构,它的架构有一点复杂,Apollo的作者宋顺虽然给出了一个架构图,但是如果没有一定的分布式微服务架构基础的话,则普通的开发人员甚至是架构师也很难一下子理解。

    2.Apollo基本功能介绍

    • 统一管理不同环境、不同集群的配置:Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置;同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等;通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖
    • 配置修改实时生效(热发布): 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序
    • 版本发布管理: 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
    • 灰度发布: 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例
    • 权限管理、发布审核、操作审计:应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。 所有的操作都有审计日志,可以方便的追踪问题
    • 客户端配置信息监控: 可以在界面上方便地看到配置在被哪些实例使用
    • 提供Java和.Net原生客户端:提供了Java和.Net的原生客户端,方便应用集成;支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+);同时提供了Http接口,非Java和.Net应用也可以方便的使用
    • 提供开放平台API:Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。对于有些使用方,它们的配置可能会有比较复杂的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制。
    • 部署简单:配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少。目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来。Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

    (二)Apollo总体架构模块分析

    总体架构模块如下图所示:

    其主要包含了四个核心模块和三个辅助模块:

    1.四个核心模块及其主要功能

    • ConfigService:提供配置获取接口、配置推送接口,服务于Apollo客户端;
    • AdminService:提供配置管理接口、配置修改发布接口,服务于管理界面Portal;
    • Client:为应用获取配置,支持实时更新,通过MetaServer获取ConfigService的服务列表,使用客户端软负载SLB方式调用ConfigService
    • Portal:配置管理界面,通过MetaServer获取AdminService的服务列表,使用客户端软负载SLB方式调用AdminService

    (ConfigService和AdminService都是多实例无状态的部署,需要将自身注册到Eureka中并保持心跳)

    2.三个辅助服务发现模块

    • Eureka:用于服务发现和注册,Config/AdminService注册实例并定期报心跳,和ConfigService在一起部署
    • MetaServer:Portal通过域名访问MetaServer获取AdminService的地址列表,Client通过域名访问MetaServer获取ConfigService的地址列表。相当于一个Eureka Proxy,逻辑角色和ConfigService在一起部署
    • NginxLB:和域名系统配合,协助Portal访问MetaServer获取AdminService地址列表;和域名系统配合,协助Client访问MetaServer获取ConfigService地址列表;和域名系统配合,协助用户访问Portal进行配置管理

    Apollo可以和Spring Cloud Config搭建的微服务进行无缝集成。

    3.为什么选择Eureka作为服务注册中心,而不是使用传统的zk、etcd呢?

    有以下几方面的原因:

    • 它提供了完整的Service Registry和Service Discovery实现。首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。
    • 和Spring Cloud无缝集成:项目本身就使用了Spring Cloud和Spring Boot,同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便。另外,Eureka还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。这一点是我们选择Eureka而不是zk、etcd等的主要原因,为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。
    • Open Source:由于代码是开源的,所以非常便于了解它的实现原理和排查问题。

    (三)Apollo客户端设计与运行环境介绍

    1.Apollo客户端设计

    Apollo客户端的实现原理:

    • 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
    • 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

           这是一个fallback机制,为了防止推送机制失效导致配置不更新

           客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified

           定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

    • 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
    • 客户端会把从服务端获取到的配置在本地文件系统缓存一份

           在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置

    • 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

    2.配置更新推送实现

    Apollo客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。长连接实际上我们是通过Http Long Polling实现的,具体而言:

    • 客户端发起一个Http请求到服务端
    • 服务端会保持住这个连接30秒
    • 如果在30秒内有客户端关心的配置变化,被保持住的客户端请求会立即返回,并告知客户端有配置变化的namespace信息,客户端会据此拉取对应namespace的最新配置
    • 如果在30秒内没有客户端关心的配置变化,那么会返回Http状态码304给客户端
    • 客户端在服务端请求返回后会自动重连

    考虑到会有数万客户端向服务端发起长连,在服务端使用了async servlet(Spring DeferredResult)来服务Http Long Polling请求。

    3.环境要求

    • Java1.7+
    • Guava15.0+ : Apollo客户端默认会引用Guava 19,如果你的项目引用了其它版本,请确保版本号大于等于15.0
    • 注:对于Apollo客户端,如果有需要的话,可以做少量代码修改来降级到Java 1.6

    Apllo这部分的内容目前我还没有在实践中应用,所以后续博客和github中可能暂不停工代码分析。

    参考书籍、文献和资料:

    【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.

    【2】徐进,叶志远,钟尊发,蔡波斯等. 重新定义Spring Cloud. 北京:机械工业出版社. 2018.

    【3】https://blog.csdn.net/xiaofeng10330111/article/details/85682513.

    【4】http://blog.sina.com.cn/s/blog_17ea544680102xive.html.

    【5】https://www.cnblogs.com/hellxz/p/9306507.html.

    【6】https://www.jb51.net/article/147212.htm.

    【7】https://blog.csdn.net/hjbbjh0521/article/details/80363947.

    【8】http://blog.didispace.com/spring-cloud-starter-edgware-3-1/.

    【9】https://blog.csdn.net/john1337/article/details/82413837.

    【10】https://blog.csdn.net/zjh_746140129/article/details/86179522.

    【11】https://blog.csdn.net/ningjiebing/article/details/90638974.

     

    展开全文
  • } Ribbon 的默认ServerListFilter实现1:ZoneAffinityServerListFilter Ribbon默认采取了区域优先的过滤策略,即当Server列表中,过滤出和当前实例所在的区域(zone)一致的server列表 Ribbon 的ServerListFilter...

    目录

    一、Spring Cloud Feign概述与工作原理解读

    (一)服务间调用的几种方式

    (二)Feign 概述

    二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读

    (一)@FeignClient 注解剖析

    (二)Spring Cloud Feign基本功能配置

    (三)Feign请求超时问题

    方法一

    方法二

    方法三

    三、Spring Cloud Ribbon概述与核心工作原理

    (一)Ribbon与负载均衡

    (二)Ribbon核心工作原理

    1.Ribbon 服务配置方式

    2.和Feign的集成模式

    (三)LoadBalancer–负载均衡器的核心

    1.负载均衡器的内部基本实现原理

    2.如何维护Server列表?(新增、更新、删除)

    3.负载均衡器如何维护服务实例的状态?

    4.如何从服务列表中挑选一个合适的服务实例?

    (1)服务实例容器:ServerList的维护

    (2) 服务实例列表过滤器ServerListFilter

    (3)LoadBalancer选择服务实例 的流程

    四、Spring Cloud Ribbon源码解读

    注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

    参考书籍、文献和资料:


    一、Spring Cloud Feign概述与工作原理解读

    (一)服务间调用的几种方式

    使用Spring Cloud开发微服务时,在服务消费者调用服务提供者时,底层通过HTTP Client 的方式访问。但实际上在服务调用时,有主要以下来实现:

    使用JDK原生的URLConnection;

    Apache提供的HTTP Client;

    Netty提供的异步HTTP Client;

    Spring提供的RestTemplate。

    Spring Cloud的Spring Cloud Open Feign相对是最方便与最优雅的,使Feign支持Spring MVC注解的同时并整合了Ribbon。

    (二)Feign 概述

    Feign 是一个声明式的 Web Service 客户端。它的出现使开发 Web Service 客户端变得很简单。使用 Feign 只需要创建一个接口加上对应的注解,比如:@FeignClient 注解。 Feign 有可插拔的注解,包括 Feign 注解和 AX-RS 注解。Feign 也支持编码器和解码器,Spring Cloud Open Feign 对 Feign 进行增强支持 Spring Mvc 注解,可以像 Spring Web 一样使用 HttpMessageConverters 等。

    Feign 是一种声明式、模板化的 HTTP 客户端。在 Spring Cloud 中使用 Feign,可以做到使用 HTTP 请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程方法,更感知不到在访问 HTTP 请求。接下来介绍一下 Feign 的特性,具体如下:

    • 可插拔的注解支持,包括 Feign 注解和AX-RS注解。
    • 支持可插拔的 HTTP 编码器和解码器。
    • 支持 Hystrix 和它的 Fallback。
    • 支持 Ribbon 的负载均衡。
    • 支持 HTTP 请求和响应的压缩。Feign 是一个声明式的 WebService 客户端,它的目的就是让 Web Service 调用更加简单。它整合了 Ribbon 和 Hystrix,从而不需要开发者针对 Feign 对其进行整合。Feign 还提供了 HTTP 请求的模板,通过编写简单的接口和注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。Feign 会完全代理 HTTP 的请求,在使用过程中我们只需要依赖注入 Bean,然后调用对应的方法传递参数即可。

    (三)Feign 工作原理

    1. 在开发微服务应用时,我们会在主程序入口添加 @EnableFeignClients 注解开启对 Feign Client 扫描加载处理。根据 Feign Client 的开发规范,定义接口并加 @FeignClients 注解。
    2. 当程序启动时,会进行包扫描,扫描所有 @FeignClients 的注解的类,并将这些信息注入 Spring IOC 容器中。当定义的 Feign 接口中的方法被调用时,通过JDK的代理的方式,来生成具体的 RequestTemplate。当生成代理时,Feign 会为每个接口方法创建一个 RequetTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,如请求参数名、请求方法等信息都是在这个过程中确定的。
    3. 然后由 RequestTemplate 生成 Request,然后把 Request 交给 Client 去处理,这里指的 Client 可以是 JDK 原生的 URLConnection、Apache 的 Http Client 也可以是 Okhttp。最后 Client 被封装到 LoadBalanceclient 类,这个类结合 Ribbon 负载均衡发起服务之间的调用。

    二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读

    (一)@FeignClient 注解剖析

    FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。

    声明接口之后,在代码中通过@Resource注入之后即可使用。@FeignClient标签的常用属性如下:

    • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
    • url: url一般用于调试,可以手动指定@FeignClient调用的地址
    • decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
    • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
    • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
    • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
    • path: 定义当前FeignClient的统一前缀

    (二)Spring Cloud Feign基本功能配置

    所谓的基本功能配置主要是指可以自定义Feign的配置,相关代码会在后续博客和github中更新。

    1. 日志配置(后续博客和github中更新)
    2. 契约配置(后续博客和github中更新)
    3. Basic认证配置(后续博客和github中更新)
    4. 超时时间配置(后续博客和github中更新)
    5. 客户端组件配置(后续博客和github中更新)
    6. GZIP压缩配置(后续博客和github中更新)
    7. 编码器解码器配置(后续博客和github中更新)
    8. Feign默认Client的替换配置(后续博客和github中更新)
    9. Feign返回图片流处理方式(后续博客和github中更新)
    10. Feign调用传递Token(后续博客和github中更新)
    11. venus-cloud-feign的设计和使用(后续博客和github中更新)

    (三)Feign请求超时问题

    Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了
    解决方案有三种,以feign为例。

    方法一

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
    该配置是让Hystrix的超时时间改为5秒

    方法二

    hystrix.command.default.execution.timeout.enabled: false
    该配置,用于禁用Hystrix的超时时间

    方法三

    feign.hystrix.enabled: false
    该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。

    三、Spring Cloud Ribbon概述与核心工作原理

    Ribbon是2013年1月Netflix公司开发的一个组件,它在云服务体系中起着至关重要的作用,一直是Netflix最活跃的项目。

    Pivotal公司将其整入了Spring Cloud生态,正式命名为Spring Cloud Ribbon作为微服务弹性扩展的组件,与其他组件结合发挥强大作用。(Pivotal中国研发中心于2013.04成立)

    (一)Ribbon与负载均衡

    负载均衡在业界有不少分类:(基本可见https://blog.csdn.net/xiaofeng10330111/article/details/85682513

    最常见的有软负载和硬负载,代表产品为nginx和F5.

    另外一组分类为集中式负载和进程内负载,即服务端负载均衡和客户端负载均衡。这种分类下,nginx和F5都为集中式负载,Ribbon为进程内负载。

    Ribbon是Spring Cloud框架中相当核心的模块,负责着服务负载调用,Ribbon也可以脱离SpringCloud单独使用。
    另外Ribbon是客户端的负载均衡框架,即每个客户端上,独立维护着自身的调用信息统计,相互隔离;也就是说:Ribbon的负载均衡表现在各个机器上变现并不完全一致
    Ribbon 也是整个组件框架中最复杂的一环,控制流程上为了保证服务的高可用性,有很多比较细节的参数控制,在使用的过程中,需要深入理清每个环节的处理机制,这样在问题定位上会高效很多。

    (二)Ribbon核心工作原理                      

    Spring Cloud集成模式下的Ribbon有以下几个特征:

    1.Ribbon 服务配置方式

    每一个服务配置都有一个Spring ApplicationContext上下文,用于加载各自服务的实例。
    比如,当前Spring Cloud 系统内,有如下几个服务:

    服务名称 角色 依赖服务
    order  订单模块 user
    user 用户模块
    mobile-bff 移动端BFF order,user

    mobile-bff服务在实际使用中,会用到order和user模块,那么在mobile-bff服务的Spring上下文中,会为order 和user 分别创建一个子ApplicationContext,用于加载各自服务模块的配置。也就是说,各个客户端的配置相互独立,彼此不收影响

    2.和Feign的集成模式

    在使用Feign作为客户端时,最终请求会转发成 http://<服务名称>/<relative-path-to-service>的格式,通过LoadBalancerFeignClient, 提取出服务标识<服务名称>,然后根据服务名称在上下文中查找对应服务的负载均衡器FeignLoadBalancer,负载均衡器负责根据既有的服务实例的统计信息,挑选出最合适的服务实例。

    (三)LoadBalancer–负载均衡器的核心

    LoadBalancer 的职能主要有三个:

    • 维护Sever列表的数量(新增、更新、删除等)
    • 维护Server列表的状态(状态更新)
    • 当请求Server实例时,能否返回最合适的Server实例

    1.负载均衡器的内部基本实现原理    

     

     

    • Server  

    Server 作为服务实例的表示,会记录服务实例的相关信息,如:服务地址,所属zone,服务名称,实例ID等    

    • ServerList    

    维护着一组Server实例列表,在应用运行的过程中,Ribbon通过ServerList中的服务实例供负载均衡器选择。ServerList维护列表可能在运行的过程中动态改变   

    • ServerStats    

    作为对应Server 的运行情况统计,一般是服务调用过程中的Server平均响应时间,累计请求失败计数,熔断时间控制等。一个ServerStats实例唯一对应一个Server实例    

    • LoadBalancerStats    

    作为 ServerStats实例列表的容器,统一维护    

    • ServerListUpdater    

    负载均衡器通过ServerListUpdater来更新ServerList,比如实现一个定时任务,每隔一段时间获取最新的Server实例列表 

    • Pinger    

    服务状态检验器,负责维护ServerList列表中的服务状态注意:Pinger仅仅负责Server的状态,没有能力决定是否删除    

    • PingerStrategy    

    定义以何种方式还检验服务是否有效,比如是按照顺序的方式还是并行的方式    

    • IPing    

    Ping,检验服务是否可用的方法,常见的是通过HTTP,或者TCP/IP的方式看服务有无认为正常的请求    

    2.如何维护Server列表?(新增、更新、删除)             

    Server列表的维护从实现方法上分为两类:

    • 基于配置的服务列表

    这种方式一般是通过配置文件,静态地配置服务器列表,这种方式相对而言比较简单,但并不是意味着在机器运行的时候就一直不变。netflix 在做Spring cloud 套件时,使用了分布式配置框架netflix archaius ,archaius 框架有一个特点是会动态的监控配置文件的变化,将变化刷新到各个应用上。也就是说,当我们在不关闭服务的情况下,如果修改了基于配置的服务列表时, 服务列表可以直接刷新

    • 结合服务发现组件(如Eureka)的服务注册信息动态维护服务列表

    基于Spring Cloud框架下,服务注册和发现是一个分布式服务集群必不可少的一个组件,它负责维护不同的服务实例(注册、续约、取消注册)。

    3.负载均衡器如何维护服务实例的状态?

    Ribbon负载均衡器将服务实例的状态维护托交给Pinger、 PingerStrategyIPing 来维护,具体交互模式如下所示:              

    4.如何从服务列表中挑选一个合适的服务实例?

    (1)服务实例容器:ServerList的维护

    负载均衡器通过 ServerList来统一维护服务实例,具体模式如上图,基础的接口定义如下:

    /**
     * Interface that defines the methods sed to obtain the List of Servers
     * @author stonse
     *
     * @param <T>
     */
    public interface ServerList<T extends Server> {
        //获取初始化的服务列表
        public List<T> getInitialListOfServers();
        
        /**
         * Return updated list of servers. This is called say every 30 secs
         * (configurable) by the Loadbalancer's Ping cycle
         * 获取更新后的的服务列表
         */
        public List<T> getUpdatedListOfServers();   
    
    }

       在Ribbon的实现中,在ServerList(负责维护服务实例,并使用ServerListFilter过滤器过滤出符合要求的服务实例列表List<Server>)中,维护着Server的实例,并返回最新的List<Server>集合,供LoadBalancer使用   。               

    (2) 服务实例列表过滤器ServerListFilter

    传入一个服务实例列表,过滤出满足过滤条件的服务列表

    public interface ServerListFilter<T extends Server> {
        public List<T> getFilteredListOfServers(List<T> servers);
    }
    • Ribbon 的默认ServerListFilter实现1:ZoneAffinityServerListFilter

    Ribbon默认采取了区域优先的过滤策略,即当Server列表中,过滤出和当前实例所在的区域(zone)一致的server列表

    • Ribbon 的ServerListFilter实现2:ZonePreferenceServerListFilter

    ZonePreferenceServerListFilter 集成自 ZoneAffinityServerListFilter,在此基础上做了拓展,在 返回结果的基础上,再过滤出和本地服务相同区域(zone)的服务列表。

    当指定了当前服务的所在Zone,并且 ZoneAffinityServerListFilter 没有起到过滤效果时,ZonePreferenceServerListFilter会返回当前Zone的Server列表。

    • Ribbon 的ServerListFilter实现3:ServerListSubsetFilter

    这个过滤器作用于当Server数量列表特别庞大时(比如有上百个Server实例),这时,长时间保持Http链接也不太合适,可以适当地保留部分服务,舍弃其中一些服务,这样可使释放没必要的链接。
    此过滤器也是继承自 ZoneAffinityServerListFilter,在此基础上做了拓展,在实际使用中不太常见。

    (3)LoadBalancer选择服务实例 的流程

    LoadBalancer的核心功能是根据负载情况,从服务列表中挑选最合适的服务实例。LoadBalancer内部采用了如下图所示的组件完成:

    LoadBalancer 选择服务实例的流程

    • 通过ServerList获取当前可用的服务实例列表;
    • 通过ServerListFilter将步骤1 得到的服务列表进行一次过滤,返回满足过滤器条件的服务实例列表;
    • 应用Rule规则,结合服务实例的统计信息,返回满足规则的某一个服务实例;

    通过上述的流程可以看到,实际上,在服务实例列表选择的过程中,有两次过滤的机会:第一次是首先通过ServerListFilter过滤器,另外一次是用过IRule 的选择规则进行过滤。

    四、Spring Cloud Ribbon源码解读

    既然是在restTemplate加了@LoadBalanced注解,那就进去这个注解里面看下吧。

    /**
     * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
     * @author Spencer Gibb
     */
    @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Qualifier
    public @interface LoadBalanced {
    }

    从注释中可以知道,这个注解是用来给RestTemplate做标记,以使用负载均衡客户端(LoadBalancerClient)来配置它。所以,我们在生成的RestTemplate的bean上添加这么一个注解,这个bean就会配置LoadBalancerClient。LoadBalancerClient的代码如下:

    /**
     * Represents a client side load balancer
     * @author Spencer Gibb
     */
    public interface LoadBalancerClient {
        /**
         * Choose a ServiceInstance from the LoadBalancer for the specified service
         * @param serviceId the service id to look up the LoadBalancer
         * @return a ServiceInstance that matches the serviceId
         */
        ServiceInstance choose(String serviceId);
    
        /**
         * execute request using a ServiceInstance from the LoadBalancer for the specified
         * service
         * @param serviceId the service id to look up the LoadBalancer
         * @param request allows implementations to execute pre and post actions such as
         * incrementing metrics
         * @return the result of the LoadBalancerRequest callback on the selected
         * ServiceInstance
         */
        <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
    
        /**
         * Create a proper URI with a real host and port for systems to utilize.
         * Some systems use a URI with the logical serivce name as the host,
         * such as http://myservice/path/to/service.  This will replace the
         * service name with the host:port from the ServiceInstance.
         * @param instance
         * @param original a URI with the host as a logical service name
         * @return a reconstructed URI
         */
        URI reconstructURI(ServiceInstance instance, URI original);
    
    }

    LoadBalancerClient是一个接口,里面有三个方法。 
    第一个,ServiceInstance choose(String serviceId);从方法名上就可以看出,是根据传入的serviceId(服务名),从负载均衡器中选择一个服务实例,服务实例通过ServiceInstance类来表示。 
    第二个,execute方法,使用从负载均衡器中选择的服务实例来执行请求内容。 
    第三个,URI reconstructURI(ServiceInstance instance, URI original);方法,是重新构建一个URI的,还记得我们在代码中,通过RestTemplate请求服务时,写的是服务名吧,这个方法就会把这个请求的URI进行转换,返回host+port,通过host+port的形式去请求服务。 
    从工程中搜索LoadBalancerClient接口的实现类,可以找到RibbonLoadBalancerClient类实现了这个接口,并且实现了接口中定义的方法。

    再梳理一下逻辑,我们在RestTemplate上添加了@LoadBalanced注解,RibbonLoadBalancerClient就会配置到这个RestTemplate实例上。

    在LoadBalancerClient接口的同一个包路径下,还会看到另一个LoadBalancerAutoConfiguration类,看名字就感觉这是一个自动配置LoadBalancer的,进去这个类看一下。

    /**
     * Auto configuration for Ribbon (client side load balancing).
     *
     * @author Spencer Gibb
     * @author Dave Syer
     */
    @Configuration
    @ConditionalOnClass(RestTemplate.class)
    @ConditionalOnBean(LoadBalancerClient.class)
    public class LoadBalancerAutoConfiguration {
    
        @LoadBalanced
        @Autowired(required = false)
        private List<RestTemplate> restTemplates = Collections.emptyList();
    
        @Bean
        public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
                final List<RestTemplateCustomizer> customizers) {
            return new SmartInitializingSingleton() {
                @Override
                public void afterSingletonsInstantiated() {
                    for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                        for (RestTemplateCustomizer customizer : customizers) {
                            customizer.customize(restTemplate);
                        }
                    }
                }
            };
        }
    @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
                final LoadBalancerInterceptor loadBalancerInterceptor) {
            return new RestTemplateCustomizer() {
                @Override
                public void customize(RestTemplate restTemplate) {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                }
            };
        }
    
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient) {
            return new LoadBalancerInterceptor(loadBalancerClient);
        }
    
    }

    注释中说明这个类是为Ribbon做自动配置的,类上的@Configuration说明这是一个配置类,在当前项目中存在RestTemplate类、并且存在LoadBalancerClient接口的实现类时,就满足了自动化配置的条件。 
    在LoadBalancerAutoConfiguration类中,创建了一个LoadBalancerInterceptor拦截器,还维护了一个被@LoadBalanced修饰的RestTemplate列表,在初始化的时候,会为每个restTemplate实例添加LoadBalancerInterceptor拦截器。 
    我们自己实现的项目,就定义了RestTemplate的一个对象,并且引入了spring-cloud相关的包,存在RibbonLoadBalancerClient作为LoadBalancerClient的实现类,所以,满足自动化配置的条件。接下来就看下,在restTemplate实例添加的LoadBalancerInterceptor拦截器的逻辑。

    public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    
        private LoadBalancerClient loadBalancer;
    
        public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
            this.loadBalancer = loadBalancer;
        }
    
        @Override
        public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                final ClientHttpRequestExecution execution) throws IOException {
            final URI originalUri = request.getURI();
            String serviceName = originalUri.getHost();
            return this.loadBalancer.execute(serviceName,
                    new LoadBalancerRequest<ClientHttpResponse>() {
    
                        @Override
                        public ClientHttpResponse apply(final ServiceInstance instance)
                                throws Exception {
                            HttpRequest serviceRequest = new ServiceRequestWrapper(request,
                                    instance);
                            return execution.execute(serviceRequest, body);
                        }
    
                    });
        }
        private class ServiceRequestWrapper extends HttpRequestWrapper {
    
            private final ServiceInstance instance;
    
            public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance) {
                super(request);
                this.instance = instance;
            }
    
            @Override
            public URI getURI() {
                URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(
                        this.instance, getRequest().getURI());
                return uri;
            }
    
        }
    
    }

    由于在自动配置类中,对restTemplate实例添加了LoadBalancerInterceptor拦截器,所以,当用restTemplate发送http请求时,就会执行这个拦截器的intercept方法。 
    intercept方法中,会根据request.getURI(),获取请求的uri,再获取host,我们在发送http请求的时候,是用的服务名作为host,所以,这里就会拿到服务名,再调用具体LoadBalancerClient实例的execute方法,发送请求。 
    LoadBalancerClient的实现类为RibbonLoadBalancerClient,最终的负载均衡请求由它来执行,所以,还需要再梳理下RibbonLoadBalancerClient的逻辑。

    先看下RibbonLoadBalancerClient中的execute方法:

    @Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        Server server = getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                serviceId), serverIntrospector(serviceId).getMetadata(server));
    
        RibbonLoadBalancerContext context = this.clientFactory
                .getLoadBalancerContext(serviceId);
        RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
    
        try {
            T returnVal = request.apply(ribbonServer);
            statsRecorder.recordStats(returnVal);
            return returnVal;
        }
        // catch IOException and rethrow so RestTemplate behaves correctly
        catch (IOException ex) {
            statsRecorder.recordStats(ex);
            throw ex;
        }
        catch (Exception ex) {
            statsRecorder.recordStats(ex);
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

    服务名作为serviceId字段传进来,先通过getLoadBalancer获取loadBalancer,再根据loadBalancer获取server,下面是getServer的代码:

    protected Server getServer(ILoadBalancer loadBalancer) {
        if (loadBalancer == null) {
            return null;
        }
        return loadBalancer.chooseServer("default"); // TODO: better handling of key
    }

    如果loadBalancer为空,就直接返回空,否则就调用loadBalancer的chooseServer方法,获取相应的server。 
    看一下ILoadBalancer是一个接口,里面声明了一系列负载均衡实现的方法:

    public interface ILoadBalancer {
        public void addServers(List<Server> newServers);
        public Server chooseServer(Object key);
        public void markServerDown(Server server);
        public List<Server> getReachableServers();
        public List<Server> getAllServers();
    }

    这里面还有一个getServerList方法,不过已经标记为Deprecated,所以就没有列出。 
    这些方法名比较直观,很容易就能猜出是干啥的,addServers是用来添加一个server集合,chooseServer是选择一个server,markServerDown用来标记某个服务下线,getReachableServers获取可用的Server集合,getAllServers是获取所有的server集合。 
    ILoadBalancer有很多实现,那具体是用的哪个类呢,可以通过RibbonClientConfiguration类看到,这个配置类在初始化的时候,返回了ZoneAwareLoadBalancer作为负载均衡器。

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
            IRule rule, IPing ping) {
        ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
                .withClientConfig(config).withRule(rule).withPing(ping)
                .withServerListFilter(serverListFilter).withDynamicServerList(serverList)
                .buildDynamicServerListLoadBalancer();
        return balancer;
    }

    ZoneAwareLoadBalancer从名字中可以看出来,这个负载均衡器和zone是有关系的。下面看下ZoneAwareLoadBalancer中的chooseServer方法:

    @Override
    public Server chooseServer(Object key) {
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (triggeringLoad == null) {
                triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
            }
    
            if (triggeringBlackoutPercentage == null) {
                triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
            }
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Throwable e) {
            logger.error("Unexpected exception when choosing server using zone aware logic", e);
        }
        if (server != null) {
            return server;
        } else {
            logger.debug("Zone avoidance logic is not invoked.");
            return super.chooseServer(key);
        }
    }
    

    这个方法会根据server的zone和可用性来选择具体的实例,返回一个Server对象。

    通过ZoneAwareLoadBalancer选择具体的Server之后,再包装成RibbonServer对象,之前返回的server是该对象中的一个字段,除此之外,还有服务名serviceId,是否需要使用https等信息。最后,通过LoadBalancerRequest的apply方法,向具体的server发请求,从而实现了负载均衡。 
    下面是apply方法的定义:

    public interface LoadBalancerRequest<T> {
        public T apply(ServiceInstance instance) throws Exception;
    }

    在请求时,传入的ribbonServer对象,被当成ServiceInstance类型的对象进行接收。ServiceInstance是一个接口,定义了服务治理系统中,每个实例需要提供的信息,比如serviceId,host,port等。 
    LoadBalancerRequest是一个接口,最终会通过实现类的apply方法去执行,实现类是在LoadBalancerInterceptor中调用RibbonLoadBalancerClient的execute方法时,传进来的一个匿名类,可以通过查看LoadBalancerInterceptor的代码看到。 
    创建LoadBalancerRequest匿名类的时候,就重写了apply方法,apply方法中,还新建了一个ServiceRequestWrapper的内部类,这个类中,就重写了getURI方法,getURI方法会调用loadBalancer的reconstructURI方法来构建uri。

    看到这里,已经可以大体知道Ribbon实现负载均衡的流程了,我们在RestTemplate上添加注解,就会有LoadBalancerClient的对象来配置它,也就是RibbonLoadBalancerClient。同时,LoadBalancerAutoConfiguration会进行配置,创建一个LoadBalancerInterceptor,并且拿到我们声明的所有restTemplate,在这些restTemplate中添加LoadBalancerInterceptor拦截器。 
    当通过restTemplate发送请求时,就会经过这个拦截器,在拦截器中,就会调用RibbonLoadBalancerClient中的方法,获取到根据服务名,通过负载均衡方法获取到服务实例,然后去请求这个实例。 
    上面说的这些,是如何对请求进行负载均衡的,但是还有个问题,我们请求的实例,是从Eureka Server上获取到的,那这个实例列表是如何获取的呢?怎么保证这个实例列表中的实例是可用的呢?

    在RibbonLoadBalancerClient选择实例的时候,是通过ILoadBalancer的实现类根据负载均衡算法选择服务实例的,也就是ZoneAwareLoadBalancer的chooseServer中的逻辑,那就在这里找线索。查看ZoneAwareLoadBalancer的继承关系,可以看到如下图所示。 

    可以看到,最上面是ILoadBalancer接口,AbstractLoadBalancer类继承了这个接口,BaseLoadBalancer继承了AbstractLoadBalancer类,DynamicServerListLoadBalancer继承了BaseLoadBalancer,ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer。

    ILoadBalancer接口的代码已经看过了,现在看下AbstractLoadBalancer的代码:

    public abstract class AbstractLoadBalancer implements ILoadBalancer {
    
        public enum ServerGroup{
            ALL,
            STATUS_UP,
            STATUS_NOT_UP        
        }
    
        /**
         * delegate to {@link #chooseServer(Object)} with parameter null.
         */
        public Server chooseServer() {
            return chooseServer(null);
        }
    
        /**
         * List of servers that this Loadbalancer knows about
         * 
         * @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP}
         */
        public abstract List<Server> getServerList(ServerGroup serverGroup);
    
        /**
         * Obtain LoadBalancer related Statistics
         */
        public abstract LoadBalancerStats getLoadBalancerStats();    
    }

    这是一个抽象类,里面加了一个枚举,增加了两个抽象方法。定义的chooseServer方法。

    下面再看BaseLoadBalancer类,BaseLoadBalancer类就算是负载均衡器的一个基础实现类,在里面可以看到定义了两个list:

    @Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> allServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    @Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> upServerList = Collections
            .synchronizedList(new ArrayList<Server>());

    从名字上看,这就是维护所有服务的实例列表,和维护状态为up的实例列表。 
    而且还可以看到BaseLoadBalancer中实现的ILoadBalancer接口中的方法,比如下面这两个,获取可用的服务列表,就会把upServerList返回,获取所有的服务列表,就会把allServerList返回。

    @Override
    public List<Server> getReachableServers() {
        return Collections.unmodifiableList(upServerList);
    }
    
    @Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }

    接下来,再看DynamicServerListLoadBalancer类。从类头上的注释可以知道,这个类可以动态的获取服务列表,并且利用filter对服务列表进行过滤。

    在DynamicServerListLoadBalancer类中,能看到定义了一个ServerList类型的serverListImpl字段,ServerList是一个接口,里面有两个方法:

    public interface ServerList<T extends Server> {
    
        public List<T> getInitialListOfServers();
    
        /**
         * Return updated list of servers. This is called say every 30 secs
         * (configurable) by the Loadbalancer's Ping cycle
         * 
         */
        public List<T> getUpdatedListOfServers();   
    
    }

    getInitialListOfServers是获取初始化的服务列表。 
    getUpdatedListOfServers是获取更新的服务列表。 
    ServerList有多个实现类,具体用的哪个呢,可以在EurekaRibbonClientConfiguration类中找到,这是Ribbon和Eureka结合的自动配置类,这里面有个方法:

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config) {
        DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
                config);
        DomainExtractingServerList serverList = new DomainExtractingServerList(
                discoveryServerList, config, this.approximateZoneFromHostname);
        return serverList;
    }

    方法中先新建了一个DiscoveryEnabledNIWSServerList类型的对象,又把这个对象作为一个参数,创建了DomainExtractingServerList类型的对象,最终返回的是DomainExtractingServerList的实例对象。 
    查看DomainExtractingServerList中重写的这两个方法,发现还是调用的DiscoveryEnabledNIWSServerList中的方法。然后,进到DiscoveryEnabledNIWSServerList类中,看这两个方法的定义:

    @Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }
    
    @Override
    public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
        return obtainServersViaDiscovery();
    }

    这两个方法,都是调用了obtainServersViaDiscovery这个方法:

    private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
    
        if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList<DiscoveryEnabledServer>();
        }
    
        EurekaClient eurekaClient = eurekaClientProvider.get();
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                // if targetRegion is null, it will be interpreted as the same region of client
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                for (InstanceInfo ii : listOfInstanceInfo) {
                    if (ii.getStatus().equals(InstanceStatus.UP)) {
    
                        if(shouldUseOverridePort){
                            if(logger.isDebugEnabled()){
                                logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                            }
    
                            // copy is necessary since the InstanceInfo builder just uses the original reference,
                            // and we don't want to corrupt the global eureka copy of the object which may be
                            // used by other clients in our system
                            InstanceInfo copy = new InstanceInfo(ii);
    
                            if(isSecure){
                                ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                            }else{
                                ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                            }
                        }
                        DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
                        des.setZone(DiscoveryClient.getZone(ii));
                        serverList.add(des);
                    }
                }
                if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                    break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
                }
            }
        }
        return serverList;
    }

    在这个方法中,就是通过eurekaClient去注册中心获取服务,将状态为up的服务实例封装成DiscoveryEnabledServer对象,然后放入列表返回,这就是获取服务列表的流程。 
    获取服务列表的流程知道了,那是如何触发去获取,如何更新服务列表的呢?再看DynamicServerListLoadBalancer类中的代码,有一段:

    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };

    ServerListUpdater是一个接口,在DynamicServerListLoadBalancer的构造函数中,创建了一个PollingServerListUpdater类的对象,为ServerListUpdater字段赋值。进入PollingServerListUpdater类看一下:

    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };
    
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }

    里面有个start方法,实现了Runnable接口,run方法里调用UpdateAction的doUpdate,之后再启动一个定时任务,执行这个方法。定时任务传入的两个时间参数:initialDelayMs和refreshIntervalMs,任务启动后一秒开始执行,并且每隔三十秒执行一次,用于刷新列表。

    看到这里,就可以大体了解了,构造DynamicServerListLoadBalancer实例的时候,就会启动一个定时任务了,一开始先获取服务列表,之后每隔三十秒获取一次。负载均衡时,就是通过负载均衡算法在实例列表中选择一个,发送请求。

    以上这就是Ribbon源码大体的流程。
     

    注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

    参考书籍、文献和资料:

    【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.

    【2】徐进,叶志远,钟尊发,蔡波斯等. 重新定义Spring Cloud. 北京:机械工业出版社. 2018.

    【3】https://blog.csdn.net/wo18237095579/article/details/83343915.

    【4】https://blog.csdn.net/a15514920226/article/details/78924483.

    【5】https://blog.csdn.net/xiaofeng10330111/article/details/85682513.

    【6】https://blog.csdn.net/luanlouis/article/details/83060310.

    【7】https://blog.csdn.net/chayangdz/article/details/82177917.

    展开全文
  • 数据库分库分表策略的具体实现方案

    万次阅读 多人点赞 2017-01-02 14:10:03
    1、 使用Spring AOP实现MySQL数据库读写分离案例分析 2、MySQL5.6 数据库主从(Master/Slave)同步安装与配置详解 3、MySQL主从复制的常见拓扑、原理分析以及如何提高主从复制的效率总结 4、使用mysqlreplicate...

    相关文章:

    1、 使用Spring AOP实现MySQL数据库读写分离案例分析

    2、MySQL5.6 数据库主从(Master/Slave)同步安装与配置详解

    3、MySQL主从复制的常见拓扑、原理分析以及如何提高主从复制的效率总结

    4、使用mysqlreplicate命令快速搭建 Mysql 主从复制

    5、大型网站应用之海量数据和高并发解决方案总结一二

    一、MySQL扩展具体的实现方式

    随着业务规模的不断扩大,需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量。

    关于数据库的扩展主要包括:业务拆分、主从复制,数据库分库与分表。这篇文章主要讲述数据库分库与分表

    1.1、业务拆分

    大型网站应用之海量数据和高并发解决方案总结一二 一篇文章中也具体讲述了为什么要对业务进行拆分。

    业务起步初始,为了加快应用上线和快速迭代,很多应用都采用集中式的架构。随着业务系统的扩大,系统变得越来越复杂,越来越难以维护,开发效率变得越来越低,并且对资源的消耗也变得越来越大,通过硬件提高系统性能的方式带来的成本也越来越高。

    因此,在选型初期,一个优良的架构设计是后期系统进行扩展的重要保障。

    例如:电商平台,包含了用户、商品、评价、订单等几大模块,最简单的做法就是在一个数据库中分别创建users、shops、comment、order四张表。

    这里写图片描述

    但是,随着业务规模的增大,访问量的增大,我们不得不对业务进行拆分。每一个模块都使用单独的数据库来进行存储,不同的业务访问不同的数据库,将原本对一个数据库的依赖拆分为对4个数据库的依赖,这样的话就变成了4个数据库同时承担压力,系统的吞吐量自然就提高了。

    这里写图片描述

    1.2、主从复制

    1、MySQL5.6 数据库主从(Master/Slave)同步安装与配置详解

    2、MySQL主从复制的常见拓扑、原理分析以及如何提高主从复制的效率总结

    3、使用mysqlreplicate命令快速搭建 Mysql 主从复制

    上述三篇文章中,讲述了如何配置主从数据库,以及如何实现数据库的读写分离,这里不再赘述,有需要的选择性点击查看。

    这里写图片描述

    上图是网上的一张关于MySQL的Master和Slave之间数据同步的过程图。

    主要讲述了MySQL主从复制的原理:数据复制的实际就是Slave从Master获取Binary log文件,然后再本地镜像的执行日志中记录的操作。由于主从复制的过程是异步的,因此Slave和Master之间的数据有可能存在延迟的现象,此时只能保证数据最终的一致性。

    1.3、数据库分库与分表

    我们知道每台机器无论配置多么好它都有自身的物理上限,所以当我们应用已经能触及或远远超出单台机器的某个上限的时候,我们惟有寻找别的机器的帮助或者继续升级的我们的硬件,但常见的方案还是通过添加更多的机器来共同承担压力。

    我们还得考虑当我们的业务逻辑不断增长,我们的机器能不能通过线性增长就能满足需求?因此,使用数据库的分库分表,能够立竿见影的提升系统的性能,关于为什么要使用数据库的分库分表的其他原因这里不再赘述,主要讲具体的实现策略。请看下边章节。

    二、分表实现策略

    关键字:用户ID、表容量

    对于大部分数据库的设计和业务的操作基本都与用户的ID相关,因此使用用户ID是最常用的分库的路由策略。用户的ID可以作为贯穿整个系统用的重要字段。因此,使用用户的ID我们不仅可以方便我们的查询,还可以将数据平均的分配到不同的数据库中。(当然,还可以根据类别等进行分表操作,分表的路由策略还有很多方式)

    接着上述电商平台假设,订单表order存放用户的订单数据,sql脚本如下(只是为了演示,省略部分细节):

    CREATE TABLE `order` (
      `order_id` bigint(32) primary key auto_increment,
      `user_id` bigint(32),
       ...
    ) 
    

    当数据比较大的时候,对数据进行分表操作,首先要确定需要将数据平均分配到多少张表中,也就是:表容量

    这里假设有100张表进行存储,则我们在进行存储数据的时候,首先对用户ID进行取模操作,根据 user_id%100 获取对应的表进行存储查询操作,示意图如下:

    这里写图片描述

    例如,user_id = 101 那么,我们在获取值的时候的操作,可以通过下边的sql语句:

    select * from order_1 where user_id= 101
    

    其中,order_1是根据 101%100 计算所得,表示分表之后的第一章order表。

    注意:

    在实际的开发中,如果你使用MyBatis做持久层的话,MyBatis已经提供了很好得支持数据库分表的功能,例如上述sql用MyBatis实现的话应该是:

    接口定义:

    
    /**
      * 获取用户相关的订单详细信息
      * @param tableNum 具体某一个表的编号
      * @param userId 用户ID
      * @return 订单列表
      */
    public List<Order> getOrder(@Param("tableNum") int tableNum,@Param("userId") int userId);
    

    xml配置映射文件:

    <select id="getOrder" resultMap="BaseResultMap">
        select * from order_${tableNum}
        where user_id = #{userId}
      </select>
    

    其中${tableNum} 含义是直接让参数加入到sql中,这是MyBatis支持的特性。

    注意:

    另外,在实际的开发中,我们的用户ID更多的可能是通过UUID生成的,这样的话,我们可以首先将UUID进行hash获取到整数值,然后在进行取模操作。
    

    三、分库实现策略

    数据库分表能够解决单表数据量很大的时候数据查询的效率问题,但是无法给数据库的并发操作带来效率上的提高,因为分表的实质还是在一个数据库上进行的操作,很容易受数据库IO性能的限制。

    因此,如何将数据库IO性能的问题平均分配出来,很显然将数据进行分库操作可以很好地解决单台数据库的性能问题。

    分库策略与分表策略的实现很相似,最简单的都是可以通过***取模***的方式进行路由。

    还是上例,将用户ID进行取模操作,这样的话获取到具体的某一个数据库,同样关键字有:

    用户ID、库容量

    路由的示意图如下:

    这里写图片描述

    上图中库容量为100。

    同样,如果用户ID为UUID请先hash然后在进行取模。

    四、分库与分表实现策略

    上述的配置中,数据库分表可以解决单表海量数据的查询性能问题,分库可以解决单台数据库的并发访问压力问题。

    有时候,我们需要同时考虑这两个问题,因此,我们既需要对单表进行分表操作,还需要进行分库操作,以便同时扩展系统的并发处理能力和提升单表的查询性能,就是我们使用到的分库分表。

    分库分表的策略相对于前边两种复杂一些,一种常见的路由策略如下:

    1、中间变量 = user_id%(库数量*每个库的表数量);
    2、库序号 = 取整(中间变量/每个库的表数量);
    3、表序号 = 中间变量%每个库的表数量;
    

    例如:数据库有256 个,每一个库中有1024个数据表,用户的user_id=262145,按照上述的路由策略,可得:

    1、中间变量 = 262145%(256*1024)= 1;
    2、库序号 = 取整(1/1024)= 0;
    3、表序号 = 1%1024 = 1;
    

    这样的话,对于user_id=262145,将被路由到第0个数据库的第1个表中。

    示意图如下:

    这里写图片描述

    五、分库分表总结

    关于分库分表策略的选择有很多种,上文中根据用户ID应该是比较简单的一种。其他方式比如使用号段进行分区或者直接使用hash进行路由等。有兴趣的可以自行查找学习。

    关于上文中提到的,如果用户的ID是通过UUID的方式生成的话,我们需要单独的进行一次hash操作,然后在进行取模操作等,其实hash本身就是一种分库分表的策略,使用hash进行路由策略的时候,我们需要知道的是,也就是hash路由策略的优缺点,优点是:数据分布均匀;缺点是:数据迁移的时候麻烦,不能按照机器性能分摊数据。

    上述的分库和分表操作,查询性能和并发能力都得到了提高,但是还有一些需要注意的就是,例如:原本跨表的事物变成了分布式事物;由于记录被切分到不同的数据库和不同的数据表中,难以进行多表关联查询,并且不能不指定路由字段对数据进行查询。分库分表之后,如果我们需要对系统进行进一步的扩阵容(路由策略变更),将变得非常不方便,需要我们重新进行数据迁移。


    最后需要指出的是,分库分表目前有很多的中间件可供选择,最常见的是使用淘宝的中间件Cobar。

    GitHub地址:https://github.com/alibaba/cobara

    文档地址为:https://github.com/alibaba/cobar/wiki

    关于淘宝的中间件Cobar本篇内容不具体介绍,会在后边的学习中在做介绍。

    另外Spring也可以实现数据库的读写分离操作,后边的文章,会进一步学习。

    另外,还有MyCat:http://www.mycat.io 等。

    六、总结

    上述中,我们学到了如何进行数据库的读写分离和分库分表,那么,是不是可以实现一个可扩展、高性能、高并发的网站那?很显然还不可以!一个大型的网站使用到的技术远不止这些,可以说,这些都是其中的最基础的一个环节,因为还有很多具体的细节我们没有掌握到,比如:数据库的集群控制,集群的负载均衡,灾难恢复,故障自动切换,事务管理等等技术。因此,还有很多需要去学习去研究的地方。

    总之:

    路漫漫其修远兮,吾将上下而求索。
    

    前方道路美好而光明,2017年新征程,不泄步!



    个人微信公众号,欢迎订阅:

    这里写图片描述

    展开全文
  • 前后端分离开发具体实现

    万次阅读 多人点赞 2018-11-29 15:02:40
    前后端分离开发具体实现 1 Visual Studio Code开发工具的安装 双击安装,即可成功 3.1.2 Visual Studio Code开发工具的配置 3.1.2.1、设置中文环境 设置中文环境 安装完毕后,vscode全部都是英文版本的,需要把...

    一. 前后端分离开发具体实现

    1 Visual Studio Code开发工具的安装

    双击安装,即可成功
    在这里插入图片描述

    3.1.2 Visual Studio Code开发工具的配置

    3.1.2.1、设置中文环境

    • 设置中文环境

    安装完毕后,vscode全部都是英文版本的,需要把vscode环境调整为中文版本。

    • 调整步骤如下:

      • 第一步:快捷键ctrl+shift+p

        选择Configure Display Language

        在这里插入图片描述

      • 第二步:修改“locale”属性

        在这里插入图片描述

      • 第三步:设置完毕保存文件,重启vscode,就会显示中文。

    在这里插入图片描述

    	好了,到此vscode已经变成中文模式了,现在就可以进入到开发中了。
    

    3.1.2.2 安装Vetur插件

    • 功能表述

      总:综合比较应该是目前 VSCode 上面最好用的 Vue 插件

      分:

      • 语法错误检查,包括 CSS/SCSS/LESS/Javascript/TypeScript
      • 语法高亮,包括 html/jade/pug css/sass/scss/less/stylus js/ts
      • emmet 支持
      • 代码自动补全(目前还是初级阶段),包括 HTML/CSS/SCSS/LESS/JavaScript/TypeScript
    • 安装步骤,如下图操作

    在这里插入图片描述

    3.2 将商品开发的页面放到vs中

    在这里插入图片描述

    复制地址,直接在浏览器打开:
    在这里插入图片描述

    3.3.跨域问题

    3.3.1.什么是跨域

    跨域是指跨域名的访问,以下情况都属于跨域:

    跨域原因说明 示例
    域名不同 www.jd.comwww.taobao.com
    域名相同,端口不同 www.jd.com:8080www.jd.com:8081
    二级域名不同 item.jd.commiaosha.jd.com
    协议不同 http://jd.comhttps://jd.com
    域名和ip地址

    如果域名和端口都相同,但是请求路径不同,不属于跨域,如:

    www.jd.com/item

    www.jd.com/goods

    而我们刚才是从file:///D:/VueWorkspace/102/goods00/Goods.html去访问http://localhost:8092/,这属于域名不同,跨域了。

    3.3.2.为什么有跨域问题?

    1 浏览器的同源策略引起跨域问题

    2 跨域不一定会有跨域问题。

    答:a.跨域没有引起问题:

    因此:跨域问题 是针对ajax的一种限制

    但是这却给我们的开发带来了不便,而且在实际生成环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?

    3.3.3.解决跨域问题的方案

    目前比较常用的跨域解决方案有3种:

    • Jsonp

      最早的解决方案,利用script标签可以跨域的原理实现。

      限制:

      • 需要服务的支持
      • 只能发起GET请求
    • nginx反向代理

      思路是:利用nginx反向代理把跨域为不跨域,支持各种请求方式

    • CORS

      规范化的跨域请求解决方案,安全可靠。

      优势:

      • 在服务端进行控制是否允许跨域,可自定义规则
      • 支持各种请求方式:get、post、put、delete
      • 限制访问的电脑的IP地址

      缺点:

      • 会产生额外的请求(可能发一次/二次请求)
    我们这里会采用nginx搭建静态页面服务器+cors的跨域方案。

    3.4.cors解决跨域

    3.4.1.什么是cors

    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

    它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

    • 浏览器端:

      目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。

    • 服务端:

      CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否运行其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。

    3.4.2.CORS原理有点复杂(了解)

    浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。

    简单请求

    只要同时满足以下两大条件,就属于简单请求。:

    (1) 请求方法是以下三种方法之一:

    • HEAD
    • GET
    • POST

    (2)HTTP的头信息不超出以下5种字段:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

    当浏览器发出的ajax请求是简单请求时,会在请求头中携带一个字段:Origin.

    在这里插入图片描述

    Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

    如果服务器允许跨域,需要在返回的响应头中携带下面信息:

    Access-Control-Allow-Origin: http://localhost:8094
    Access-Control-Allow-Credentials: true
    Content-Type: text/html; charset=utf-8
    
    • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
    • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true

    注意:

    如果跨域请求要想操作cookie,需要满足3个条件:

    • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
    • 浏览器发起ajax需要指定withCredentials 为true
    • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

    特殊请求

    不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。

    预检请求

    特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

    浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

    一个“预检”请求的样板:

    OPTIONS /cors HTTP/1.1
    Origin: http://localhost:8094
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: http://localhost:8094
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    与简单请求相比,除了Origin以外,多了两个头:

    • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
    • Access-Control-Request-Headers:会额外用到的头信息

    预检请求的响应

    服务的收到预检请求,如果许可跨域,会发出响应:

    HTTP/1.1 200 OK
    Date: Mon, 01 Dec 2008 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://localhost:8094
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Access-Control-Max-Age: 1728000
    Content-Type: text/html; charset=utf-8
    Content-Encoding: gzip
    Content-Length: 0
    Keep-Alive: timeout=2, max=100
    Connection: Keep-Alive
    Content-Type: text/plain
    

    除了Access-Control-Allow-OriginAccess-Control-Allow-Credentials以外,这里又额外多出3个头:

    • Access-Control-Allow-Methods:允许访问的方式
    • Access-Control-Allow-Headers:允许携带的头
    • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

    如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。

    3.4.3.实现非常简单

    虽然原理比较复杂,但是前面说过:

    • 浏览器端都有浏览器自动完成,我们无需操心
    • 服务端可以通过拦截器统一实现,不必每次都去进行跨域判定的编写。

    事实上,SpringMVC已经帮我们写好了CORS的跨域过滤器:CorsFilter ,内部已经实现了刚才所讲的判定逻辑,我们直接用就好了。

    在config目录中编写一个配置类,并且注册CorsFilter:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
    import org.springframework.web.filter.CorsFilter;
    
    @Configuration
    public class GlobalCorsConfig {
        @Bean
        public CorsFilter corsFilter() {
            //1.添加CORS配置信息
            CorsConfiguration config = new CorsConfiguration();
            //1) 允许的域,不要写*,否则cookie就无法使用了
            config.addAllowedOrigin("http://localhost:8094");
            //2) 是否发送Cookie信息
            config.setAllowCredentials(true);
            //3) 允许的请求方式
            config.addAllowedMethod("OPTIONS");
            config.addAllowedMethod("HEAD");
            config.addAllowedMethod("GET");
            config.addAllowedMethod("PUT");
            config.addAllowedMethod("POST");
            config.addAllowedMethod("DELETE");
            config.addAllowedMethod("PATCH");
            // 4)允许的头信息
            config.addAllowedHeader("*");
    
            //2.添加映射路径,我们拦截一切请求
            UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
            configSource.registerCorsConfiguration("/**", config);
    
            //3.返回新的CorsFilter.
            return new CorsFilter(configSource);
        }
    }
    

    结构:

    在这里插入图片描述

    4.5.4.重启测试:还是无法访问

    ##3.5 nginx搭建静态HTML服务器

    在这里插入图片描述

    在这里插入图片描述

    只需要修改nginx的config配置文件即可,然后启动

    # windows文件服务器配置
    # 搭建前后端分离页面服务器
    server {
    listen       8094;
    server_name localhost;
    
    charset utf-8;
    
    
    #浏览器打开路径:localhost:8094(listen端口)/location设置的路径/文件.后缀
    location ~ /.*\.(html|htm)$ {
    #文件/图片保存在电脑的地址
    root   D:\\VueWorkspace\\102\\goods00;
    }
    }
    

    在这里插入图片描述

    测试,OK

    展开全文
  • idea如何查看方法的具体实现

    万次阅读 热门讨论 2018-03-05 21:46:27
    按住Ctrl+鼠标点击去,跳转到这个方法的接口上,这明显不是我们想要的,那如何跳转到这个方法的具体实现方法上呢? 解答:http://www.yayihouse.com/yayishuwu/chapter/1099
  • idea如何查看方法的具体实现而不是接口

    万次阅读 多人点赞 2018-06-04 16:36:58
    转自http://www.yayihouse.com/yayishuwu/chapter/1099开发的时候,有时想看所调用的方法的具体实现代码,按住Ctrl+鼠标点击去,跳转到这个方法的接口上,这明显不是我们想要的,那如何跳转到这个方法的具体实现方法...
  • SpringCloud zuul 网关解决跨域问题得具体实现 跨域 在SpringCloud中 zuul 和springboot 要同时配置才能实现网关处理跨域 解决Access to XMLHttpRequest at ...
  • 在研究OpenJDK源码过程中,我发现常用的设计方法就是2种:接口,接口-抽象类-具体实现类 。 在一些其它开源框架和业务开发中,经常存在着第3种设计,接口-具体实现类。
  • 从头认识java-1.3 隐藏具体实现

    千次阅读 2015-10-15 11:58:12
    这一章节我们来聊聊隐藏具体实现。一般来说,编程分为类的创建者与类的使用者,创建者将一部分脆弱的代码隐藏起来,只暴露某些必须的方法给使用者,这样,创建者就可以根据需要修改隐藏部分,而不会影响使用者的调用...
  • 代码: &lt;T&gt; Map&...1. 传入一个接口的Class 类型,获取这个class 的所有具体实现,不包括抽象类 2. 还可以将 applicationContext 单独设置一个值,写成一个工具类,结合Applicat...
  • java 中JSP 实现把excel表中数据导入到mysql数据库中的表中的具体实现方法
  • TCP加速分为单边加速和双边加速,对于单边加速,通过优化拥塞控制算法实现。调研了以下国内外的厂商,有的加速产品通过软件的方式实现,即在客户端或者服务端安装一个...此外TCP加速有木有开源项目或者具体实现思路?
  • Android语音识别的具体实现实例特点分析 在 Android语音识别的具体实现实例介绍的三种具体实现方法,三种方法的特点分析如下: 方法一使用intent调用Google的语音识别程序,通过访问google云端数据来进行识别...
  • 以前是一名php开发人员,最近公司开始一个java开发的erp项目,从新学起的感觉倍爽,各种问题各种遇到,从通过ajax异步提交数据具体实现方法这里开始,以后在博客上慢慢呈现,话不多说,下面就是我通过springMvc和...
  • 具体代码看代码云:传送门,有问题欢迎随时私信~ 引言这篇博客衔接上一篇博客: Holt-Winters模型原理分析及代码实现(python),我们在三次指数平滑的基础上,来进一步讨论下对时序数据的预测。LSTM原理分析(参考...
  • 本文就在Microsoft SQL系列数据库系统中分布式数据的具体实现进行了阐述,并对相关问题进行深入的分析。 2.分布式数据库简介 分布式数据库系统是在集中式数据库系统的基础上发展起来的,由分布
  • 支付宝第三方登录具体实现

    万次阅读 2018-05-25 16:43:52
    具体请参考官方文档:...具体步骤:Step One:创建应用访问https://open.alipay.com/platform/home.htm,创建自己的应用,如下图因为我要实现的是网站使用支付宝登录,因此上图中选择"自定义接入...
  • lingo导入excel数据具体实现步骤

    万次阅读 多人点赞 2017-08-26 21:13:54
    lingo导入excel数据的具体操作步骤
  • 具体实现如下: 1、新增按钮的实现代码(js部分): //弹出框跳转页面 function toPage(flag){ var path="/template/report_27"; //对话框要加载的页面所在路径 $('#table').dialog({ title: "新增
  • Sprite 从PS切图到具体实现完整过程

    万次阅读 2017-10-20 22:17:17
    CSS雪碧图(sprite)是一种网页图片非常高效的处理方式,它是将页面中包含的零星图片利用PS制作到一张大图中,再结合background-position属性进行定位的的方式实现
  • 数据库Connection接口的具体实现方法

    千次阅读 2018-01-02 20:53:04
    连接数据库://1.启动MySQL,可连接数据库 Connection connect = DriverManager.getConnection( "jdbc:mysql://localhost:3306/user","root","root"); //连接URL为 jdbc:mysql//...
  • matlab中 rgb2gray() 函数 具体实现

    万次阅读 多人点赞 2018-06-03 20:13:46
    简介 rgb2gray是matlab内部一种处理图像的函数,通过消除图像色调和饱和度信息同时保留亮度实现将将RGB图像或彩色图转换为灰度图像,即灰度化处理的功能,调用这个功能的格式是I = rgb2gray(RGB),意思是将真彩色...
  • Hadoop -- Map-Reduce具体实现详解

    万次阅读 2007-10-08 15:27:00
    HadoopMapReduce --Map-Reduce具体实现详解 Keyword: FileSplit:文件的子集--文件分割体 简介: 这篇文档描述在hadoop中map和reduce操作是怎样具体完成的。如果你对Google的MapReduce各式模式不熟悉,请先参阅...
  • 我们可以通过实现接口Comparable并重写compareTo()方法来添加对自定义数组的比较,然后通过Arrays的sort()方法来将这个数组排序。有些同学就要问了:为什么重写了compareTo()方法就可以通过Arrays.sort()来进行...
  • 为了演示效果更,后面的statistic.data1-5的内容是相同的,因为使用了相同的Component。 按钮操作 应业务需要,所以需要用到这种动态加载组件的tab,这种方式其实完全没有用到...F12 看到这些tab的具体内容,通过
  • 【代码阅读】PointNet++具体实现详解

    千次阅读 多人点赞 2019-06-03 20:38:41
    ...话不多说,直接上图。 ...虚线大框:一个虚线框代表一个完整的子网络,对应代码中的一个class ...蓝色小块:一个蓝色小块代表一个tensor,蓝色小框的第一行为tensor的名称,第二...但具体的计算过程与PointNet++相同。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 282,066
精华内容 112,826
关键字:

具体实现