Spring Cloud 基础
微服务基础
简介
- 微服务的基本概念、设计与拆分原则
- 微服务和Spring Cloud的关系
- 微服务常见的组件和功能
- 课程查询案例基本介绍、系统架构设计和接口设计
- 分模块构建Spring Cloud 项目
- 课程服务整合,服务注册与发现
- 整合Feign实现服务间调用
- 网管集成与开发,并接入服务
- 引入服务的熔断与降级,并进行实操演练
微服务基础
-
什么是微服务
-
微服务的特点
-
微服务的优缺点
-
微服务两大门派
- SpringCloud
- Dubbo
-
微服务拆分
-
微服务扩展
-
微服务重要模块
什么是微服务
- 单体应用的痛点
- 部署效率低下
- 团队协作开发成本高
- 系统高可用性差
- 什么是服务化
- 把传统的单机应用中的本地方法调用,改造成RPC、HTTP产生的远程方法调用
- 把模块从单体应用中拆分出来
- 用户模块就可以独立开发、测试、上线、 运维,可以交由专门的团队来做,与主模块不耦合
- 一种架构风格
- 开发单个应用作为一系列小型服务的套件,其中每个服务都运行在自己的进程中,并且通过轻量级的机制实现彼此的通信,这通常是HTTP资源API
- 这些服务是围绕这业务功能构建的,并且可以通过完全自动化的部署机制进行独立部署
- 这些服务的集中式管理做到了最小化(例如docker相关技术),每一种服务都可以通过不同的编程语言进行编写,并且可以使用不同的数据存储技术
- 一系列、一部分(选用一部分适合的组件)
微服务的特点
- 组件以服务形式提供
- 产品不是项目
- 轻量级通信、独立进程
- 分散治理、去中心化治理
- 容错性设计
- 会带来组织架构的调整
微服务优缺点
-
优点:
-
服务简单、便与学习上手,相对易于维护
-
独立部署、灵活扩展
-
技术栈丰富
-
-
缺点:
- 运维成本过高
- 接口可能不匹配
- 代码可能重复
- 架构复杂度提高
微服务两大模块
-
Spring Cloud:众多子项目
-
Dubbo: 高性能、轻量级的开源java RPC 框架,它提供了三大核心能力
- 面向接口的远程方法调用
- 智能容错和负载均衡
- 服务自动注册和发现
-
Dubbo提供的能力是SpringCloud的一个子集
核心组件 | Dubbo | Spring Cloud |
---|---|---|
服务注册中心 | Zookeepper | Spring Cloud Netflix Eureka |
服务调用方式 | RPC | REST API |
服务网关 | 无 | Spring Cloud Netflix Zuul |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
- 通信协议对比
- RPC vs REST
- RPC服务提供方与调用接口依赖方式太强
- Dubbo 服务对平台敏感,难以简单复用
- 文档质量
- Dubbo 有中文文档
- Spring Cloud文档体量大,更多是偏向整合,更深入的使用方法还是需要查看其整合组件的详细文档
- 比喻:组装电脑、品牌机
- 需要根据自身的研发水平和所处的阶段选择
微服务的拆分
- 什么时候进行服务化拆分
- 第一阶段: 只要目标是快速开发和验证想法
- 进一步增加更多新特性来吸引更多的目标用户
- 同时进行开发人员超过了10人,这个时候就该考虑进行服务化拆分
- 不适合拆分的情况
- 小团队,技术基础薄弱
- 流量不高,压力小,业务变化也不大
- 对延迟很敏感的低延迟、高并发系统
- 服务化拆分的两种姿势
- 纵向拆分
- 横向拆分
- 综合业务综合分析
服务扩展
-
X轴-水平复制
-
Y轴-功能解耦
-
Z轴-数据分区
-
自动按需扩展
-
根据CPU负责程度、特定时间(比如周末)、消息中间件的队列长度、业务具体规则、预测等来解决是否扩展
-
自动分配一个新的服务实例,提高可用性
-
提高了可伸缩性(双十一之后自动减少服务器)
-
具有最佳使用率,节约成本
-
微服务重要模块
- 服务描述
- 注册中心
- 服务框架
- 负载均衡
- 熔断和降级
- 网关
总结
Spring Cloud开发课程查询功能
项目整体介绍
- Spring Cloud简介
- 项目整体设计
- 课程列表模块开发
- 课程价格模块开发
- 服务注册与发现Eureka
- 服务间调用Feign
- 负载均衡Ribbon
- 熔断器Hystrix
- 网关Zuul
- 整体测试
- 项目总结
SpringCloud核心组件介绍
-
Spring Cloud 简介
- 成熟的微服务框架,定位为开发人员提供工具,以快速构建分布式系统
-
核心组件
核心组件 Spring Cloud 服务注册中心 Spring Cloud Netflix Eureka 服务调用方式 REST API、Feign、 Ribbon 服务网关 Spring Cloud Netflix Zuul 断路器 Spring Cloud Netflix Hystrix
项目技术设计
-
项目介绍
-
接口设计
- 课程列表
- 单个课程价格
- 整合课程列表和价格
-
系统数据流向
-
数据表
新建多模块项目
课程列表模块开发
mapper类上边添加的注解
@Mapper
@Repository
public interface CourseMapper {
@Select("SELECT * FROM course WHERE valid = 1")
List<Course> findValidCourse();
}
常见错误排查
序列化时类中一定要有Setter 、Getter 和toString方法的
# 实现自动映射,Mybatis 驼峰配置
mybatis.configuration.map_underscore-to-camel-case = true
课程价格模块开发
Eureka的作用和架构
-
Eureka服务注册与发现模块
-
为什么需要服务注册与发现
- IP变化
- 难以维护
- 改进
-
架构
-
Eureka Server
-
Eureka Client
-
开发Eureka Server
-
引入Eureka
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> ... <!-- 表示SpringCloud的版本 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
配置文件
spring.application.name=eureka-server server.port=8000 eureka.instance.hostname=localhost #fetch-registry:获取注册表。不需要同步其他节点数据。 eureka.client.fetch-registry=false #register-with-eureka代表是否将自己注册到Eureka Server,默认是true。 eureka.client.register-with-eureka=false eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
-
启动注解
- 在启动类上添加
@EnableEurekaServer
- 在启动类上添加
-
进行Eureka Client改造
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
eureka.client.service-url.defaultZone=http://localhost:8000/eureka/
一些老的版本还需要在启动类上添加 @EnableEurekaClient
利用Feign实现服务间调用
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
配置文件
#openfeign消费的负载均衡后期再配
-
注解
//启动类的客户端 @EnableFeignClients
-
客户端(在调用类(模块)写接口,复制被调用服务(模块)的controller方法)
package com.bennyrhys.course.client; import com.bennyrhys.entity.Course; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import java.util.List; /** * 课程列表的Feign客户端 */ @FeignClient("course-list") public interface CourseListClient { @GetMapping("/course") List<Course> getList(); }
-
验证pom中(自动引入其他服务的依赖)
-
controller(在price服务中调用course服务的方法)
-
验证
利用Ribbon实现负载均衡
-
负载均衡的两种类型
-
客户端负载均衡(Ribbon)
-
服务端负载均衡(Nginx)
-
-
负载均衡策略
- RandomRule表示随机策略
- RoundRobinRule表示轮询策略
- ResponseTimeWeightedRule加权,根据每一个Server的平均响应时间动态加权
-
配置不同的负载均衡方式
# Ribbon.NFLoadBanlancerRuleClassName # price服务调用course服务的负载均衡设置 # openfeign消费的负载均衡 course-list.ribbon.NFLoadBanlancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
利用Hystrix实现断路器
-
比如获取用户信息卡住,但数据库的连接池一直未被释放。系统崩溃、断路器保护,某一处出现问题,保证不影响全部不可用,避免故障蔓延
-
引入依赖
<!-- 断路器 客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
配置
#断路器 客户端(默认关闭) feign.hystrix.enabled=true
-
启动注解
@EnableCircuitBreaker
-
断路器实现类CourseListClientHystrix
package com.bennyrhys.course.client; import com.bennyrhys.entity.Course; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Component; /** * 描述: 断路器实现类 */ @Component public class CourseListClientHystrix implements CourseListClient{ @Override public List<Course> getList() { List<Course> defaultCourses = new ArrayList<>(); Course course = new Course(); course.setId(1); course.setCourseId(1); course.setCourseName("默认课程"); course.setValid(1); defaultCourses.add(course); return defaultCourses; } }
-
指明调用服务的断路器类
/** * 课程列表的Feign客户端 */ @FeignClient(value = "course-list", fallback = CourseListClientHystrix.class) @Primary //防止调用服务的controller爆红线不好看 public interface CourseListClient { @GetMapping("/course") List<Course> getList(); }
-
断路器效果
整合两个服务
-
将课程列表和课程价格进行整合
-
返回实体CourseAndPrice
Integer id; Integer courseId; String name; Integer price;
-
service
@Override public List<CourseAndPrice> getCoursesAndPrice() { List<CourseAndPrice> courseAndPriceList = new ArrayList<>(); List<Course> courses = courseListClient.courseList(); for (int i = 0; i < courses.size(); i++) { Course course = courses.get(i); if (course != null) { CoursePrice coursePrice = getCoursePrice(course.getCourseId()); CourseAndPrice courseAndPrice = new CourseAndPrice(); courseAndPrice.setPrice(coursePrice.getPrice()); courseAndPrice.setName(course.getCourseName()); courseAndPrice.setId(course.getId()); courseAndPrice.setCourseId(course.getCourseId()); courseAndPriceList.add(courseAndPrice); } } return courseAndPriceList; } }
-
实现
通过网关Zuul实现路由功能
-
两个特点
-
签名校验、登录校验冗余问题
-
API网关允许将API请求(内部或外部)路由到正确地址
-
-
集成
- 把自己注册到Eureka这个注册中心
- 引入依赖
- 配置路由地址
-
新建mudle模块sourse-zuul
-
引入依赖
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
配置文件
spring.application.name=course-gateway server.port=9000 logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} mybatis.configuration.map-underscore-to-camel-case=true eureka.client.service-url.defaultZone=http://localhost:8000/eureka/ #zuul.prefix=/bennyrhys zuul.routes.course-list.path=/list/** zuul.routes.course-list.service-id=course-list zuul.routes.course-price.path=/price/** zuul.routes.course-price.service-id=course-price
-
启动类 注解
package com.bennyrhys.course; import org.springframework.boot.SpringApplication; import org.springframework.cloud.client.SpringCloudApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; /** * 描述: 网关启动类 */ @EnableZuulProxy @SpringCloudApplication public class ZuulGatewayApplication { public static void main(String[] args) { SpringApplication.run(ZuulGatewayApplication.class, args); } }
-
效果
实现网关过滤器
-
pre 过滤器在路由请求之前运行
-
route 过滤器可以处理请求的实际路由
-
post 路由请求后运行过滤器
-
error 如果在处理请求的过程中发生错误,则过滤器将运行
-
过滤前
package com.bennyrhys.course.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; /** * 描述: 记录请求时间 */ @Component public class PreRequestFilter extends ZuulFilter { @Override public String filterType() { //过滤器的类型 return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { //是否启用过滤器 return true; } @Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); currentContext.set("startTime", System.currentTimeMillis()); System.out.println("过滤器已经记录时间"); return null; } }
-
过滤后
package com.bennyrhys.course.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.cache.annotation.Cacheable; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; /** * 描述: 请求处理后的过滤器 */ @Component public class PostRequestFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext currentContext = RequestContext.getCurrentContext(); Long startTime = (Long) currentContext.get("startTime"); long duration = System.currentTimeMillis() - startTime; String requestURI = currentContext.getRequest().getRequestURI(); System.out.println("uri:" + requestURI + ",处理时长:" + duration); return null; } }