单体架构 =》垂直应用架构 =》SOA架构 =》 微服务架构
微服务架构和SOA架构很明显的一个区别就是服务拆分粒度的不同,但是对于拉勾的架构发展来说,我们所看到的SOA阶段其实服务拆分粒度相对来说已经⽐较细了(超前哦!),所以上述拉勾SOA到拉勾微服务,从服务拆分上来说变化并不⼤,只是引入了相对完整的新⼀代Spring Cloud微服务技术。⾃然,上述我们看到的都是拉勾架构演变的阶段结果,每⼀个阶段其实都经历了很多变化,拉勾的服务拆分其实也是⾛过了从粗到细,并⾮绝对的⼀步到位。举个拉勾案例来说明SOA和微服务拆分粒度不同我们在SOA架构的初期,“简历投递模块”和“⼈才搜索模块”都有简历内容展示的需求,只不过说可能略有区别,刚开始在两个模块中各维护了⼀套简历查询和展示的代码;后期我们将服务更细粒度拆分,拆分出简历基础服务,那么不同模块调用这个基础服务即可。
优点
微服务很小,便于特定业务功能的聚焦 A B C D
微服务很小,每个微服务都可以被一个小团队单独实施(开发、测试、部署上线、运维),团队合作一定程度解耦,便于实施敏捷开发
微服务很小,便于重用和模块之间的组装
微服务很独立,那么不同的微服务可以使用不同的语言开发,松耦合
微服务架构下,我们更容易引入新技术
微服务架构下,我们可以更好的实现DevOps开发运维⼀体化;
缺点
微服务架构下,分布式复杂难以管理,当服务数量增加,管理将越加复杂;
微服务架构下,分布式链路跟踪难等;
4.1 服务注册与服务发现
4.2 负载均衡
4.3 熔断
4.4 链路追踪
4.5 API网关(API GATEWAY)
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud是一系列框架的有序集合(Spring Cloud是一个规范) 开发服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等 利用Spring Boot的开发便利性简化了微服务架构的开发(自动装配)
Spring Cloud 规范及实现意图要解决的问题其实就是微服务架构实施过程中存在的一些问题,比如微服务架构中的服务注册发现问题、网络问题(比如熔断场景)、统一认证安全授权问题、负载均衡问题、链路追踪等问题。
3.1 核心组件
3.2 体系结构
Spring Cloud中的各组件协同工作,才能够支持一个完整的微服务架构。比如
注册中心负责服务的注册与发现,很好将各服务连接起来
API网关负责转发所有外来的请求
断路器负责监控服务之间的调用情况,连续多次失败进行熔断保护
配置中心提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息
Dubbo是阿⾥巴巴公司开源的⼀个⾼性能优秀的服务框架,基于RPC调用,对于⽬前使⽤率较⾼的Spring Cloud Netflix来说,它是基于HTTP的,所以效率上没有Dubbo⾼,但问题在于Dubbo体系的组件不全,不能够提供⼀站式解决⽅案,⽐如服务注册与发现需要借助于Zookeeper等实现,⽽Spring Cloud Netflix则是真正的提供了⼀站式服务化解决⽅案,且有Spring⼤家族背景。前些年,Dubbo使⽤率⾼于SpringCloud,但⽬前Spring Cloud在服务化/微服务解决⽅案中已经有了⾮ 常好的发展趋势。
Spring Cloud 只是利用了Spring Boot 的特点,让我们能够快速的实现微服务组件开发,否则不使⽤Spring Boot的话,我们在使⽤Spring Cloud时,每一个组件的相关Jar包都需要我们自己导入配置以及需要开发⼈员考虑兼容性等各种情况。所以Spring Boot是我们快速把Spring Cloud微服务技术应用起来的一种方式。
本部分我们按照普通⽅式模拟一个微服务之间的调用(后续我们将⼀步步使用Spring Cloud的组件对案例进行改造)。 拉勾App有这样一个功能:“面试直通车”,当求职⽤户开启了⾯试直通⻋之后,会根据企业客户的招聘岗位需求进行双向匹配。其中有⼀个操作是:为企业⽤户开启⼀个定时任务,根据企业录入的⽤⼈条件,每⽇匹配⼀定数量的应聘者“投递”到企业的资源池中去,那么系统在将匹配到的应聘者投递到资源池的时候需要先检查:此时应聘者默认简历的状态(公开/隐藏),如果此时默认简历的状态已经被应聘者设置为“隐藏”,那么不再执行“投递”操作。 “⾃动投递功能”在“⾃动投递微服务”中,“简历状态查询功能”在“简历微服务”中,那么就涉及到“⾃动投递微服务”调用“简历微服务”查询简历。在这种场景下,“自动投递微服务”就是一个服务消费者,“简历微服务”就是⼀个服务提供者。
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50717 Source Host : 127.0.0.1:3306 Source Schema : lagou Target Server Type : MySQL Target Server Version : 50717 File Encoding : 65001 Date: 19/04/2020 17:49:15 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for r_resume -- ---------------------------- DROP TABLE IF EXISTS `r_resume`; CREATE TABLE `r_resume` ( `id` int(11) NOT NULL AUTO_INCREMENT, `sex` varchar(10) DEFAULT NULL COMMENT '性别', `birthday` varchar(30) DEFAULT NULL COMMENT '出生日期', `work_year` varchar(100) DEFAULT NULL COMMENT '工作年限', `phone` varchar(20) DEFAULT NULL COMMENT '手机号码', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `status` varchar(80) DEFAULT NULL COMMENT '目前状态', `resumeName` varchar(500) DEFAULT NULL COMMENT '简历名称', `name` varchar(40) DEFAULT NULL, `createTime` datetime DEFAULT NULL COMMENT '创建日期', `headPic` varchar(100) DEFAULT NULL COMMENT '头像', `isDel` int(2) DEFAULT NULL COMMENT '是否删除 默认值0-未删除 1-已删除', `updateTime` datetime DEFAULT NULL COMMENT '简历更新时间', `userId` int(11) DEFAULT NULL COMMENT '用户ID', `isDefault` int(2) DEFAULT NULL COMMENT '是否为默认简历 0-默认 1-非默认', `highestEducation` varchar(20) DEFAULT '' COMMENT '最高学历', `deliverNearByConfirm` int(2) DEFAULT '0' COMMENT '投递附件简历确认 0-需要确认 1-不需要确认', `refuseCount` int(11) NOT NULL DEFAULT '0' COMMENT '简历被拒绝次数', `markCanInterviewCount` int(11) NOT NULL DEFAULT '0' COMMENT '被标记为可面试次数', `haveNoticeInterCount` int(11) NOT NULL DEFAULT '0' COMMENT '已通知面试次数', `oneWord` varchar(100) DEFAULT '' COMMENT '一句话介绍自己', `liveCity` varchar(100) DEFAULT '' COMMENT '居住城市', `resumeScore` int(3) DEFAULT NULL COMMENT '简历得分', `userIdentity` int(1) DEFAULT '0' COMMENT '用户身份1-学生 2-工人', `isOpenResume` int(1) DEFAULT '3' COMMENT '人才搜索-开放简历 0-关闭,1-打开,2-简历未达到投放标准被动关闭 3-从未设置过开放简历', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2195388 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of r_resume -- ---------------------------- BEGIN; INSERT INTO `r_resume` VALUES (2195320, '女', '1990', '2年', '199999999', '[email protected]', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 13:40:14', 'images/myresume/default_headpic.png', 0, '2015-04-24 13:40:14', 1545132, 1, '本科', 0, 0, 0, 0, '', '广州', 15, 0, 3); INSERT INTO `r_resume` VALUES (2195321, '女', '1990', '2年', '199999999', '[email protected]', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:17:54', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:20:35', 1545133, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195322, '女', '1990', '2年', '199999999', '[email protected]', '我目前已离职,可快速到岗', '稻壳儿的简历', 'wps', '2015-04-24 14:42:45', 'images/myresume/default_headpic.png', 0, '2015-04-24 14:43:34', 1545135, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195374, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 17:53:37', 'images/myresume/default_headpic.png', 0, '2015-06-04 17:53:39', 1545523, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195375, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:11:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:11:07', 1545524, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195376, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:12:19', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:12:19', 1545525, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195377, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:13:28', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:13:28', 1545526, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195378, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:15:16', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:15:16', 1545527, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195379, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:06', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:06', 1545528, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195380, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:23:38', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:23:39', 1545529, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); INSERT INTO `r_resume` VALUES (2195381, '女', '1990', '1年', '199999999', '[email protected]', '我目前正在职,正考虑换个新环境', '稻壳儿', 'wps', '2015-06-04 18:27:33', 'images/myresume/default_headpic.png', 0, '2015-06-04 18:27:33', 1545530, 1, '本科', 0, 0, 0, 0, '', '广州', 65, 0, 3); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
3.1 创建父工程
my-lagou-sc
4.0.0 pom com.limeng.sc my-lagou-sc 1.0-SNAPSHOT my-lagou-common-service my-lagou-resume-8095 my-lagou-resume-8096 my-lagou-autodeliever-8097 my-lagou-autodeliever-8098 my-lagou-eureka-server-8761 my-lagou-eureka-server-8762 my-lagou-hystrix-dashboard-8100 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-devtools runtime true org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-starter-openfeign com.sun.xml.bind jaxb-core 2.2.11 javax.xml.bind jaxb-api com.sun.xml.bind jaxb-impl 2.2.11 org.glassfish.jaxb jaxb-runtime 2.2.10-b140310.1920 javax.activation activation 1.1.1 junit junit 4.12 test org.apache.maven.plugins maven-compiler-plugin 11 11 utf-8 org.springframework.boot spring-boot-maven-plugin
3.2 创建common工程
my-lagou-common-service
my-lagou-sc com.limeng.sc 1.0-SNAPSHOT 4.0.0 my-lagou-common-service org.springframework.boot spring-boot-starter-data-jpa mysql mysql-connector-java runtime org.springframework.boot spring-boot-maven-plugin true
创建pojo Resume类
package com.limeng.sc.common.pojo; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="r_resume") public class Resume { @Id /** * 生成策略经常使用的两种: * GenerationType.IDENTITY:依赖数据库中主键自增功能 Mysql * GenerationType.SEQUENCE:依靠序列来产生主键 Oracle */ @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 主键 private String sex; // 性别 //.... //set/get方法 }
3.3 创建 简历子工程 my-lagou-resume
使用springboot + spring jpa
my-lagou-sc com.limeng.sc 1.0-SNAPSHOT 4.0.0 my-lagou-resume com.limeng.sc my-lagou-common-service 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-eureka-client
注意:
1,连接mysql url 要跟 serverTimezone=UTC, 配置文件是application.yml ,也可以是bootstrap.yml(优先级更高)
2,打jar包的时候要打父工程jar包,既 my-lagou-sc -> maven -> Lifecycle ->package/install
server: port: 8095 spring: application: name: my-lagou-resume datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lagou?serverTimezone=UTC username: root password: root jpa: database: MySQL show-sql: true hibernate: naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #避免将驼峰命名转换为下划线命名 #注册到Eureka服务中心 eureka: client: service-url: # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka instance: prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它 instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ metadata-map: m1: n1 m2: n2
启动类
要有扫描包注解,要扫描数据库的东西 @EntityScan("com.limeng.sc.common.pojo") 否则JPA相关报错
Caused by: java.lang.IllegalArgumentException: Not a managed type: class com.limeng.sc.common.pojo.Resume
package com.limeng.sc.resume; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; @SpringBootApplication @EnableDiscoveryClient // 开启服务发现,让Eureka能发现这个服务 @EntityScan("com.limeng.sc.common.pojo") public class ResumeApplication { public static void main(String[] args) { SpringApplication.run(ResumeApplication.class,args); } }
dao:
package com.limeng.sc.resume.dao; import com.limeng.sc.common.pojo.Resume; import org.springframework.data.jpa.repository.JpaRepository; public interface ResumeDao extends JpaRepository{ }
service :
package com.limeng.sc.resume.service; import com.limeng.sc.common.pojo.Resume; import com.limeng.sc.resume.dao.ResumeDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class ResumeService { @Autowired private ResumeDao resumeDao; public Resume getDefaultResumeByUserId(long userId){ Resume resume = new Resume(); resume.setUserId(userId); resume.setIsDefault(1); Exampleexample = Example.of(resume); Optional optional = resumeDao.findOne(example); return optional.get(); } }
controller:
package com.limeng.sc.resume.controller; import com.limeng.sc.resume.service.ResumeService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/resume") public class ResumeController { @Autowired private ResumeService resumeService; @Value("${server.port}") private Integer port; @GetMapping(value="/isOpenResume/{userId}") public Integer getDefaultResumeState(@PathVariable long userId){ // return resumeService.getDefaultResumeByUserId(userId).getIsOpenResume(); return port; } }
3.4 创建auto-deliver子工程
引入maven
org.springframework.cloud spring-cloud-commons org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-netflix-hystrix
配置yaml
server: port: 8098 eureka: client: service-url: # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka instance: prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它 instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ metadata-map: k1: v1 k2: v2 k3: v3 spring: application: name: my-lagou-autodelievr #针对的被调用方微服务名称,不加就是全局生效 my-lagou-resume: ribbon: #请求连接超时时间 ConnectTimeout: 2000 #请求处理超时时间 ##########################################Feign超时时长设置 ReadTimeout: 5000 #对所有操作都进行重试 OkToRetryOnAllOperations: true ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用 MaxAutoRetriesNextServer: 0 #切换实例的重试次数 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整 logging: level: com.limeng.sc.autodeliever.service.ResumeFeignService: debug #配置消费者调用远程接口的权限定路径 # 开启Feign的熔断功能 feign: hystrix: enabled: true compression: request: enabled: true # 开启请求压缩 mime-types: text/html,application/xml,application/json # 设置压缩的数据类型,此处也是默认值 min-request-size: 2048 # 设置触发压缩的大小下限,此处也是默认值 response: enabled: true # 开启响应压缩 hystrix: command: default: execution: isolation: thread: ##########################################Hystrix的超时时长设置 timeoutInMilliseconds: 2000
远程调用my-lagou-resume的API,使用Spring的 RestTemplate
RestTemplate有下列方法:
ResponseEntity getForEntity(String url, Class responseType, Object... uriVariables)//get 得到Entity ResponseEntity postForEntity(String url, @Nullable Object request, Class responseType, Object... uriVariables) T getForObject(String url, Class responseType, Object... uriVariables) //get T postForObject(String url, @Nullable Object request, Class responseType, Object... uriVariables)
使用:
package com.limeng.sc.autodeliever.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @RequestMapping(value="/autoDeliver") public class AutoDeliverController { @Autowired private RestTemplate restTemplate; /* //普通方法调用 @GetMapping(value="/checkState/{userId}") public Integer checkState(@PathVariable long userId){ return restTemplate.getForObject("http://localhost:8095/resume/isOpenResume/"+userId, Integer.class); }*/ @Autowired private DiscoveryClient discoveryClient; /* //Eureka调用 @GetMapping(value="/checkState/{userId}") public Integer checkState(@PathVariable long userId){ Listinstances = discoveryClient.getInstances("my-lagou-resume"); ServiceInstance instance = instances.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://"+host+":"+port+"/resume/isOpenResume/"+userId; System.out.println("==========>>>自动投递服务调用简历查看服务。。。。。。"); return restTemplate.getForObject(url,Integer.class); }*/ @GetMapping(value="/checkState/{userId}") public Integer checkState(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; } @Autowired private ResumeFeignService resumeFeignService; @GetMapping(value="/checkStateFeign/{userId}") public Integer checkStateFeign(@PathVariable long userId){ Integer result = resumeFeignService.isOpenResume(userId); System.out.println(result); return result; } }
启动类:
package com.limeng.sc.autodeliever; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient // 开启服务发现(Eureka,Nacos) //@EnableCircuitBreaker // 开启熔断器功能 /** @SpringCloudApplication包含@SpringBootApplication,@EnableDiscoveryClient,@EnableCircuitBreaker */ @EnableFeignClients // 开启Feign, Feign支持熔断器功能 public class DelieverApplication8097 { public static void main(String[] args) { SpringApplication.run(DelieverApplication8097.class,args); } @LoadBalanced//Ribbon负载均衡 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
我们在自动投递微服务中使用RestTemplate调用简历微服务的简历状态接口时(Restful API 接口)。 在微服务分布式集群环境下会存在什么问题呢?怎么解决? 存在的问题:
在服务消费者中,我们把url地址硬编码到代码中,不方便后期维护。
服务提供者只有一个服务,即便服务提供者形成集群,服务消费者还需要自己实现负载均衡。
在服务消费者中,不清楚服务提供者的状态。
服务消费者调用服务提供者时候,如果出现故障能否及时发现不向用户抛出异常信息?
RestTemplate这种请求调用方式是否还有优化空间?能不能类似于Dubbo那样玩?
这么多的微服务统一认证如何实现?
配置文件每次都修改好多个很麻烦!? ....
上述分析出的问题,其实就是微服务架构中必然面临的一些问题:
服务管理:自动注册与发现、状态监管
服务负载均衡
熔断,服务降级
远程过程调用(RPC)
网关拦截、路由转发
统一认证
集中式配置管理,配置信息实时变动更新
1.1 关于服务注册中心
对于任何一个微服务,原则上都应存在或者支持多个提供者(比如简历微服务部署多个实例),这是由微服务的分布式属性决定的。 更进一步,为了支持弹性扩缩容特性,一个微服务的提供者的数量和分布往往是动态变化的
1.2 主流服务中心对比
Zookeeper Zookeeper它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应 ⽤中经常遇到的⼀些数据管理问题,如:统⼀命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。简单来说zookeeper本质=存储+监听通知。 znode Zookeeper 用来做服务注册中心,主要是因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能及时的通知到监听客户端,这样作为调用方只要使用Zookeeper 的客户端就能实现服务节点的订阅和变更通知功能了,非常方便。另外,Zookeeper可用性也可以,因为只要半数以上的选举节点存活,整个集群就是可用的。集群最小要有3个节点。 Eureka 由Netflix开源,并被Pivatal集成到SpringCloud体系中,它是基于 RestfulAPI 风格开发的服务注册与发现组件。 Consul Consul是由HashiCorp基于Go语⾔开发的⽀持多数据中⼼分布式⾼可⽤的服务发布和注册服务软件, 采⽤Raft算法保证服务的⼀致性,且⽀持健康检查。 Nacos Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说 Nacos就是 注册中心 + 配置中心的组合,帮助我们解决微服务开发必会涉及到的服务注册 与发现,服务配置,服务管理等问题。Nacos 是 Spring Cloud Alibaba 核心组件之一,负责服务注册与发现,还有配置。
组件名 | 语言 | CAP | 对外暴露接口 |
---|---|---|---|
Eureka | Java | AP(自我保护机制,保证可用) | HTTP |
Consul | Go | CP | HTTP/DNS |
Zookeeper | Java | CP | 客户端 |
Nacos | Java | 支持AP/CP切换 | HTTP |
P:分区容错性(一定的要满足的) C:数据一致性 A:高可用 CAP不可能同时满足三个,要么是AP,要么是CP
1.3 服务注册中心组件 Eureka
renew(续约)是Eureka client向server端的,相当于向server发送心跳,报告我是活着的!
Eureka 包含两个组件:Eureka Server 和 Eureka Client,Eureka Client是⼀个Java客户端,由于简化与Eureka Server的交互;Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server 进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息; 1)图中us-east-1c、us-east-1d,us-east-1e代表不同的区也就是不同的机房 2)图中每一个Eureka Server都是一个集群。 3)图中Application Service作为服务提供者向Eureka Server中注册服务,Eureka Server接受到注册事件会在集群和分区中进行数据同步,Application Client作为消费端(服务消费者)可以从Eureka Server中获取到服务注册信息,进行服务调用。 4)微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息 5)Eureka Server在⼀定时间内没有接收到某个微服务节点的⼼跳,Eureka Server将会注销该微服务节点(默认90秒) 6)每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的⽅式完成服务注册列表的同步 7)Eureka Client会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使⽤缓存中的信息找到服务提供者
Eureka通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和可用性。
1.4 Eureka应用及高可用集群
单实例Eureka Server—>访问管理界面—>Eureka Server集群
服务提供者(简历微服务注册到集群)
服务消费者(自动投递微服务注册到集群/从Eureka Server集群获取服务信息)
完成调用
1.4.1 搭建Eureka Server服务注册中心
在lagou-parent中引入Spring Cloud 依赖
org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import
注意1:
springboot需要和springcloud版本对应,否则会报错。
这里springboot版本用 2.1.3.RELEASE
springcloud版本用Greenwich.RELEASE
2020-10-16 11:38:41.332 ERROR 4724 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
注意2:
要在父工程的pom文件中手动引入jaxb的jar,因为Jdk9之后默认没有加载该模块,Eureka Server使用到,所以需要手动导入,否则EurekaServer服务无法启动
创建 eureka server子项目 my-lagou-eureka-server-8761
org.springframework.cloud spring-cloud-starter-netflix-eureka-server
eureka配置文件application.yml
1),eureka单实例配置
server: port: 8761 spring: application: name: lagou-cloud-eureka-server # 应用名称,应用名称会在Eureka中作为服务名称 # eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client eureka: instance: hostname: localhost # 当前eureka实例的主机名 client: service-url: #客户端与EurekaServer交互的地址,如果是集群,也需要写其它Server的地址 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ register-with-eureka: false # ⾃⼰就是服务不需要注册⾃⼰ fetch-registry: false # #⾃⼰就是服务不需要从Eureka Server获取服务信息,默认为true dashboard: enabled: true
2),eureka集群配置
my-lagou-eureka-server-8761,port是8761,hostname是LagouCloudEurekaServerA,defaultZone: http://LagouCloudEurekaServerB:8762/eureka
my-lagou-eureka-server-8762,port是8762,hostname是LagouCloudEurekaServerB,defaultZone: http://LagouCloudEurekaServerA:8761/eureka
#eureka server服务端口 server: port: 8761 spring: application: name: lagou-cloud-eureka-server # 应用名称,应用名称会在Eureka中作为服务名称 #eureka 客户端配置(和Server交互),Eureka Server 其实也是一个Client eureka: instance: hostname: LagouCloudEurekaServerA # 当前eureka实例的主机名 client: service-url: # 配置客户端所交互的Eureka Server的地址(Eureka Server集群中每一个Server其实相对于其它Server来说都是Client) # 集群模式下,defaultZone应该指向其它Eureka Server,如果有更多其它Server实例,逗号拼接即可 defaultZone: http://LagouCloudEurekaServerB:8762/eureka register-with-eureka: true # 集群模式下可以改成true fetch-registry: true # 集群模式下可以改成true dashboard: enabled: true
Eureka启动类 ,@EnableEurekaServer 声明当前工程为Eureka server服务
package com.limeng.sc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer //声明本项目是一个Eureka服务 public class LagouEurekaServerApplication { public static void main(String[] args) { SpringApplication.run(LagouEurekaServerApplication.class,args); } }
访问http://127.0.0.1:8761,如果看到Eureka注册中心后台页面,则表明EurekaServer发布成功
1.4.2 搭建Eureka Server HA高可用集群
在互联网应用中,服务实例很少有单个的。 即使微服务消费者会缓存服务列表,但是如果EurekaServer只有一个实例,该实例挂掉,正好微服务消费者本地缓存列表中的服务实例也不可用,那么这个时候整个系统都受影响。 在生产环境中,我们会配置Eureka Server集群实现高可用。Eureka Server集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。我们开启两台 Eureka Server 以搭建集群。
1.4.3 微服务提供者—>注册到Eureka Server集群
此处自动注册微服务是消费者
org.springframework.cloud spring-cloud-commons org.springframework.cloud spring-cloud-starter-netflix-eureka-client
配置application.ym
eureka: instance: prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它 instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ metadata-map: k1: v1 k2: v2 k3: v3
在启动类添加注解@EnableDiscoveryClient,开启服务发现
服务消费者调用提供者 (使用DiscoveryClient)
1.5 Eureka 细节详解
1.5.1 Eureka元数据
Eureka的元数据有两种:标准元数据和自定义元数据。 标准元数据:主机名、IP地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。 自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。
类似于:
eureka: client: service-url: # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka instance: prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它 instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ metadata-map: k1: v1 k2: v2 k3: v3
使用及测试:调用 getMetadata() 方法 得到一个map
import com.limeng.sc.autodeliever.DelieverApplication; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List; import java.util.Map; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = {DelieverApplication.class}) public class AutoDeliverApplicationTest { @Autowired private DiscoveryClient discoveryClient; @Test public void testAutoDeliverMetadata(){ Listinstances = discoveryClient.getInstances("my-lagou-autodelievr"); ServiceInstance instance = instances.get(0); Map metadata = instance.getMetadata(); for(Map.Entry entry:metadata.entrySet()){ System.out.println(entry.getKey()+":"+entry.getValue()); } } }
结果:
k2:v2 management.port:8096 k1:v1 k3:v3
1.5.2 Eureka客户端详解
服务提供者(也是Eureka客户端)要向Eureka Server注册服务,并完成服务续约等工作
服务注册详解(服务提供者)
当我们导入了Eureka-client依赖后,配置Eureka服务注册中心地址
服务在启动时会向注册中心发起注册请求,携带服务元数据信息
Eureka注册中心会把服务的信息保存在Map中
服务续约详解(服务提供者)
服务每隔30秒会向注册中心续约(心跳)一次(也称为报活),如果没有续约,租约在90秒后到期,然后服务会被失效。每隔30秒的续约操作我们称之为心跳检测
#向Eureka服务中心集群注册服务 eureka: instance: #租约续约间隔时间,默认30秒 lease-renewal-interval-in-seconds: 30 #租约到期,服务时效时间,默认值90秒,服务超过90秒没有发送心跳,EurekaServer会将服务从列表移除 lease-expiration-duration-in-seconds: 90
获取服务列表详解(服务消费者)
每隔30秒服务会从注册中心拉取一份服务列表,这个时间可以通过配置修改。往往不需要我们调整
#向Eureka服务中心集群注册服务 eureka: client: #每隔多久拉取一次服务列表 registry-fetch-interval-seconds: 30
服务消费者启动时,从 EurekaServer服务列表获取只读备份,缓存到本地
每隔30秒,会重新获取并更新数据
每隔30秒的时间可以通过配置eureka.client.registry-fetch-interval-seconds修改
1.5.3 Eureka服务端详解
服务下线
当服务正常关闭时,会发送服务下线的REST请求给EurekaServer。
服务中心接受到请求后,将该服务置为下线状态
失效剔除
Eureka Server会定时(间隔值是eureka.server.eviction-interval-timer-in-ms,默认60s)进行检查,如果发现实例在在一定时间(此值由客户端设置的eureka.instance.lease-expiration-duration-in-seconds定义,默认值为90s)内没有收到心跳,则会注销此实例。
自我保护
定期的续约(服务提供者和注册中心通信),假如服务提供者和注册中心之间的网络有点问题,不代表服务提供者不可用,不代表服务消费者无法访问服务提供者
如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制。
为什么会有自我保护机制?
默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引起了自我保护机制。
服务中心页面会显示如下提示信息
当处于自我保护模式时
不会剔除任何服务实例(可能是服务提供者和EurekaServer之间网络问题),保证了大多数服务依然可用
Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用,当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中。
在Eureka Server工程中通过eureka.server.enable-self-preservation配置可用关停自我保护,默认值是打开
eureka: server: enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
1.6 Eureka源码分析
1.6.1 Eureka Server启动过程
入口:Eureka利用了Springboot自动装配特点
2.1 关于负载均衡
负载均衡一般分为服务器端负载均衡和客户端负载均衡 所谓服务器端负载均衡,比如Nginx、F5这些,请求到达服务器之后由这些负载均衡器根据一定的算法将请求路由到⽬标服务器处理。 所谓客户端负载均衡,比如我们要说的Ribbon,服务消费者客户端会有⼀个服务器地址列表,调用方在请求前通过一定的负载均衡算法选择一个服务器进行访问,负载均衡算法的执行是在请求客户端进行。 Ribbon是Netflix发布的负载均衡器。Eureka一般配合Ribbon进行使用,Ribbon利用从Eureka中读取到服务信息,在调用服务提供者提供的服务时,会根据一定的算法进行负载。
2.2 Ribbon高级应用
不需要引入jar包(引入了Eureka就有Ribbon)
将应用Resume改成两个工程,行成负载均衡
在消费者AutoDeliver项目的RestTemplate加上相应注解
@Bean @LoadBalanced//Ribbon负载均衡 public RestTemplate getRestTemplate(){ return new RestTemplate(); }
调用:host要用 服务者名spring.application.name :my-lagou-resume
@GetMapping(value="/checkState/{userId}") public Integer checkState(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; }
在服务者Resume项目修改服务提供者api返回值,返回当前实例的端口号,便于观察负载情况
@Value("${server.port}") private int port; @GetMapping(value="/isOpenResume/{userId}") public Integer getDefaultResumeState(@PathVariable long userId){ // return resumeService.getDefaultResumeByUserId(userId).getIsOpenResume(); return port; }
结果:
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@435ca70 8096 8096 8096 8095
2.3 Ribbon负载均衡策略
Ribbon内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为 com.netflix.loadbalancer.IRule
负载均衡策略 | 描述 |
---|---|
RoundRobinRule:轮询策略 | 默认超过10次获取到的server都不可用,会返回一个空的server |
RandomRule:随机策略 | 如果随机到的server为null或者不可⽤的话,会while不停的循环选 |
RetryRule:重试策略 | 一定时限内循环重试。默认继承RoundRobinRule,也支持自定义 注册,RetryRule会在每次选取之后,对选举的server进行判断, 是否为null,是否alive,并且在500ms内会不停的选取判断。⽽ RoundRobinRule失效的策略是超过10次,RandomRule是没有失 效时间的概念,只要serverList没都挂。 |
BestAvailableRule:最小连接数策略 | 遍历serverList,选取出可⽤的且连接数最小的一个server。该算 法里面有一个LoadBalancerStats的成员变量,会存储所有server 的运行状况和连接数。如果选取到的server为null,那么会调用 RoundRobinRule重新选取。1(1) 2(1) 3(1) |
AvailabilityFilteringRule:可用过滤策略 | 扩展了轮询策略,会先通过默认的轮询选取一个server,再去判断 该server是否超时可用,当前连接数是否超限,都成功再返回。 |
ZoneAvoidanceRule:区域权衡策略(默认策略) | 扩展了轮询策略,继承了2个过滤器:ZoneAvoidancePredicate和 AvailabilityPredicate,除了过滤超时和链接数过多的server,还会 过滤掉不符合要求的zone区域里面的所有节点,AWS --ZONE 在一 个区域/机房内的服务实例中轮询 |
修改负载均衡策略:
#针对服务被调用方微服务名称,没有则为全局生效 my-lagou-resume: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3.1 微服务中的雪崩效应
什么是微服务中的雪崩效应呢?
微服务中,一个请求可能需要多个微服务接口才能实现,会形成复杂的调用链路。
扇入:代表着该微服务被调用的次数,扇入大,说明该模块复用性好
扇出:该微服务调用其他微服务的个数,扇出大,说明业务逻辑复杂
扇入大是一个好事,扇出大不一定是好事。
在微服务架构中,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”,最终导致上游服务甚⾄整个系统瘫痪。
3.2 雪崩效应解决方案
从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段;
下面,我们介绍三种技术手段应对微服务中的雪崩效应,这三种手段都是从系统可用性、可靠性⻆度出发,尽量防止系统整体缓慢甚至瘫痪。
熔断服务
熔断机制是应对雪崩效应的⼀种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。高压电路中,如果某个地⽅的电压过⾼,熔断器就会熔断,对电路进行保护。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,进行服务的降级,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
注意:
服务熔断重点在“断”,切断对下游服务的调用
服务熔断和服务降级往往是一起使用的,Hystrix就是这样。
服务降级 通俗讲就是整体资源不够用了,先将一些不关紧的服务停掉(调用我的时候,给你返回一个预留的值,也叫做兜底数据),待渡过难关洪峰过去,再把那些服务打开。
服务降级一般是从整体考虑,就是当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备一个本地的fallback回调,返回⼀个缺省值,这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强。
服务限流
服务降级是当服务出问题或者影响到核心流程的性能时,暂时将服务屏蔽掉,待洪峰或者问题解决后再打开;但是有些场景并不能⽤服务降级来解决,比如秒杀业务这样的核心功能,这个时候可以结合服务限流来限制这些场景的并发/请求量限流措施也很多,比如:
限制总并发数(比如数据库连接池、线程池)
限制瞬时并发数(如nginx限制瞬时并发连接数)
限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)
限制远程接口调用速率、限制MQ的消费速率等
3.3 Hystrix简介
Hystrix(豪猪----->刺),宣⾔“defend your app”是由Netflix开源的一个延迟和容错库,对于隔离访问远程系统、服务或者第三类库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。
包裹请求:使用HystrixCommand包裹对依赖的调用逻辑。自动投递微服务方法(@HystrixCommand 添加Hystrix控制) ——调用简历微服务
跳闸机制:当某服务的错误率超过⼀定的阈值时,Hystrix可以跳闸,停止请求该服务一段时间。
资源隔离:Hystrix为每个依赖都维护了⼀个⼩型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被⽴即拒绝,⽽不是排队等待,从而加速失败判定。
监控:Hystrix可以近乎实时地监控运⾏指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
自我修复:断路器打开一段时间后,会自动进入“半开”状态。
3.4 Hystrix熔断应用
目的:简历微服务长时间没有响应,服务消费者—>自动投递微服务快速失败给用户提示
服务消费者工程(自动投递微服务)中引入Hystrix依赖坐标(也可以添加在父工程中)
org.springframework.cloud spring-cloud-starter-netflix-hystrix
服务消费者工程(自动投递微服务)的启动类中添加熔断器开启注解@EnableCircuitBreaker
package com.limeng.sc.autodeliever; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableDiscoveryClient // 开启服务发现 @EnableCircuitBreaker // 开启熔断 public class DelieverApplication8097 { public static void main(String[] args) { SpringApplication.run(DelieverApplication8097.class,args); } }
3.4.1 熔断
调用方:
/** * Hystric熔断服务 * @param userId * @return */ @HystrixCommand( //熔断细节熟悉 commandProperties = //每一个属性都是一个 HystrixProperty @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1000") ) @GetMapping(value="/checkStateTimeout/{userId}") public Integer checkStateTimeout(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; }
3.4.2 熔断+服务降级
/** * Hystric熔断服务 * @param userId * @return */ @HystrixCommand( //熔断细节熟悉 commandProperties = //每一个属性都是一个 HystrixProperty @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1000") ) @GetMapping(value="/checkStateTimeout/{userId}") public Integer checkStateTimeout(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; } /** * Hystric熔断+服务降级 * @param userId * @return */ @HystrixCommand( //熔断细节熟悉 commandProperties = { //每一个属性都是一个 HystrixProperty @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000") },fallbackMethod = "myFallback" ) @GetMapping(value="/checkStateTimeoutFallback/{userId}") public Integer checkStateTimeoutFallback(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; } /** * 预设回退方法,返回默认值 * 形参和返回值需要和原方法一致 */ public Integer myFallback(long userId){ return -1; }
3.5 Hystrix舱壁模式(线程池隔离策略)
如果不进行任何设置,所有熔断方法使用一个Hystrix线程池(10个线程),那么这样的话会导致问题,这个问题并不是扇出链路微服务不可用导致的,这是我们
的线程机制导致的,如果方法A的请求把10个线程都占了,方法2请求处理的时候压根都没法去访问B,因为没有线程可用,并不是B服务不可用。
Hystrix舱壁模式程序修改:
/** * Hystric熔断+服务降级+仓壁模式 * @param userId * @return */ @HystrixCommand( threadPoolKey = "checkStateTimeoutFallbacks",//线程池标志,要保持唯一,不唯一的话就公用了 //线程池属性配置 threadPoolProperties = { @HystrixProperty(name="coreSize",value="5"),//线程数,默认是10 @HystrixProperty(name="maxQueueSize",value="20")//等待队列长度 }, //熔断细节熟悉 commandProperties = { //每一个属性都是一个 HystrixProperty @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000") },fallbackMethod = "myFallback" ) @GetMapping(value="/checkStateTimeoutFallback/{userId}") public Integer checkStateTimeoutFallback(@PathVariable long userId){ Integer result = restTemplate.getForObject("http://my-lagou-resume/resume/isOpenResume/"+userId, Integer.class); System.out.println(result); return result; }
3.6 Hystrix工作流程与高级应用
...
3.7 Hystrix Dashboard断路监控仪表盘
正常状态是UP,跳闸是一种状态CIRCUIT_OPEN,可以通过/health查看。
前提是工程中需要引入SpringBoot的actuator(健康监控),它提供了很多监控所需的接口,可以对应用系统进行配置查看、相关功能统计等。 统一添加在父工程中
org.springframework.boot spring-boot-starter-actuator
如果我们想看到Hystrix相关数据,比如有多少请求、多少成功、多少失败、多少降级等,那么引入SpringBoot健康监控之后,访问/actuator/hystrix.stream接口可以获取到监控的文字信息,但是不直观,所以Hystrix官方还提供了基于图形化的DashBoard(仪表板)监控平 台。Hystrix仪表板可以显示每个断路器(被@HystrixCommand注解的方法)的状态。
新建一个监控服务工程,导入依赖
org.springframework.cloud spring-cloud-starter-netflix-hystrix org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard org.springframework.cloud spring-cloud-starter-netflix-eureka-client
启动类添加@EnableHystrixDashboard激活仪表盘
package com.limeng.sc.hystrix; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; @SpringBootApplication @EnableDiscoveryClient @EnableHystrixDashboard//激活仪表盘 public class HystrixDashboardApplication8100 { public static void main(String[] args) { SpringApplication.run(HystrixDashboardApplication8100.class,args); } }
application.yml:
server: port: 8100 spring: application: name: my-lagou-hystrix-dashboard eureka: client: serviceUrl: # eureka server的路径 defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip) prefer-ip-address: true #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
在被监测的微服务中注册监控servlet(自动投递微服务,监控数据就是来自于这个微服务)
package com.limeng.sc.autodeliever; import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet; import org.springframework.boot.SpringApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; /** @SpringBootApplication @EnableDiscoveryClient // 开启服务发现 @EnableCircuitBreaker // 开启熔断 */ @SpringCloudApplication public class DelieverApplication8097 { public static void main(String[] args) { SpringApplication.run(DelieverApplication8097.class,args); } @LoadBalanced//Ribbon负载均衡 @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } /** * 在被监控的微服务中注册一个serlvet,后期我们就是通过访问这个servlet来获取该服务的Hystrix监控数据的 * 前提:被监控的微服务需要引入springboot的actuator功能 * @return */ @Bean public ServletRegistrationBean getServlet(){ HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/actuator/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } }
访问测试 http://localhost:8100/hystrix
输入监控的微服务端点地址,展示监控的详细数据,比如监控服务消费者 http://localhost:8097/actuator/hystrix.stream
3.8 Hystrix Turbine聚合监控
微服务架构下,一个微服务往往部署多个实例,如果每次只能查看单个实例的监控,就需要经常切换很不方便,在这样的场景下,我们可以使用Hystrix Turbine 进行聚合监控,它可以把相关微服务的监控数据聚合在一起,便于查看。
Turbine服务搭建
新建项目my-lagou-hystrix-turbine-8101,引入依赖坐标
org.springframework.cloud spring-cloud-starter-netflix-turbine org.springframework.cloud spring-cloud-starter-netflix-eureka-client
将需要进行Hystrix监控的多个微服务配置起来,在工程application.yml中开启Turbine及进行相关配置
server: port: 8101 Spring: application: name: my-lagou-hystrix-turbine eureka: client: serviceUrl: # eureka server的路径 defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip) prefer-ip-address: true #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ #turbine配置 turbine: # appCofing配置需要聚合的服务名称,比如这里聚合自动投递微服务的hystrix监控数据 # 如果要聚合多个微服务的监控数据,那么可以使用英文逗号拼接,比如 a,b,c appConfig: my-lagou-autodeliver clusterNameExpression: "'default'" # 集群默认名称
在当前项目启动类上添加注解@EnableTurbine,开启仪表盘以及Turbine聚合
import org.springframework.cloud.netflix.turbine.EnableTurbine; @EnableTurbine // 开启聚合功能
浏览器访问Turbine项目,http://localhost:8101/turbine.stream,就可以看到监控数据了
我们通过dashboard的页面查看数据更直观,把刚才的地址输入dashboard地址栏
4.1 Feign简介
Feign是一个轻量级RESTful的HTTP服务客户端(用它来发起请求,远程调用的),是以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用,Feign被广泛应用在Spring Cloud 的解决方案中。类似于Dubbo,服务消费者拿到服务提供者的接口,然后像调用本地接口方法一样去调用,实际发出的是远程的请求。
Feign可帮助我们更加便捷,优雅的调用HTTP API:不需要我们去拼接url然后呢调用restTemplate的api,在SpringCloud中,使用Feign非常简单,创建一个
接口(在消费者--服务调方用这一端),并在接口上添加一些注解,代码就完成了
SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解(OpenFeign)
本质:封装了Http调用流程,更符合面向接口化 的编程习惯,类似于Dubbo的服务调用 Dubbo的调用方式其实就是很好的面向接口编程
4.2 Feign配置应用
(效果)Feign = RestTemplate+Ribbon+Hystrix
服务消费者工程(自动投递微服务)中引入Feign依赖(或者父类工程)
加入Feign依赖(在父工程或消费者工程中)
org.springframework.cloud spring-cloud-starter-openfeign
服务消费者工程(自动投递微服务)启动类使用注解@EnableFeignClients添加Feign支持
package com.limeng.sc.autodeliever; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients // 开启Feign public class DelieverApplication8098 { public static void main(String[] args) { SpringApplication.run(DelieverApplication8098.class,args); } }
注意:此时去掉Hystrix熔断的支持注解@EnableCircuitBreaker即可包括引入的依赖,因为Feign会自动引入
创建Feign接口
package com.limeng.sc.autodeliever.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; //name:调用的服务名称,和服务提供者yml文件中spring.application.name保持一致 @FeignClient(name="my-lagou-resume") @RequestMapping(value="/resume") public interface ResumeFeignService { //调用的请求路径 @GetMapping(value="/isOpenResume/{userId}") Integer isOpenResume(@PathVariable(value = "userId") long userId); }
注意: 1)@FeignClient注解的name属性用于指定要调用的服务提供者名称,和服务提供者yml文件中spring.application.name保持一致 2)接口中的方法,就好比是远程服务提供者Controller中的Hander方法(只不过如同本地调用了),那么在进行参数绑定的时,可以使用@PathVariable、@RequestParam、@RequestHeader等,这也是OpenFeign对SpringMVC注解的支持,但是需要注意value必须设置,否则会抛出异常
使用接口中方法完成远程调用
package com.limeng.sc.autodeliever.controller; import com.limeng.sc.autodeliever.service.ResumeFeignService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(value="/autoDeliver") public class AutoDeliverController { @Autowired private ResumeFeignService resumeFeignService; @GetMapping(value="/checkState/{userId}") public Integer checkState(@PathVariable long userId){ Integer result = resumeFeignService.isOpenResume(userId); System.out.println(result); return result; } }
4.3 Feign对负载均衡(Ribbon)的支持
#针对的被调用方微服务名称,不加就是全局生效 my-lagou-resume: ribbon: #请求连接超时时间 ConnectTimeout: 2000 #请求处理超时时间 ##########################################Feign超时时长设置 ReadTimeout: 5000 #对所有操作都进行重试 OkToRetryOnAllOperations: true ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用 MaxAutoRetriesNextServer: 0 #切换实例的重试次数 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
4.4 Feign对熔断器(Hystrix)的支持
在Feign客户端工程配置文件(application.yml)中开启Feign对熔断器的支持
# 开启Feign的熔断功能 feign: hystrix: enabled: true
自定义FallBack处理类(需要实现FeignClient接口)
package com.limeng.sc.autodeliever.service; import org.springframework.stereotype.Component; /** * 降级回退逻辑需要定义⼀个类,实现FeignClient接口,实现接口中的方法 */ @Component//扫描到该类的注解 public class ResumeFallback implements ResumeFeignService{ @Override public Integer isOpenResume(long userId) { return -2; } }
在@FeignClient注解中关联2)中自定义的处理类
package com.limeng.sc.autodeliever.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; //name:调用的服务名称,和服务提供者yml文件中spring.application.name保持一致 /** * 使用fallback的时候,类上的 * @RequestMapping的url前缀限定,改成配置在@FeignClient的path属性中 * 否则会报错 */ @FeignClient(name="my-lagou-resume",fallback=ResumeFallback.class, path = "/resume") //@RequestMapping(value="/resume") public interface ResumeFeignService { //调用的请求路径 @GetMapping(value="/isOpenResume/{userId}") Integer isOpenResume(@PathVariable(value = "userId") long userId); }
Feign的超时时长设置那其实就上是Ribbon的超时时长设置
Hystrix超时设置(就按照之前Hystrix设置的方式就OK了)
注意:
开启Hystrix之后,Feign中的方法都会被进行一个管理了,一旦出现问题就进行对应的回退逻辑处理
针对超时这一点,当前有两个超时时间设置(Feign/hystrix),熔断的时候是根据这两个时间的最小值来进行的,即处理时长超过最短的那个超时时间了就熔断进行回退降级逻辑
hystrix: command: default: execution: isolation: thread: ##########################################Hystrix的超时时长设置 timeoutInMilliseconds: 6000
4.5 Feign对请求压缩和响应压缩的支持
Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下⾯的参数 即可开启请求与响应的压缩功能:
# 开启Feign的熔断功能 feign: hystrix: enabled: true compression: request: enabled: true # 开启请求压缩 mime-types: text/html,application/xml,application/json # 设置压缩的数据类型,此处也是默认值 min-request-size: 2048 # 设置触发压缩的大小下限,此处也是默认值 response: enabled: true # 开启响应压缩
4.6 Feign的日志级别配置
Feign是http请求客户端,类似于咱们的浏览器,它在请求和接收响应的时候,可以打印出比较详细的一些日志信息(响应头,状态码等等)
如果我们想看到Feign请求时的日志,我们可以进行配置,默认情况下Feign的日志没有开启。
开启Feign日志功能及级别
package com.limeng.sc.autodeliever.config; import feign.Logger; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FeignConfig { // Feign的日志级别(Feign请求过程信息) // NONE:默认的,不显示任何日志----性能最好 // BASIC:仅记录请求方法、URL、响应状态码以及执行时间----生产问题追踪 // HEADERS:在BASIC级别的基础上,记录请求和响应的header // FULL:记录请求和响应的header、body和元数据----适用于开发及测试环境定位问题 @Bean public Logger.Level feignLevel(){ return Logger.Level.FULL; } }
配置log日志级别为debug
logging: level: com.limeng.sc.autodeliever.service.ResumeFeignService: debug #配置消费者调用远程接口的权限定路径
结果:
网关(翻译过来就叫做GateWay):微服务架构中的重要组成部分
局域网中就有网关这个概念,局域网接收或者发送数据出去通过这个网关,比如用Vmware虚拟机软件搭建虚拟机集群的时候,往往我们需要选择IP段中的一个
IP作为网关地址。
我们学习的GateWay-->Spring Cloud GateWay(它只是众多网关解决方案中的一种)
5.1 GateWay简介
Spring Cloud GateWay是Spring Cloud的一个全新项目,目标是取代Netflix Zuul,它基于Spring5.0+SpringBoot2.0+WebFlux(基于高性能的Reactor模式响应式通信框架Netty,异步非阻塞模型)等技术开发,性能高于Zuul,官⽅测试,GateWay是Zuul的1.6倍,旨在为微服务架构提供一种简单有效的统⼀的API路由管理⽅式。Spring Cloud GateWay不仅提供统一的路由方式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成一些功能) 链的式提供了相关基本的功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。
网关在架构中的位置
5.2 GateWay核心概念
Zuul1.x 阻塞式IO 2.x 基于Netty Spring Cloud GateWay天⽣就是异步⾮阻塞的,基于Reactor模型 一个请求—>网关根据一定的条件匹配—匹配成功之后可以将请求转发到指定的服务地址;⽽在这个过程中,我们可以进行⼀些⽐较具体的控制(限流、⽇志、⿊⽩名单)
路由(route): 网关最基础的部分,也是⽹关⽐较基础的⼯作单元。路由由一个ID、一个⽬标URL(最终路由到的地址)、⼀系列的断⾔(匹配条件判断)和Filter过滤器(精细化控制)组成。如果断⾔为true,则匹配该路由。
断言(predicates):参考了Java8中的断⾔java.util.function.Predicate,开发⼈员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于nginx中的location匹配⼀样),如果断⾔与请求相匹配则路由。
过滤器(filter):一个标准的Spring webFilter,使用过滤器,可以在请求之前或者之后执行业务逻辑。
5.3 GateWay工作过程
客户端向Spring Cloud GateWay发出请求,然后在GateWay Handler Mapping中找到与请求相匹配的路由,将其发送到GateWay Web Handler;Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间⽤虚线分开是因为过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑。Filter在“pre”类型过滤器中可以做参数校验、权限校验、流量监控、⽇志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等。
GateWay核心逻辑:路由转发+执行过滤器链
5.4 GateWay应用
使用网关对自动投递微服务进行代理(添加在它的上游,相当于隐藏了具体微服务的信息,对外暴露的是网关)
创建工程my-lagou-gateway-server-9002导入依赖
GateWay不需要使用web模块,它引入的是WebFlux(类似于SpringMVC)
注意:不要引人starter-web模块,需要引入web-flux
4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE my-lagou-gateway-9002 org.springframework.cloud spring-cloud-commons org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-test test com.sun.xml.bind jaxb-core 2.2.11 javax.xml.bind jaxb-api com.sun.xml.bind jaxb-impl 2.2.11 org.glassfish.jaxb jaxb-runtime 2.2.10-b140310.1920 javax.activation activation 1.1.1 org.springframework.boot spring-boot-starter-actuator org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import org.apache.maven.plugins maven-compiler-plugin 11 11 utf-8 org.springframework.boot spring-boot-maven-plugin
application.yml 配置文件部分内容
server: port: 9002 eureka: client: serviceUrl: # eureka server的路径 defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip) prefer-ip-address: true #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ spring: application: name: my-lagou-gateway cloud: gateway: routes: # 路由可以有多个 - id: service-autodeliver-router # 我们自定义的路由 ID,保持唯一 #uri: http://127.0.0.1:8098 # 目标服务地址 自动投递微服务(部署多实例)动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址 uri: lb://my-lagou-autodelievr # gateway网关从服务注册中心获取实例信息然后负载后路由 # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。 predicates: - Path=/autoDeliver/** - id: service-resume-router # 我们自定义的路由 ID,保持唯一 #uri: http://127.0.0.1:8095 # 目标服务地址 #http://localhost:9002/resume/openstate/1545132 #http://127.0.0.1:8081/openstate/1545132 uri: lb://my-lagou-resume # 断言:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默 认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。 predicates: - Path=/resume/** filters: - StripPrefix=1
上面这段配置的意思是,配置了一个 id 为 service-autodeliver-router 的路由规则,当向网关发起请求
http://lagoucloudeurekaservera:9002/autoDeliver/checkState/1545132,请求会被分发路由到对应的微服务上
启动引导类
package com.limeng.sc.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients // 开启Feign public class LagouGatewayServerApplication { public static void main(String[] args) { SpringApplication.run(LagouGatewayServerApplication.class,args); } }
#### 5.5 GateWay路由规则详解  #### 5.6 GateWay动态路由详解 GateWay支持自动从注册中心中获取服务列表并访问,即所谓的动态路由 实现步骤如下 GateWay支持自动从注册中心中获取服务列表并访问,即所谓的动态路由 1)pom.xml中添加注册中心端依赖(因为要获取注册中心服务列表,eureka客户端已经引入) 2)动态路由配置 **注意:动态路由设置时,uri以 lb: //开头**(lb代表从注册中心获取服务),后面是需要转发到的服务名称 ```properties spring: application: name: my-lagou-gateway cloud: gateway: routes: # 路由可以有多个 - id: service-autodeliver-router # 我们自定义的路由 ID,保持唯一 #uri: http://127.0.0.1:8098 # 目标服务地址 自动投递微服务(部署多实例) 动态路由:uri配置的应该是一个服务名称,而不应该是一个具体的服务实例的地址 uri: lb://my-lagou-autodelievr # gateway网关从服务注册中心获取实例信息然后负载后路由 predicates: - Path=/autoDeliver/**
5.7 GateWay过滤器
5.7.1 GateWay过滤器简介
从过滤器生命周期(影响时机点)的⻆度来说,主要有两个pre和post:
生命周期时机点 | 作用 |
---|---|
pre | 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、 在集群中选择 请求的微服务、记录调试信息等。 |
post | 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、 收集统计信息和指标、将响应从微服务发送给客户端等。 |
从过滤器类型的⻆度,Spring Cloud GateWay的过滤器分为GateWayFilter和GlobalFilter两种
如Gateway Filter可以去掉url中的占位后转发路由,比如
predicates: - Path=/resume/** filters: - StripPrefix=1 # 可以去掉resume之后转发
注意:GlobalFilter全局过滤器是程序员使用比较多的过滤器,我们主要讲解这种类型
5.7.2 Gateway自定义过滤器
过滤器代码
package com.limeng.sc.filter; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.io.UnsupportedEncodingException; import java.util.concurrent.ConcurrentHashMap; @Component public class IPDefendFilter implements GlobalFilter, Ordered { private static ConcurrentHashMapIP_COUNTER = new ConcurrentHashMap<>(); @Value("${ip_defend.limit}") private int LIMIT; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); String path = request.getURI().getPath(); if(path.startsWith("/api/user/register")){ String clientIp = request.getRemoteAddress().getHostString(); int count = IP_COUNTER.getOrDefault(clientIp,0); System.out.println(clientIp+"=============>count:"+count); if(count>=LIMIT){ response.setStatusCode(HttpStatus.UNPROCESSABLE_ENTITY); // 状态码 String data = "注册请求过多。。。!"; DataBuffer wrap = null; try { wrap = response.bufferFactory().wrap(data.getBytes("utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } System.out.println("超过"+LIMIT+",频繁进行行行注册,请求已被拒绝"); return response.writeWith(Mono.just(wrap)); } IP_COUNTER.put(clientIp,count+1); } return chain.filter(exchange); } @Override public int getOrder() { return 1; } }
6.1 分布式配置中心应用场景
往往,我们使⽤配置⽂件管理⼀些配置信息,比如application.yml
单体应用架构,配置信息的管理、维护并不会显得特别麻烦,⼿动操作就可以,因为就⼀个⼯程;
微服务架构,因为我们的分布式集群环境中可能有很多个微服务,我们不可能⼀个⼀个去修改配置然后重启⽣效,在⼀定场景下我们还需要在运⾏期间动态调整配置
信息,⽐如:根据各个微服务的负载情况,动态调整数据源连接池⼤⼩,我们希望配置内容发⽣变化的时候,微服务可以⾃动更新。
场景总结如下:
1)集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的(⼀次修改、到处⽣效)
2)不同环境不同配置,比如数据源配置在不同环境(开发dev,测试test, 生产prod)中是不同的
3)运⾏期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池⼤⼩等配置修改后可⾃动更新
4)如配置内容发⽣变化,微服务可以⾃动更新配置
那么,我们就需要对配置文件进行集中式管理,这也是分布式配置中心的作用
6.2 Spring Cloud Config
6.2.1 Config简介
Spring Cloud Config是一个分布式配置管理方案,包含了 Server端和 Client端两个部分。
Server 端:提供配置⽂件的存储、以接口的形式将配置⽂件的内容提供出去,通过使用@EnableConfigServer注解在 Spring boot 应⽤中⾮常简单的嵌入
Client 端:通过接口获取配置数据并初始化⾃⼰的应用
6.2.2 Config分布式配置应用
说明:Config Server是集中式的配置服务,对于集中管理应⽤程序各个环境下的配置。 默认使用Git存储配置文件内容,也可以SVN。
比如,我们要对“简历微服务”的application.yml进行管理(区分开发环境、测试环境、生产环境)
1)登录码云,创建项目 lagou-config-server-9006
2)上传yml配置文件,命名规则如下:
{application}-{profile}.yml 或者 {application}-{profile}.properties
其中,application为应用名称,profile指的是环境(对于区分开发环境,测试环境、生产环境等)
示例:my-lagou-resume-dev.yml、my-lagou-resume-test.yml、my-lagou-resume-prod.yml
还需要安装rabbitmq, 具体见 https://www.jianshu.com/p/3d43561bb3ee
[安装RabbitMQ] https://www.jianshu.com/p/3d43561bb3ee
开启MQ 服务:
在命令行CMD中,进入rabbitmq安装目录的sbin ,执行以下命令 ???
rabbitmq-plugins enable rabbitmq_management
访问 http://127.0.0.1:15672/
(RabbitMQ应用间通信端口是5672)
username=guest
password=guest
3)构建Config Server统一配置中心 新建SpringBoot工程,引入依赖坐标(需要注册应用到Eureka)
my-lagou-sc com.limeng.sc 1.0-SNAPSHOT 4.0.0 my-lagou-configserver-9006 org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-config-server org.springframework.cloud spring-cloud-starter-bus-amqp
启动引导类:
package com.limeng.sc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.config.server.EnableConfigServer; @SpringBootApplication @EnableDiscoveryClient @EnableConfigServer//开启配置服务器功能 public class LagouConfigServerApplication9006 { public static void main(String[] args) { SpringApplication.run(LagouConfigServerApplication9006.class,args); } }
yaml配置文件
server: port: 9006 #注册到Eureka服务中心 eureka: client: service-url: # 注册到集群,就把多个Eurekaserver地址使用逗号连接起来即可;注册到单实例(非集群模式),那就写一个就ok defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka instance: prefer-ip-address: true #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) # 实例名称: 192.168.1.103:lagou-service-resume:8080,我们可以自定义它 instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@ spring: application: name: my-lagou-configserver cloud: config: server: git: uri: https://gitee.com/BranJava/springcloud1_work.git #配置git服务地址 username: [email protected] #配置git用户名 password: a2b7z7s3!Q #配置git密码 search-paths: - springcloud1_work # 读取分支 label: master rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest # springboot中暴露健康检查等断点接口 management: endpoints: web: exposure: include: "*" # 暴露健康接口的细节 endpoint: health: show-details: always
测试访问:http://127.0.0.1:9006/master/my-lagou-resume-dev.yml,查看到配置文件内容
4)构建Client客户端(在已有简历微服务基础上)
已有工程中添加依赖坐标
org.springframework.cloud spring-cloud-config-client
application.yml修改为bootstrap.yml配置文件
bootstrap.yml是系统级别的,优先级比application.yml高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启动。(主要是把与统一配置中心连接的配置信息放到bootstrap.yml)
注意:需要统一读取的配置信息,从集中配置中心获取bootstrap.yml
spring: cloud: # config客户端配置,和ConfigServer通信,并告知ConfigServer希望获取的配置信息在哪个文件中 config: name: my-lagou-config #配置文件名称 profile: dev #后缀名称 label: master #分支名称 uri: http://127.0.0.1:9006 #ConfigServer配置中心地址
这时候可能会报错:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configServerTest': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'datasource.driver' in value "${datasource.driver}"
将 application.yml修改为bootstrap.yml配置文件 就好了
6.3 Config配置手动刷新
不用重启微服务,只需要手动的做一些其他的操作(访问这个地址/refresh)刷新,之后再访问即可此时,客户端取到了配置中心的值,但当我们修改GitHub上面
的值时,服务端(Config Server)能实时获取最新的值,但客户端(Config Client)读的是缓存,⽆法实时获取最新值。Spring Cloud已 经为我们解决了这个问
题,那就是客户端使用post去触发refresh,获取最新数据。
1)Client客户端添加依赖springboot-starter-actuator(已添加)
2)Client客户端bootstrap.yml中添加配置(暴露通信端点)
management: endpoints: web: exposure: include: refresh #也可以暴露所有的端⼝ management: endpoints: web: exposure: include: "*"
3)Client客户端使用到配置信息的类上添加@RefreshScope
4)手动向Client客户端发起POST请求,http://127.0.0.1:8095/actuator/refresh,刷新配置信息
注意:手动刷新方式避免了服务重启(流程:Git改配置—>for循环脚本手动刷新每个微服务
6.4 Config配置手动更新
在微服务架构中,我们可以结合消息总线(Bus)实现分布式配置的自动更新(Spring CloudConfig+Spring Cloud Bus
6.4.1 消息总线Bus
所谓消息总线Bus,即我们经常会使用MQ消息代理构建一个共用的Topic,通过这个Topic连接各个微服务实例,MQ广播的消息会被所有在注册中心的微服务实例
监听和消费。换⾔之就是通过一个主题连接各个微服务,打通脉络
Spring Cloud Bus(基于MQ的,支持RabbitMq/Kafka) 是Spring Cloud中的消息总线方案,SpringCloud Config + Spring Cloud Bus 结合可以实现配置信息的⾃动更新。
6.4.2 Spring Cloud Config+Spring Cloud Bus 实现自动刷新
MQ消息代理,我们还选择使用RabbitMQ,ConfigServer和ConfigClient都添加都消息总线的⽀持以及与RabbitMq的连接信息
1)Config Server服务端添加消息总线支持
org.springframework.cloud spring-cloud-starter-bus-amqp
2)ConfigServer添加配置
spring: rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
3)微服务暴露端口
management: endpoints: web: exposure: include: bus-refresh #暴露所有端口 management: endpoints: web: exposure: include: "*"
5)重启各个服务,更改配置之后,向配置中心服务端发送post请求 http://127.0.0.1:9006/actuator/bus-refresh,各个客户端配置即可自动刷新
在广播模式下实现了一次请求,处处更新,如果我只想定向更新呢?
在发起刷新请求的时候 http://127.0.0.1:9006/actuator/bus-refresh/lagou-service-resume:8085
即为最后面跟上要定向刷新的实例的 服务名:端口号即可
1.1 分布式链路追踪技术适用场景
1.2 分布式链路追踪技术核心思想
本质:记录日志,作为一个完整的技术,分布式链路追踪也有自己的理论和概念
微服务架构中,针对请求处理的调用链可以展现为一棵树,示意如下
注意:我们往往把Spring Cloud Sleuth 和 Zipkin 一起使用,把 Sleuth 的数据信息发送给 Zipkin 进行聚合,利用 Zipkin 存储并展示数据。
1.3 Sleuth + Zipkin
每一个需要被追踪踪迹的微服务工程都引入依赖坐标
org.springframework.cloud spring-cloud-starter-sleuth
每一个微服务都修改application.yml配置文件,添加日志级别
#分布式链路追踪 logging: level: org.springframework.web.servlet.DispatcherServlet: debug org.springframework.cloud.sleuth: debug
请求到来时,我们在控制台可以观察到 Sleuth 输出的日志(全局 TraceId、SpanId等)。
这样的日志首先不容易阅读观察,另外日志分散在各个微服务服务器上,接下来我们使用zipkin统一聚合轨迹日志并进行存储展示
结合 Zipkin 展示追踪数据
Zipkin 包括Zipkin Server和 Zipkin Client两部分,Zipkin Server是一个单独的服务,Zipkin Client就是具体的微服务
Zipkin Server 构建,创建 my-lagou-zipkin-server-9411
pom.xml
io.zipkin.java zipkin-server 2.12.3 org.springframework.boot spring-boot-starter-log4j2 io.zipkin.java zipkin-autoconfigure-ui 2.12.3 io.zipkin.java zipkin-autoconfigure-storage-mysql 2.12.3 mysql mysql-connector-java com.alibaba druid-spring-boot-starter 1.1.10 org.springframework spring-tx org.springframework spring-jdbc
入口启动类
package com.limeng.sc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import zipkin2.server.internal.EnableZipkinServer; @SpringBootApplication @EnableZipkinServer public class LagouZipkinServerApplication9411 { public static void main(String[] args) { SpringApplication.run(LagouZipkinServerApplication9411.class,args); } }
application.yml
server: port: 9411 management: metrics: web: server: auto-time-requests: false # 关闭自动检测 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lagou?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000 # 指定zipkin持久化介质为mysql zipkin: storage: type: mysql
Zipkin Client 构建(在具体微服务中修改,在gateway微服务,auto-deliver微服务,resume微服务中)
pom.xml
org.springframework.cloud spring-cloud-starter-zipkin
application.yml(或bootstrap.yml) 中添加对zipkin server的引用(zipkin和sleuth是同一级别,都在spring下)
spring: application: name: lagou-service-autodeliver zipkin: base-url: http://127.0.0.1:9411 # zipkin server的请求地址 sender: # web 客户端将踪迹日志数据通过网络请求的方式传送到服务端,另外还有配置 # kafka/rabbit 客户端将踪迹日志数据传递到mq进行中转 type: web service: # 需要有这个名字,否则zipkin找不到 name: my-lagou-gateway sleuth: sampler: # 采样率 1 代表100%全部采集 ,默认0.1 代表10% 的请求踪迹数据会被采集 # 生产环境下,请求量非常大,没有必要所有请求的踪迹数据都采集分析,对于网络包括server端压力都是比较大的,可以配置采样率采集一定比例的请求的踪迹数据进行分析即可 probability: 1
另外,对于log日志,依然保持开启debug状态
#分布式链路追踪 logging: level: org.springframework.web.servlet.DispatcherServlet: debug org.springframework.cloud.sleuth: debug
Zipkin server 页面方便我们查看服务调用依赖关系及一些性能指标和异常信息
将Zipkin追踪数据 持久化到mysql
mysql中创建名称为zipkin的数据库,并执行如下sql语句(官方提供)
CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `remote_service_name` VARCHAR(255), `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query', PRIMARY KEY (`trace_id_high`, `trace_id`, `id`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds'; ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames'; ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames'; ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range'; CREATE TABLE IF NOT EXISTS zipkin_annotations ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1', `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB', `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation', `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp', `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address', `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null', `endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds'; ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames'; ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values'; ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values'; ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job'; CREATE TABLE IF NOT EXISTS zipkin_dependencies ( `day` DATE NOT NULL, `parent` VARCHAR(255) NOT NULL, `child` VARCHAR(255) NOT NULL, `call_count` BIGINT, `error_count` BIGINT, PRIMARY KEY (`day`, `parent`, `child`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
zipkin server pom文件引入相关依赖
io.zipkin.java zipkin-autoconfigure-storage-mysql 2.12.3 mysql mysql-connector-java com.alibaba druid-spring-boot-starter 1.1.10 org.springframework spring-tx org.springframework spring-jdbc
zipkin server端修改配置文件,添加如下内容
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lagou?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000 # 指定zipkin持久化介质为mysql zipkin: storage: type: mysql
zipkin server端启动类中注入事务管理器
@Bean public PlatformTransactionManager txManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
测试
访问 http://localhost:8097/autoDeliver/checkState/1
在zipkin页面 查找服务:
2.1 OAuth2介绍
OAuth(开放授权)是一个开放协议/标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,⽽不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。
允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容
资源所有者(Resource Owner):可以理解为用户自己
客户端(Client):我们想登陆的网站或应用,比如拉勾网
认证服务器(Authorization Server):可以理解为微信或者QQ
资源服务器(Resource Server):可以理解为微信或者QQ
2.2 OAuth2的颁发Token授权方式
1)授权码(authorization-code) --》比如授权QQ登录
2)密码式(password)提供用户名+密码换取token令牌 --》
授权码模式使用到了回调地址,是最复杂的授权方式,微博、微信、QQ等第三方登录就是这种模式。
我们重点讲解接口对接中常使用的password密码模式(提供⽤户名+密码换取token)。
2.3 Spring Cloud OAuth2 + JWT 实现
2.3.1 Spring Cloud OAuth2介绍
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务
(统⼀认证授权服务)发送某个类型的grant_type进行集中认证和授权,从⽽获得access_token(访问令牌),⽽这个token是受其他微服务信任的。
注意:使用OAuth2解决问题的本质是,引入了一个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。
2.3.2 Spring Cloud OAuth2构建微服务统一认证服务思路
注意:在我们统一认证的场景中,Resource Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接口就是资源,发起http请求的浏览器就是Client客户端(对应为第三方应用)
2.3.3 搭建认证服务器(Authorization Server)
认证服务器(Authorization Server),负责颁发token
新建项目lagou-oauth-server-9999
pom.xml
my-lagou-sc com.limeng.sc 1.0-SNAPSHOT 4.0.0 com.limeng.sc my-lagou-oauth-server-9999 com.limeng.sc my-lagou-common-service 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.11.RELEASE org.springframework.security.oauth spring-security-oauth2 2.3.4.RELEASE mysql mysql-connector-java com.alibaba druid-spring-boot-starter 1.1.10 org.springframework spring-tx org.springframework spring-jdbc
application.yml(构建认证服务器,配置文件无特别之处)
server: port: 9999 Spring: application: name: lagou-cloud-oauth-server datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000 eureka: client: serviceUrl: # eureka server的路径 defaultZone: http://lagoucloudeurekaservera:8761/eureka/,http://lagoucloudeurekaserverb:8762/eureka/ #把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表 instance: #使用ip注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是ip) prefer-ip-address: true #自定义实例显示格式,加上版本号,便于多版本管理,注意是ip-address,早期版本是ipAddress instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
入口类无特殊之处
package com.limeng.sc; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class LagouOauthServerApplication9999 { public static void main(String[] args) { SpringApplication.run(LagouOauthServerApplication9999.class,args); } }
认证服务器配置类
package com.limeng.sc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; /** * 当前类为Oauth2 server的配置类(需要继承特定的⽗类AuthorizationServerConfigurerAdapter) */ @Configuration @EnableAuthorizationServer//开启认证服务器功能 public class AauthServerConfiger extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; /** * 认证服务器最终是以API接口的方式对外提供服务(校验合法性并生成令牌,校验令牌等) * 涉及到接口访问权限,在这里设置 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口 security //允许客户端表单认证 .allowFormAuthenticationForClients() //开启端口token_key的访问权限 .tokenKeyAccess("permitAll()") //开启端口check_token的访问权限 .checkTokenAccess("permitAll()"); } /** * 客户端详情配置 * 比如 client_id , secret (username, pwd??) * 认证服务器会给客户端 颁发client_id等必要参数,表明客户端是谁 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { super.configure(clients); clients.inMemory()//客户端信息存储在什么地⽅,可以在内存中,可以在数据库⾥ .withClient("client_limeng")//添加⼀个client配置,指定其client_id .secret("aaa")//指定客户端密码/安全码 .resourceIds("autodeliver")//指定客户端所能访问资源 id清单,此处的资源id是需要在具体的资源服务器上也配置⼀样 //认证类型/令牌颁发模式,可以配置多个在这⾥,但是不⼀定都⽤,具体使⽤哪种⽅式颁发token,需要客户端调用的时候传递参数指定 .authorizedGrantTypes("password","refresh_token") .scopes("all");//客户端的权限范围,此处配置为all全部即可 } /** * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储, * 那么存储在哪里呢?都是在这里配置) * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { super.configure(endpoints); endpoints .tokenStore(tokenStore())//指定token存储的地方,这里是存在内存中 .tokenServices(AuthorizationServerTokenServices())//token服务的⼀个描述,可以认为是token⽣成细节的描述,⽐如有效时间多少等 .authenticationManager(authenticationManager) 指定认证管理器,随后注⼊⼀个到当前类使⽤即可 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); } /** * 该⽅法⽤于创建tokenStore对象(令牌存储对象)token以什么形式存储 * @return */ private TokenStore tokenStore() { return new InMemoryTokenStore(); } /** * 该方法用个token服务对象(该对象描述了token有效期等信息) * @return */ public AuthorizationServerTokenServices AuthorizationServerTokenServices(){ DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); //设置令牌有效时间(⼀般设置为2个⼩时) defaultTokenServices.setAccessTokenValiditySeconds(120); defaultTokenServices.setRefreshTokenValiditySeconds(259200);// 设置刷新令牌的有效时间 return defaultTokenServices; } }
关于三个configure方法
configure(ClientDetailsServiceConfigurer clients)
用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调
取详情信息
configure(AuthorizationServerEndpointsConfigurer endpoints)
用来配置令牌(token)的访问端点和令牌服务(token services)
configure(AuthorizationServerSecurityConfigurer oauthServer) 用来配置令牌端点的安全约束.
关于 TokenStore
InMemoryTokenStore
默认采用,它可以完美的工作在单服务器上(即访问并发量 压⼒不⼤的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本
的实现来进行尝试,你可以在开发的时候使⽤它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
JdbcTokenStore
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。
JwtTokenStore
这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),缺点就是这个令牌占⽤的空间会⽐较⼤,如果你加入了比较多⽤户凭证信息,JwtTokenStore 不会保存任何数据。
认证服务器安全配置类
package com.limeng.sc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.ArrayList; @Configuration public class SecurityConfoger extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 密码编码对象(密码不进行加密处理) * @return */ @Bean public PasswordEncoder passwordEncoder(){ return NoOpPasswordEncoder.getInstance(); } @Autowired private PasswordEncoder passwordEncoder; /** * 处理⽤户名和密码验证事宜 * 1)客户端传递username和password参数到认证服务器 * 2)⼀般来说,username和password会存储在数据库中的⽤户表中 * 3)根据⽤户表中数据,验证当前传递过来的⽤户信息的合法性 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { UserDetails user = new User("admin","aaa",new ArrayList<>()); auth.inMemoryAuthentication() .withUser(user).passwordEncoder(passwordEncoder); } }
测试
获取token:http://localhost:9999/oauth/token?client_secret=aaa&grant_type=password&username=admin&password=aaa&client_id=client_limeng
endpoint:/oauth/token
获取token携带的参数
client_id:客户端id
client_secret:客户单密码
grant_type:指定使用哪种颁发类型,password
username:用户名
password:密码
校验token:http://localhost:9999/oauth/check_token?token=a7d55a97-e403-4196-81bc-d43454767f89
刷新token:http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_limeng&client_secret=aaa&refresh_token=65a855e7-f661-4bfc-bbc0-babc0e57ef1e
刷新token后再校验需要携带新的token
资源服务器(希望访问被认证的微服务)Resource Server配置
pom.xml里加入
org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.11.RELEASE org.springframework.security.oauth spring-security-oauth2 2.3.4.RELEASE
资源服务配置类
package com.limeng.sc.autodeliever.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.RemoteTokenServices; @Configuration @EnableResourceServer // 开启资源服务器功能 @EnableWebSecurity // 开启web访问安全 public class ResourceServerConfiger extends ResourceServerConfigurerAdapter { /** * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜 * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { // 设置当前资源服务的资源id resources.resourceId("autodeliver"); // 定义token服务对象(token校验就应该靠token服务对象) RemoteTokenServices remoteTokenServices = new RemoteTokenServices(); // 校验端点/接口设置 remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token"); // 携带客户端id和客户端安全码 remoteTokenServices.setClientId("client_limeng"); remoteTokenServices.setClientSecret("aaa"); // 别忘了这⼀步 resources.tokenServices(remoteTokenServices); } /** 场景:⼀个服务中可能有很多资源(API接口) * 某⼀些API接口,需要先认证,才能访问 * 某⼀些API接口,压根就不需要认证,本来就是对外开放的接口 * 我们就需要对不同特点的接口区分对待(在当前configure⽅法中完成),设置是否需要经过认证 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() // 设置session的创建策略(根据需要创建即可) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() .antMatchers("/autodeliver/**").authenticated() .antMatchers("/demo/**").authenticated() .anyRequest().permitAll(); } }
写"/demo","/others" controller方法
测试
2.3.4 JWT改造统一认证授权中心的令牌存储机制
JWT令牌介绍
通过上边的测试我们发现,当资源服务和授权服务不在⼀起时资源服务使⽤RemoteTokenServices 远程请求授权 服务验证token,如果访问量较⼤将会影响系统的性能。
解决上边问题: 令牌采⽤JWT格式即可解决上边的问题,⽤户认证通过会得到⼀个JWT令牌,JWT令牌中已经包括了⽤户相关的信 息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法⾃⾏完成令牌校验,⽆需每次都请求认证 服务完成授权。
1)什么是JWT?
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519),它定义了⼀种简介的、⾃包含的协议格式,⽤于 在通信双⽅传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使⽤RSA的公 钥/私钥对来签名,防止被篡改。
2)JWT令牌结构
JWT令牌由三部分组成,每部分中间使⽤点(.)分隔,⽐如:xxxxx.yyyyy.zzzzz
Header
Payload
Signature
认证服务器端JWT改造(改造主配置类)
package com.limeng.sc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; /** * 当前类为Oauth2 server的配置类(需要继承特定的⽗类AuthorizationServerConfigurerAdapter) */ @Configuration @EnableAuthorizationServer//开启认证服务器功能 public class AauthServerConfiger extends AuthorizationServerConfigurerAdapter { private String sign_key = "limeng123"; @Autowired private AuthenticationManager authenticationManager; /** * 认证服务器最终是以API接口的方式对外提供服务(校验合法性并生成令牌,校验令牌等) * 涉及到接口访问权限,在这里设置 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口 security //允许客户端表单认证 .allowFormAuthenticationForClients() //开启端口token_key的访问权限 .tokenKeyAccess("permitAll()") //开启端口check_token的访问权限 .checkTokenAccess("permitAll()"); } /** * 客户端详情配置 * 比如 client_id , secret (username, pwd??) * 认证服务器会给客户端 颁发client_id等必要参数,表明客户端是谁 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { super.configure(clients); clients.inMemory()//客户端信息存储在什么地⽅,可以在内存中,可以在数据库⾥ .withClient("client_limeng")//添加⼀个client配置,指定其client_id .secret("aaa")//指定客户端密码/安全码 .resourceIds("autodeliver")//指定客户端所能访问资源 id清单,此处的资源id是需要在具体的资源服务器上也配置⼀样 //认证类型/令牌颁发模式,可以配置多个在这⾥,但是不⼀定都⽤,具体使⽤哪种⽅式颁发token,需要客户端调用的时候传递参数指定 .authorizedGrantTypes("password","refresh_token") .scopes("all");//客户端的权限范围,此处配置为all全部即可 } /** * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储, * 那么存储在哪里呢?都是在这里配置) * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { super.configure(endpoints); endpoints .tokenStore(tokenStore())//指定token存储的地方,这里是存在内存中 .tokenServices(AuthorizationServerTokenServices())//token服务的⼀个描述,可以认为是token⽣成细节的描述,⽐如有效时间多少等 .authenticationManager(authenticationManager) 指定认证管理器,随后注⼊⼀个到当前类使⽤即可 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); } /** * 该⽅法⽤于创建tokenStore对象(令牌存储对象)token以什么形式存储 * @return */ private TokenStore tokenStore() { // 使⽤jwt令牌 return new JwtTokenStore(jwtAccessTokenConvertor()); } /** * 该⽅法⽤户获取⼀个token服务对象(该对象描述了token有效期等信息) * @return */ public AuthorizationServerTokenServices AuthorizationServerTokenServices(){ DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setTokenEnhancer(jwtAccessTokenConvertor()); //设置令牌有效时间(⼀般设置为2个⼩时) defaultTokenServices.setAccessTokenValiditySeconds(300); defaultTokenServices.setRefreshTokenValiditySeconds(259200);// 设置刷新令牌的有效时间 return defaultTokenServices; } /** * 返回jwt令牌转换器 * 这里,可以把签名秘钥传递过去给转换器 * @return */ private JwtAccessTokenConverter jwtAccessTokenConvertor() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(sign_key);// 签名密钥 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));// 验证时使⽤的密钥,和签名密钥保持⼀致 return jwtAccessTokenConverter; } }
资源服务器校验JWT令牌
不需要和远程认证服务器交互,添加本地tokenStore
package com.limeng.sc.autodeliever.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; @Configuration @EnableResourceServer // 开启资源服务器功能 @EnableWebSecurity // 开启web访问安全 public class ResourceServerConfiger extends ResourceServerConfigurerAdapter { private String sign_key = "limeng123"; /** * 该⽅法⽤于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜 * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { //jwt令牌改造,改为无状态设置 resources.resourceId("autodeliver").tokenStore(tokenStore()).stateless(true); } private TokenStore tokenStore() { //使用jwt令牌 return new JwtTokenStore(jwtAccessTokenConvertor()); } /** * 返回jwt令牌转换器 * 这里,可以把签名秘钥传递过去给转换器 * @return */ private JwtAccessTokenConverter jwtAccessTokenConvertor() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(sign_key);// 签名密钥 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));// 验证时使⽤的密钥,和签名密钥保持⼀致 return jwtAccessTokenConverter; } /** 场景:⼀个服务中可能有很多资源(API接口) * 某⼀些API接口,需要先认证,才能访问 * 某⼀些API接口,压根就不需要认证,本来就是对外开放的接口 * 我们就需要对不同特点的接口区分对待(在当前configure⽅法中完成),设置是否需要经过认证 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() // 设置session的创建策略(根据需要创建即可) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() .antMatchers("/autodeliver/**").authenticated() .antMatchers("/demo/**").authenticated() .anyRequest().permitAll(); } }
测试
获取token
刷新token,刷新后原来的token还能用
资源服务器获取资源
2.3.5 从数据库加载Oauth2客户端信息
创建数据表并初始化数据(表名及字段保持固定)
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of oauth_client_details -- ---------------------------- BEGIN; INSERT INTO `oauth_client_details` VALUES ('client_limeng', 'autodeliver,resume', '123', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
配置数据源
Spring: application: name: lagou-cloud-oauth-server datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lagou?serverTimezone=UTC username: root password: root druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000
认证服务器主配置类改造
package com.limeng.sc.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.jwt.crypto.sign.MacSigner; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import javax.sql.DataSource; /** * 当前类为Oauth2 server的配置类(需要继承特定的⽗类AuthorizationServerConfigurerAdapter) */ @Configuration @EnableAuthorizationServer//开启认证服务器功能 public class AauthServerConfiger extends AuthorizationServerConfigurerAdapter { private String sign_key = "limeng123"; @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; /** * 认证服务器最终是以API接口的方式对外提供服务(校验合法性并生成令牌,校验令牌等) * 涉及到接口访问权限,在这里设置 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口 security //允许客户端表单认证 .allowFormAuthenticationForClients() //开启端口token_key的访问权限 .tokenKeyAccess("permitAll()") //开启端口check_token的访问权限 .checkTokenAccess("permitAll()"); } /** * 客户端详情配置 * 比如 client_id , secret (username, pwd??) * 认证服务器会给客户端 颁发client_id等必要参数,表明客户端是谁 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { super.configure(clients); clients.withClientDetails(jdbcClientDetailsService()); } private ClientDetailsService jdbcClientDetailsService() { // 从内存中加载客户端详情改为从数据库中加载客户端详情 JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource); return jdbcClientDetailsService; } /** * 认证服务器是玩转token的,那么这里配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储, * 那么存储在哪里呢?都是在这里配置) * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { super.configure(endpoints); endpoints .tokenStore(tokenStore())//指定token存储的地方,这里是存在内存中 .tokenServices(AuthorizationServerTokenServices())//token服务的⼀个描述,可以认为是token⽣成细节的描述,⽐如有效时间多少等 .authenticationManager(authenticationManager) 指定认证管理器,随后注⼊⼀个到当前类使⽤即可 .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); } /** * 该⽅法⽤于创建tokenStore对象(令牌存储对象)token以什么形式存储 * @return */ private TokenStore tokenStore() { // 使⽤jwt令牌 return new JwtTokenStore(jwtAccessTokenConvertor()); } /** * 该⽅法⽤户获取⼀个token服务对象(该对象描述了token有效期等信息) * @return */ public AuthorizationServerTokenServices AuthorizationServerTokenServices(){ DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setTokenEnhancer(jwtAccessTokenConvertor()); //设置令牌有效时间(⼀般设置为2个⼩时) defaultTokenServices.setAccessTokenValiditySeconds(300); defaultTokenServices.setRefreshTokenValiditySeconds(259200);// 设置刷新令牌的有效时间 return defaultTokenServices; } /** * 返回jwt令牌转换器 * 这里,可以把签名秘钥传递过去给转换器 * @return */ private JwtAccessTokenConverter jwtAccessTokenConvertor() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(sign_key);// 签名密钥 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));// 验证时使⽤的密钥,和签名密钥保持⼀致 return jwtAccessTokenConverter; } }
2.3.6 从数据库验证用户合法性
创建数据表user(表名不需固定),初始化数据
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` char(10) DEFAULT NULL, `password` char(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of user -- ---------------------------- BEGIN; INSERT INTO `users` VALUES (4, 'limeng', 'aaa'); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
操作数据表的JPA配置以及针对该表的操作的Dao接口
开发UserDetailsService接口的实现类,根据用户名从数据库加载用户信息
package com.limeng.sc.service; import com.limeng.sc.dao.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; @Service public class UserService implements UserDetailsService { @Autowired private UserRepository userRepository; /** * 根据username查询出该⽤户的所有信息,封装成UserDetails类型的对象返回,⾄于密码,框 * 架会⾃动匹配 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { com.limeng.sc.common.pojo.User user = userRepository.findByUsername(username); return new User(user.getUsername(),user.getPassword(),new ArrayList<>()); } }
使用自定义的用户详情服务对象
package com.limeng.sc.config; import com.limeng.sc.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * 该配置类,主要处理用户名和密码的校验等事宜 */ @Configuration public class SecurityConfoger extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private PasswordEncoder passwordEncoder; /** * 密码编码对象(密码不进行加密处理) * @return */ @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 处理⽤户名和密码验证事宜 * 1)客户端传递username和password参数到认证服务器 * 2)⼀般来说,username和password会存储在数据库中的⽤户表中 * 3)根据⽤户表中数据,验证当前传递过来的⽤户信息的合法性 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(passwordEncoder); } }
测试
获取令牌: http://localhost:9999/oauth/token?client_secret=123&grant_type=password&username=limeng&password=aaa&client_id=client_limeng
2.3.7 基于Oauth2的 JWT 令牌信息扩展
OAuth2帮我们⽣成的JWT令牌载荷部分信息有限,关于⽤户信息只有⼀个user_name,有些场景下我们希望放⼊⼀些扩展信息项,⽐如,之前我们经常向session中存⼊userId,或者现在我希望在JWT的载荷部分存⼊当时请求令牌的客户端IP,客户端携带令牌访问资源服务时,可以对⽐当前请求的客户端真实IP和令牌中存放的客户端IP是否匹配,不匹配拒绝请求,以此进⼀步提⾼安全性。
认证服务器生成JWT令牌时存入扩展信息(比如clientIp)
package com.limeng.sc.config; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Map; @Component public class LagouAccessTokenConvertor extends DefaultAccessTokenConverter { @Override public MapconvertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { Map stringMap = (Map ) super.convertAccessToken(token, authentication); HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest(); stringMap.put("clientIp", request.getRemoteAddr()); return stringMap; } }
将自定义的转换器对象注入
private JwtAccessTokenConverter jwtAccessTokenConvertor() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(sign_key);// 签名密钥 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));// 验证时使⽤的密钥,和签名密钥保持⼀致 jwtAccessTokenConverter.setAccessTokenConverter(accessTokenConvertor); return jwtAccessTokenConverter; }
2.3.8 资源服务器取出 JWT令牌扩展信息
资源服务器也需要定义一个转换器类,继承DefaultAccessTokenConverter,重写extractAuthentication提取方法,把载荷信息设置到认证对象的details属性中
package com.limeng.sc.autodeliever.config; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; import org.springframework.stereotype.Component; import java.util.Map; @Component public class LagouAccessTokenTransfer extends DefaultAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Mapmap) { OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map); oAuth2Authentication.setDetails(map);// 将map放⼊认证对象中,认证对象在controller中可以拿到 return oAuth2Authentication; } }
将自定义的转换器对象注入
/** * 返回jwt令牌转换器 * 这里,可以把签名秘钥传递过去给转换器 * @return */ private JwtAccessTokenConverter jwtAccessTokenConvertor() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setSigningKey(sign_key);// 签名密钥 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key));// 验证时使⽤的密钥,和签名密钥保持⼀致 jwtAccessTokenConverter.setAccessTokenConverter(accessTokenTransfer); return jwtAccessTokenConverter; }
业务类比如Controller类中,可以通过SecurityContextHolder.getContext().getAuthentication()获取到认证对象,进一步获取到扩展信息
获取到扩展信息后,就可以做其他的处理了,⽐如根据userId进⼀步处理,或者根据clientIp处理,或者其他都是可以的了
1.1 Nacos 介绍
Nacos (Dynamic Naming and Configuration Service)是阿里巴巴开源的一个针对微服务架构中服务发现、配置管理和服务管理平台。
Nacos就是注册中心+配置中心的组合(Nacos=Eureka+Config+Bus)
官网:https://nacos.io 下载地址:https://github.com/alibaba/Nacos
Nacos功能特性
服务发现与健康检查
动态配置管理
动态DNS服务
服务和元数据管理(管理平台的⻆度,nacos也有⼀个ui⻚⾯,可以看到注册的服务及其实例信息(元数据信息)等),动态的服务权重调整、动态服务优雅下线,都可以去做
1.2 Nacos 单例服务部署
下载解压安装包,执行命令启动(我们使⽤最近⽐较稳定的版本 nacos-server-1.2.0.tar.gz)1.2.1?
linux/mac:sh startup.sh -m standalone windows:cmd startup.cmd
访问nacos管理界面:http://127.0.0.1:8848/nacos/#/login(默认端口8848,账号和密码nacos/nacos)
1.3 Nacos 服务注册中心
1.3.1 服务提供者注册到Nacos(改造简历微服务)
在父pom中引入SCA依赖
com.alibaba.cloud spring-cloud-alibaba-dependencies 2.1.0.RELEASE pom import
改造服务提供者工程。引入nacos客户端依赖(注释eureka客户端)
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery
application.yml修改,添加nacos配置信息
spring: application: name: my-lagou-resume-nacos-8093 cloud: nacos: discovery: server-addr: 127.0.0.1:8848 cluster-name: BEIJING # cluster 会去优先找同一个cluster下的服务
启动简历,自动投递微服务,观察nacos控制台
http://127.0.0.1:8848/nacos/#/login(账号和密码nacos/nacos)
保护阈值:可以设置为0-1之间的浮点数,它其实是一个比例值(当前服务健康实例数/当前服务总实例数)
保护阈值的意义在于
当服务A健康实例数/总实例数 < 保护阈值 的时候,说明健康实例真的不多了,这个时候保护阈值会被触发(状态true)
nacos将会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,但这样也会造成雪崩要好,牺牲了
⼀些请求,保证了整个系统的高可用。
注意:阿里内部在使用nacos的时候,也经常调整这个保护阈值参数。
1.3.2 负载均衡
Nacos客户端引用的时候,会关联引入Ribbon的依赖包,我们使用OpenFiegn的时候也会引入Ribbon的依赖,Ribbon包括Hystrix都按原来方式进行配置即可此
处,我们将简历微服务,并启动了一个8083端口,注册到Nacos上,便于测试负载均衡,我们通过后台也可以看出。
1.3.3 Nacos 数据模型(领域模型)
Namespace命名空间、Group分组、集群这些都是为了进行归类管理,把服务和配置⽂件进行归类,归类之后就可以实现⼀定的效果,
比如隔离:对于服务来说,不同命名空间中的服务不能够互相访问调用
Namespace:命名空间,对不同的环境进行隔离,比如隔离开发环境、测试环境和生产环境
Group:分组,将若干个服务或者若干个配置集归为一组,通常习惯一个系统归为一个组
Service:某个服务,比如简历微服务
DataId:配置集或者可以认为是一个配置⽂件
Namespace + Group + Service 如同 Maven 中的GAV坐标,GAV坐标是为了锁定Jar,⼆这⾥是为了锁定服务
Namespace + Group + Service 如同 Maven 中的GAV坐标,GAV坐标是为了锁定Jar,⼆这⾥是为了锁定服务
Nacos抽象出了Namespace、Group、Service、DataId等概念,具体代表什么取决于怎么用(非常灵活),推荐用法如下
概念 | 描述 |
---|---|
Namespace | 代表不同的环境,如开发dev、测试test、生产环境prod |
Group | 代表某项目,比如拉勾云项目 |
Service | 某个项目中具体xxx服务 |
DataId | 某个项目中具体的xxx配置文件 |
1.3.4 Nacos Server 数据持久化
注意:1.2版本server mysql数据库要用5.7,不要使用过高版本,否则会报错!
Nacos 默认使用嵌入式数据库进行数据存储,它支持改为外部Mysql存储
新建数据库 nacos_config,数据库初始化脚本文件 ${nacoshome}/conf/nacos-mysql.sql
修改${nacoshome}/conf/application.properties,增加Mysql数据源配置
spring.datasource.platform=mysql ### Count of DB: db.num=1 ### Connect URL of DB: db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config? characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=root
然后需要做权限相关配置:
1.4 Nacos 配置中心
之前:Spring Cloud Config + Bus
1) Github 上添加配置文件
2)创建Config Server 配置中心—>从Github上去下载配置信息
3)具体的微服务(最终使⽤配置信息的)中配置Config Client—> ConfigServer获取配置信息
有Nacos之后,分布式配置就简单很多
Github不需要了(配置信息直接配置在Nacos server中),Bus也不需要了(依然可以完成动态刷新)
注意:
data id
配置内容要能折叠的!?
否则会出现异常 java.lang.IllegalArgumentException: Could not resolve placeholder 'limeng.message' in value "${limeng.message}"
Nacos 服务端已经搭建完毕,那么我们可以在我们的微服务中开启 Nacos 配置管理
在Nacos Server中添加配置信息
com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config
微服务中如何锁定 Nacos Server 中的配置文件(dataId)
通过 Namespace + Group + dataId 来锁定配置文件,Namespace不指定就默认public,Group不指定就默认 DEFAULT_GROUP
dataId 的完整格式如下:
${prefix}-${spring.profile.active}.${file-extension}
prefix 默认为 spring.application.name 的值,也可以通过配置项spring.cloud.nacos.config.prefix 来配置。
spring.profile.active 即为当前环境对应的 profile。 注意:当 spring.profile.active为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
file-exetension 为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension 来配置。当前只支持 properties 和 yaml类型。
cloud: nacos: discovery: server-addr: 127.0.0.1:8848 cluster-name: BEIJING # cluster 会去优先找同一个cluster下的服务 config: server-addr: 127.0.0.1:8848 namespace: 421c1021-b0ca-4826-8931-e31ca5f9999e #dev group: DEFAULT_GROUP file-extension: yaml
通过 Spring Cloud 原生注解 @RefreshScope 实现配置自动更新
package com.limeng.sc.nacos_resume.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/config") @RefreshScope public class ConfigController { @Value("${limeng.message}") private String message; @Value("${datasource.url}") private String driverUrl; @GetMapping("/message") public String message(){ return message+":"+driverUrl; } }
2.1 Sentinel 介绍
Sentinel是一个面向云原生微服务的流量控制、熔断降级组件。
替代Hystrix,针对问题:服务雪崩、服务降级、服务熔断、服务限流
Sentinel:
1)独立可部署Dashboard/控制台组件
2)减少代码开发,通过UI界面配置即可完成细粒度控制(自动投递微服务)
Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿⾥巴巴近 10 年的双⼗⼀⼤促流量的核⼼场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填⾕、集
群流量控制、实时熔断下游不可用应⽤等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接⼊应⽤的单台机器秒级数据,甚⾄ 500 台以下规模的集群的汇总运⾏情况。
广泛的开源生态:Sentinel 提供开箱即⽤的与其它开源框架/库的整合模块,例如与 SpringCloud、Dubbo的整合。您只需要引⼊相应的依赖并进行简单的配置
即可快速地接⼊ Sentinel。
完善的 SPI 扩展点:Sentinel 提供简单易⽤、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
2.2 Sentinel 部署
下载地址:https://github.com/alibaba/Sentinel/releases 我们使用v1.7.1
启动:java -jar sentinel-dashboard-1.7.1.jar &
用户名/密码:sentinel/sentinel
2.3 服务改造
在我们已有的业务场景中,“自动投递微服务”调用了“简历微服务”,我们在⾃动投递微服务进行的熔断降级等控制,那么接下来我们改造⾃动投递微服务,引用Sentinel核心包。
为了不污染之前的代码,复制一个自动投递微服务 lagou-service-autodeliver-8098-sentinel
pom.xml引人依赖
com.alibaba.cloud spring-cloud-starter-alibaba-sentinel
application.yml修改(配置sentinel dashboard,暴露断点依然要有,删除原有hystrix配置,删除原有OpenFeign的降级配置)
server: port: 8098 spring: application: name: lagou-service-autodeliver cloud: nacos: discovery: server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 sentinel: transport: dashboard: 127.0.0.1:8080 # sentinel dashboard/console 地址 port: 8719 # sentinel会在该端口启动http server,那么这样的话,控制台定义的一些限流等规则才能发送传递过来, #如果8719端口被占用,那么会依次+1 # Sentinel Nacos数据源配置,Nacos中的规则会自动同步到sentinel流控规则中 datasource: # 自定义的流控规则数据源名称 flow: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} data-id: ${spring.application.name}-flow-rules groupId: DEFAULT_GROUP data-type: json rule-type: flow # 类型来自RuleType类 # 自定义的降级规则数据源名称 degrade: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} data-id: ${spring.application.name}-degrade-rules groupId: DEFAULT_GROUP data-type: json rule-type: degrade # 类型来自RuleType类 management: endpoints: web: exposure: include: "*" # 暴露健康接口的细节 endpoint: health: show-details: always #针对的被调用方微服务名称,不加就是全局生效 lagou-service-resume: ribbon: #请求连接超时时间 ConnectTimeout: 2000 #请求处理超时时间 ##########################################Feign超时时长设置 ReadTimeout: 3000 #对所有操作都进行重试 OkToRetryOnAllOperations: true ####根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置), ####如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置), ####如果依然不行,返回失败信息。 MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第一次调用 MaxAutoRetriesNextServer: 0 #切换实例的重试次数 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整 logging: level: # Feign日志只会对日志级别为debug的做出响应 com.lagou.edu.controller.service.ResumeServiceFeignClient: debug
上述配置之后,启动自动投递微服务,使用Sentinel 监控自动投递微服务
此时我们发现控制台没有任何变化,因为懒加载,我们只需要发起一次请求触发即可
2.4 Sentinel 关键概念
概念名称 | 概念描述 |
---|---|
资源 | 它可以是 Java 应⽤程序中的任何内容,例如,由应⽤程序提供的服务,或由应⽤程序调⽤ 的其它应⽤提供的服务,甚⾄可以是⼀段代码。我们请求的API接⼝就是资源 |
规则 | 围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规 则。所有规则可以动态实时调整。 |
2.5 Sentinel 流量规则模块
系统并发能⼒有限,比如系统A的QPS支持1个,如果太多请求过来,那么A就应该进行流量控制了,比如其他请求直接拒绝
资源名:默认请求路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名称,默认default(不区分来源)阈值类型/单机阈值
QPS:(每秒钟请求数量)当调用该资源的QPS达到阈值时进行限流
线程数:当调用该资源的线程数达到阈值的时候进行限流(线程处理请求的时候,如果说业务逻辑执行时间很⻓,流量洪峰来临时,会耗费很多线程资源,这些线程资源会堆积,最终可能造成服务不可用,进⼀步上游服务不可用,最终可能服务雪崩)
是否集群:是否集群限流
流控模式:
直接:资源调用达到限流条件时,直接限流
关联:关联的资源调用达到阈值时候限流⾃⼰
链路:只记录指定链路上的流量
流控效果:
快速失败:直接失败,抛出异常
Warm Up:根据冷加载因⼦(默认3)的值,从阈值/冷加载因⼦,经过预热时⻓,才达到设置的QPS阈值
排队等待:匀速排队,让请求匀速通过,阈值类型必须设置为QPS,否则无效
流控模式之关联限流
关联的资源调用达到阈值时候限流⾃⼰,⽐如用户注册接口,需要调调身份证校验接口(往往身份证校验接口),如果身份证校验接口请求达到阈值,使⽤关联,可以使用户注册接口进行限流。
流控模式之链路限流
流控效果之Warm up
当系统⻓期处于空闲的情况下,当流量突然增加时,直接把系统拉升到⾼⽔位可能瞬间把系统压垮,比如电商⽹站的秒杀模块。通过 Warm Up 模式(预热模式),让通过的流量缓慢增加,经过设置的预热时间以后,到达系统处理请求速率的设定值。Warm Up 模式默认会从设置的 QPS 阈值的 1/3 开始慢慢往上增加⾄ QPS 设置值。
流控效果之排队等待
例如,QPS 配置为 5,则代表请求每 200 ms 才能通过一个,多出的请求将排队等待通过。超时时间代表最⼤排队时间,超出最⼤排队时间的请求将会直接被拒绝。排队等待模式下,QPS 设置值不要超过1000(请求间隔 1 ms)。
2.6 Sentinel 降级规则模块
2.7 Sentinel 自定义兜底逻辑
2.8 基于 Nacos 实现 Sentinel 规则持久化
目前,Sentinel Dashboard中添加的规则数据存储在内存,微服务停掉规则数据就消失,在生产环境下不合适。我们可以将Sentinel规则数据持久化到Nacos配置中心,让微服务从Nacos获取规则数据。
自动投递微服务的pom.xml中添加依赖
com.alibaba.csp sentinel-datasource-nacos
自动投递微服务的application.yml中配置Nacos数据源
spring: application: name: lagou-service-autodeliver cloud: nacos: discovery: server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 sentinel: transport: dashboard: 127.0.0.1:8080 # sentinel dashboard/console 地址 port: 8719 # sentinel会在该端口启动http server,那么这样的话,控制台定义的一些限流等规则才能发送传递过来, #如果8719端口被占用,那么会依次+1 # Sentinel Nacos数据源配置,Nacos中的规则会自动同步到sentinel流控规则中 datasource: # 自定义的流控规则数据源名称 flow: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} data-id: ${spring.application.name}-flow-rules groupId: DEFAULT_GROUP data-type: json rule-type: flow # 类型来自RuleType类 # 自定义的降级规则数据源名称 degrade: nacos: server-addr: ${spring.cloud.nacos.discovery.server-addr} data-id: ${spring.application.name}-degrade-rules groupId: DEFAULT_GROUP data-type: json rule-type: degrade # 类型来自RuleType类
Nacos Server中添加对应规则配置集,选择JSON格式(public命名空间—>DEFAULT_GROUP中添加)流控规则配置集 lagou-service-autodeliver-flow-rules
[ { "resource":"findResumeOpenState", "limitApp":"default", "grade":1, "count":1, "strategy":0, "controlBehavior":0, "clusterMode":false } ]
所有属性来自源码FlowRule类
resource:资源名称
limitApp:来源应用
grade:阈值类型 0 线程数 1 QPS
count:单机阈值
strategy:流控模式,0 直接 1 关联 2 链路
controlBehavior:流控效果,0 快速失败 1 Warm Up 2 排队等待
clusterMode:true/false 是否集群
降级规则配置集 lagou-service-autodeliver-degrade-rules
[ { "resource":"findResumeOpenState", "grade":2, "count":1, "timeWindow":5 } ]
所有属性来自源码DegradeRule类
resource:资源名称
grade:降级策略 0 RT 1 异常比例 2 异常数
count:阈值
timeWindow:时间窗
改造“自动投递微服务”和“简历微服务”,删除OpenFeign 和 Ribbon,使用Dubbo RPC 和 Dubbo LB。
首先,需要删除或者注释掉父工程中的热部署依赖(否则会报缺少feign依赖的错误)
3.1 服务提供者工程改造
接口类
改造提供者工程(简历微服务)
com.alibaba.cloud spring-cloud-starter-dubbo com.alibaba.csp sentinel-apache-dubbo-adapter com.lagou.edu lagou-service-dubbo-api 1.0-SNAPSHOT
删除原有ResumeService接口,引入dubbo服务接口工程中的ResumeService接口,适当调整代码,在service的实现类上添加dubbo的@Service注解
application.yml或者bootstrap.yml配置文件中添加dubbo配置
spring: main: allow-bean-definition-overriding: true #Springboot 2.1需要设定 dubbo: scan: # dubbo 服务扫描基准包 base-packages: com.lagou.edu.service.impl protocol: # dubbo 协议 name: dubbo # dubbo 协议端⼝( -1 表示⾃增端⼝,从 20880 开始) port: -1 registry: # 挂载到 Spring Cloud 的注册中⼼ address: spring-cloud://localhost
运行发布之后,会发现Nacos控制台已经有了服务注册信息,从元数据中可以看出,是dubbo注册上来的
3.2 服务消费者工程改造
pom.xml中删除OpenFeign相关内容
application.yml配置⽂件中删除和Feign、Ribbon相关的内容;代码中删除Feign客户端内容;
pom.xml添加内容和服务提供者⼀样
application.yml配置⽂件中添加dubbo相关内容
dubbo: registry: # 挂载到 Spring Cloud 注册中⼼ address: spring-cloud://localhost cloud: # 订阅服务提供⽅的应⽤列表,订阅多个服务提供者使⽤ "," 连接 subscribed-services: lagou-service-resume
同样,也配置下spring.main.allow-bean-definition-overriding=true
Controller代码改造: 用 Dubbo的 @Reference 引入接口
运行发布之后,同样会发现Nacos控制台已经有了服务注册信息
测试:http://localhost:8099/autodeliver/checkState/1545132 (我新复制了一个工程,占用端口8099)
1,jwt刷新token后原来的token还能用
2,为什么yml里不加数据库配置会报错:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured
3,怎么对不同参数的API进行统一限流
1.1 发邮件功能
端口号要改成465 否则linux上连不上邮件服务器
server: port: 8083 spring: application: name: lagou-service-email mail: #smtp服务主机 qq邮箱则为smtp.qq.com host: smtp.163.com #服务协议 protocol: smtp # 编码集 default-encoding: utf-8 #发送邮件的账户 # username: [email protected] #授权码 password: PITXFKTHAOAWQOHU test-connection: true properties: mail: smtp: auth: true socketFactory: # 端口号要改成465 否则linux上连不上邮件服务器 port: 465 class: javax.net.ssl.SSLSocketFactory fallback: false starttls: enable: true required: true