网站实名制注册怎么做,天元建设集团有限公司审计项目,南昌网站建设怎么样,企业宣传视频模板免费下载在此感谢黑马程序员的SpringCloud课程 所有笔记、生活分享首发于个人博客 想要获得最佳的阅读体验#xff08;无广告且清爽#xff09;#xff0c;请访问本篇笔记 认识微服务
随着互联网行业的发展#xff0c;对服务的要求也越来越高#xff0c;服务架构也从单体架构逐渐… 在此感谢黑马程序员的SpringCloud课程 所有笔记、生活分享首发于个人博客 想要获得最佳的阅读体验无广告且清爽请访问本篇笔记 认识微服务
随着互联网行业的发展对服务的要求也越来越高服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢
单体架构 单体架构将业务的所有功能集中在一个项目中开发打成一个包部署。 单体架构的优缺点
优点 架构简单、部署成本低
缺点 耦合度高维护困难、升级困难
分布式架构 分布式架构根据业务功能对系统做拆分每个业务功能模块作为独立项目开发称为一个服务。 分布式架构的优缺点
优点 降低服务耦合、有利于服务升级和拓展
缺点 服务调用关系错综复杂
分布式架构虽然降低了服务耦合但是服务拆分时也有很多问题需要思考
服务拆分的粒度如何界定服务之间如何调用服务的调用关系如何管理
人们需要制定一套行之有效的标准来约束分布式架构。
微服务
微服务的架构特征
单一职责微服务拆分粒度更小每一个服务都对应唯一的业务能力做到单一职责自治团队独立、技术独立、数据独立独立部署和交付面向服务服务提供统一标准的接口与语言和技术无关隔离性强服务调用做好隔离、容错、降级避免出现级联问题 微服务的上述特性其实是在给分布式架构制定一个标准进一步降低服务之间的耦合度提供服务的独立性和灵活性。做到高内聚低耦合。
因此可以认为微服务是一种经过良好架构设计的分布式架构方案 。
其中在Java领域最引人注目的就是SpringCloud提供的方案了。
SpringCloud
SpringCloud是目前国内使用最广泛的微服务框架。官网地址https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件并基于SpringBoot实现了这些组件的自动装配从而提供了良好的开箱即用体验。
其中常见的组件包括 另外SpringCloud 底层是依赖于SpringBoot的并且有版本的兼容关系如下
Release TrainBoot Version2020.0.x aka llford2.4.xHoxton2.2.x,2.3.x (Starting with SR5)Greenwich2.1.xFinchley2.0.xEdgware1.5.xDalston1.5.X
本文的学习版本是Hoxton.SR10因此对应的是SpringBoot版本是2.3.x
总结
单体架构简单方便高度耦合扩展性差适合小型项目。例如学生管理系统分布式架构松耦合扩展性好但架构复杂难度大。适合大型互联网项目。例如京东、淘宝微服务一种更好的分布式架构方案 优点拆分力度更小、服务更独立、耦合度更低缺点架构非常复杂运维、监控、部署难度提高 SpringCloud 是微服务架构的一站式解决方案集成了各种优秀的微服务功能组件
服务拆分和远程调用
服务拆分原则
这里我总结了微服务拆分时的几个原则
不同微服务不要重复开发相同业务微服务数据独立不要访问其它微服务的数据库微服务可以将自己的业务暴露为接口供其它微服务调用 服务拆分示例
cloud-demo父工程管理依赖 order-service订单微服务负责订单相关业务user-service用户微服务负责用户相关业务 需求 订单微服务和用户微服务必须有各自的数据库相互独立订单服务和用户服务都对外暴露Restful的接口订单服务如果需要查询用户信息只能调用用户服务的Restful接口不能查询用户数据库
导入SQL语句
Order表
CREATE DATABASE cloud_order;
USE cloud_order;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS 0;-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS tb_order;
CREATE TABLE tb_order (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 订单id,user_id bigint(20) NOT NULL COMMENT 用户id,name varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 商品名称,price bigint(20) NOT NULL COMMENT 商品价格,num int(10) NULL DEFAULT 0 COMMENT 商品数量,PRIMARY KEY (id) USING BTREE,UNIQUE INDEX username(name) USING BTREE
) ENGINE InnoDB AUTO_INCREMENT 109 CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Compact;-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO tb_order VALUES (101, 1, Apple 苹果 iPhone 12 , 699900, 1);
INSERT INTO tb_order VALUES (102, 2, 雅迪 yadea 新国标电动车, 209900, 1);
INSERT INTO tb_order VALUES (103, 3, 骆驼CAMEL休闲运动鞋女, 43900, 1);
INSERT INTO tb_order VALUES (104, 4, 小米10 双模5G 骁龙865, 359900, 1);
INSERT INTO tb_order VALUES (105, 5, OPPO Reno3 Pro 双模5G 视频双防抖, 299900, 1);
INSERT INTO tb_order VALUES (106, 6, 美的Midea) 新能效 冷静星II , 544900, 1);
INSERT INTO tb_order VALUES (107, 2, 西昊/SIHOO 人体工学电脑椅子, 79900, 1);
INSERT INTO tb_order VALUES (108, 3, 梵班FAMDBANN休闲男鞋, 31900, 1);SET FOREIGN_KEY_CHECKS 1;Use表
CREATE DATABASE cloud_user;
USE cloud_user;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS 0;-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS tb_user;
CREATE TABLE tb_user (id bigint(20) NOT NULL AUTO_INCREMENT,username varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 收件人,address varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 地址,PRIMARY KEY (id) USING BTREE,UNIQUE INDEX username(username) USING BTREE
) ENGINE InnoDB AUTO_INCREMENT 109 CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Compact;-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO tb_user VALUES (1, 柳岩, 湖南省衡阳市);
INSERT INTO tb_user VALUES (2, 文二狗, 陕西省西安市);
INSERT INTO tb_user VALUES (3, 华沉鱼, 湖北省十堰市);
INSERT INTO tb_user VALUES (4, 张必沉, 天津市);
INSERT INTO tb_user VALUES (5, 郑爽爽, 辽宁省沈阳市大东区);
INSERT INTO tb_user VALUES (6, 范兵兵, 山东省青岛市);SET FOREIGN_KEY_CHECKS 1;导入demo
导入黑马提供好的demo里面包含了order-service和user-service将其配置文件中的数据库修改为自己的配置随后将这两个服务启动开始我们的调用案例
实现远程调用案例
在order-service中的web包下有一个OrderController是根据id查询订单的接口
RestController
RequestMapping(order)
public class OrderController {Autowiredprivate OrderService orderService;GetMapping({orderId})public Order queryOrderByUserId(PathVariable(orderId) Long orderId) {// 根据id查询订单并返回return orderService.queryOrderById(orderId);}
}我们打开浏览器访问http://localhost:8080/order/101 是可以查询到数据的但此时的user是null
{id: 101,price: 699900,name: Apple 苹果 iPhone 12 ,num: 1,userId: 1,user: null
}在user-service中的web包下也有一个UserController其中包含一个根据id查询用户的接口
Slf4j
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;/**我们打开浏览器访问http://localhost:8081/user/1 查询到的数据如下
{id: 1,username: 柳岩,address: 湖南省衡阳市
}案例需求 修改order-service中的根据id查询订单业务要求在查询订单的同时根据订单中包含的userId查询出用户信息一并返回 因此我们需要在order-service 中向user-service 发起一个http 请求调用http://localhost:8081/user/{userId} 这个接口。 大概步骤如下 注册一个RestTemplate 的实例到Spring 容器修改order-service 服务中的OrderService 类中的queryOrderById 方法根据Order 对象中的userId 查询User将查询到的User 填充到Order 对象一并返回
注册RestTemplate
首先我们在order-service服务中的OrderApplication启动类中注册RestTemplate实例
MapperScan(cn.itcast.order.mapper)
SpringBootApplication
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}实现远程调用
修改order-service服务中的queryById方法
Service
public class OrderService {Autowiredprivate OrderMapper orderMapper;Autowiredprivate RestTemplate restTemplate;public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.findById(orderId);// 2. 远程查询User// 2.1 url地址这里的url是写死的后面会改进String url http://localhost:8081/user/ order.getUserId();// 2.2 发起调用User user restTemplate.getForObject(url, User.class);// 3. 存入orderorder.setUser(user);// 4.返回return order;}
}再次访问http://localhost:8080/order/101 这次就能看到User数据了
{id: 101,price: 699900,name: Apple 苹果 iPhone 12 ,num: 1,userId: 1,user: {id: 1,username: 柳岩,address: 湖南省衡阳市}
}提供者与消费者
在服务调用关系中会有两个不同的角色 服务提供者一次业务中被其他微服务调用的服务提供接口给其他微服务服务消费者一次业务中调用其他微服务的服务调用其他微服务提供的接口 但是服务提供者与服务消费者的角色并不是绝对的而是相对于业务而言如果服务A调用了服务B而服务B又调用的服务C那么服务B的角色是什么 对于A调用B的业务而言A是服务消费者B是服务提供者对于B调用C的业务而言B是服务消费者C是服务提供者 因此服务B既可以是服务提供者也可以是服务消费者
Eureka注册中心
假如我们的服务提供者user-service提供了三个实例占用的分别是8081、8082、8083端口那我们来思考几个问题 问题一order-service在发起远程调用的时候该如何得知user-service实例的ip地址和端口问题二有多个user-service实例地址order-service调用时该如何选择问题三order-service如何得知某个user-service实例是否健康是不是已经宕机
Eureka的结构和作用 这些问题都需要利用SpringCloud中的注册中心来解决其中最广为人知的注册中心就是Eureka其结构如下 那么现在来回答之前的各个问题 问题一 order-service如何得知user-service实例地址 获取地址信息流程如下 user-service服务实例启动后将自己的信息注册到eureka-serverEureka服务端这个叫服务注册eureka-server保存服务名称到服务实例地址列表的映射关系order-service根据服务名称拉取实例地址列表这个叫服务发现或服务拉取 问题二order-service如何从多个user-service实例中选择具体的实例 order-service从实例列表中利用负载均衡算法选中一个实例地址向该实例地址发起远程调用 问题三order-service如何得知某个user-service实例是否依然健康是不是已经宕机 user-service会每隔一段时间默认30秒向eureka-server发起请求报告自己的状态成为心跳当超过一定时间没有发送心跳时eureka-server会认为微服务实例故障将该实例从服务列表中剔除order-service拉取服务时就能将该故障实例排除了 注意一个微服务即可以是服务提供者又可以是服务消费者因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端 因此我们接下来动手实践的步骤包括 搭建注册中心 搭建EurekaServer 服务注册 将user-service、order-service都注册到eureka 服务发现 在order-service中完成服务拉取然后通过负载均衡挑选一个服务实现远程调用
搭建eureka-server
首先我们注册中心服务端eureka这必须是一个独立的微服务
创建eureka-server服务
在cloud-demo父工程下创建一个子模块这里就直接创建一个maven项目就好了然后填写服务信息
引入eureka依赖
引入SpringCloud为eureka提供的starter依赖
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-server/artifactId
/dependency编写启动类
给eureka-server服务编写一个启动类一定要添加一个EnableEurekaServer注解开启eureka的注册中心功能
SpringBootApplication
EnableEurekaServer
public class EurekaApplication {public static void main(String[] args) {SpringApplication.run(EurekaApplication.class);}
}编写配置文件
编写一个application.yml文件内容如下为什么也需要配置eureka的服务名称呢 eureka也会将自己注册为一个服务
server:port: 10086 ## 服务端口
spring:application:name: eureka-server ## eureka的服务名称
eureka:client:service-url: ## eureka的地址信息defaultZone: http://127.0.0.1:10086/eureka启动服务 如果出现报错请检查你的JDK版本是否是17是17请降级为11就能正常启动! 启动微服务然后在浏览器访问 http://localhost:10086/ 看到如下结果就是成功了
服务注册
下面我们将user-service注册到eureka-server中去
引入依赖
在user-service的pom.xml文件中引入下面的eurekaclient依赖
!-- eureka-client --
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependency配置文件
在user-service中修改application.yml文件添加服务名称、eureka地址
spring:application:name: userservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka启动多个user-service实例
为了演示一个服务有多个实例的场景我们添加一个SpringBoot的启动配置再启动一个user-service其操作步骤就是复制一份user-service的配置name配置为UserApplication2同时也要配合VM选项修改端口号-Dserver.port8082点击确定之后在IDEA的服务选项卡中就会出现两个user-service启动配置一个端口是8081一个端口是8082之后我们按照相同的方法配置orderservice并将两个user-service和一个order-service都启动然后查看eureka-server管理页面发现服务确实都启动了而且user-service有两个 服务发现
下面我们将order-service的逻辑修改向eureka-server拉取user-service的信息实现服务发现
引入依赖
之前说过服务发现、服务注册统一都封装在eureka-client依赖因此这一步与服务注册时一致在order-service的pom.xml文件中引入eureka-client依赖
!-- eureka-client --
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-netflix-eureka-client/artifactId
/dependencyspring:application:name: orderservice
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eureka服务拉取和负载均衡
最后我们要去eureka-server中拉取user-service服务的实例列表并实现负载均衡不过这些操作并不需要我们来做是需要添加一些注解即可在order-service的OrderApplication中给RestTemplate这个Bean添加一个LoadBalanced注解
MapperScan(cn.itcast.order.mapper)
SpringBootApplication
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}BeanLoadBalancedpublic RestTemplate restTemplate() {return new RestTemplate();}
}修改order-service服务中的OrderService类中的queryOrderById方法修改访问路径用服务名来代替ip、端口
public Order queryOrderById(Long orderId) {// 1.查询订单Order order orderMapper.findById(orderId);// 2. 远程查询User// 2.1 url地址用user-service替换了localhost:8081String url http://user-service/user/ order.getUserId();// 2.2 发起调用User user restTemplate.getForObject(url, User.class);// 3. 存入orderorder.setUser(user);// 4.返回return order;
}Spring会自动帮我们从eureka-server端根据user-service这个服务名称获取实例列表然后完成负载均衡
小结
搭建EurekaServer 引入eureka-server依赖添加EnableEurekaServer注解在application.yml中配置eureka地址 服务注册 引入eureka-client依赖在application.yml中配置eureka地址 服务发现 引入eureka-client依赖在application.yml中配置eureka地址在RestTemplate添加LoadBalanced注解用服务提供者的服务名称远程调用
Ribbon负载均衡
在这个小节我们来说明LoadBalanced注解是怎么实现的负载均衡功能
负载均衡原理
SpringCloud底层其实是利用了一个名为Ribbon的组件来实现负载均衡功能的 那么我们明明发出的请求是http://userservice/user/1 怎么变成了http://localhost:8080/user/1 的呢
源码跟踪
为什么我们只输入了service名称就可以访问了呢之前还得获取ip和端口答案显然是有人帮我们根据service名称获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor这个类会第RestTemplate的请求进行拦截然后从Eureka根据服务id获取服务列表随后利用负载均衡算法得到真实的服务地址信息替换服务id那下面我们来进行源码跟踪 LoadBalancerInterceptor 代码如下 public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {private LoadBalancerClient loadBalancer;private LoadBalancerRequestFactory requestFactory;public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {this.loadBalancer loadBalancer;this.requestFactory requestFactory;}public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));}public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {URI originalUri request.getURI();String serviceName originalUri.getHost();Assert.state(serviceName ! null, Request URI does not contain a valid hostname: originalUri);return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));}
}可以看到这里的intercept方法拦截了用户的HttpRequest请求然后做了几件事
request.getURI()获取请求uri本例中就是 http://user-service/user/8originalUri.getHost()获取uri路径的主机名其实就是服务iduser-servicethis.loadBalancer.execute()处理服务id和用户请求。
这里的this.loadBalancer是LoadBalancerClient类型我们继续跟入。
LoadBalancerClient
继续跟入execute方法 代码是这样的
getLoadBalancer(serviceId)根据服务id获取ILoadBalancer而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。getServer(loadBalancer)利用内置的负载均衡算法从服务列表中选择一个。本例中可以看到获取了8082端口的服务
放行后再次访问并跟踪发现获取的是8081果然实现了负载均衡。 负载均衡策略IRule
在刚才的代码中可以看到获取服务是通过一个getServer的方法来做负载均衡我们继续跟入会发现这样一段代码
public Server chooseServer(Object key) {if (this.counter null) {this.counter this.createCounter();}this.counter.increment();if (this.rule null) {return null;} else {try {return this.rule.choose(key);} catch (Exception var3) {logger.warn(LoadBalancer [{}]: Error choosing server for key {}, new Object[]{this.name, key, var3});return null;}}
}在try/catch代码块中进行服务选择的是this.rule.choose(key)那我们看看这个rule是谁 这里的rule默认值是一个RoundRobinRule也就是轮询那么到这里整个负载均衡的流程我们就清楚了
总结
SpringCloudRibbon的底层采用了一个拦截器拦截了RestTemplate发出的请求对地址做了修改用一幅图来总结一下 整个流程如下 拦截我们的RestTemplate请求http://user-service/user/1RibbonLoadBalancerClient会从请求url中获取服务名称也就是user-serviceDynamicServerListLoadBalancer根据user-service到eureka拉取服务列表eureka返回列表localhost:8081、localhost:8082IRule利用内置负载均衡规则从列表中选择一个例如localhost:8081RibbonLoadBalancerClient修改请求地址用localhost:8081替代user-service得到http://localhost:8081/user/1 发起真实请求
负载均衡策略
负载均衡策略
负载均衡的规则都定义在IRule接口中而IRule有很多不同的实现类 不同规则的含义如下
内置负载均衡规则类规则描述RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。AvailabilityFilteringRule对以下两种服务器进行忽略 1在默认情况下这台服务器如果3次连接失败这台服务器就会被设置为“短路”状态。短路状态将持续30秒如果再次连接失败短路的持续时间就会几何级地增加。 2并发数过高的服务器。如果一个服务器的并发连接数过高配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限可以由客户端的…ActiveConnectionsLimit属性进行配置。WeightedResponseTimeRule为每一个服务器赋予一个权重值。服务器响应时间越长这个服务器的权重就越小。这个规则会随机选择服务器这个权重值会影响服务器的选择。ZoneAvoidanceRule以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。BestAvailableRule忽略那些短路的服务器并选择并发数较低的服务器。RandomRule随机选择一个可用的服务器。RetryRule重试机制的选择逻辑
默认的实现就是ZoneAvoidanceRule是一种轮询方案
自定义负载均衡策略 通过定义IRule实现可以修改负载均衡规则有两种方式 代码方式在order-service中的OrderApplication类中定义一个IRule此种方式定义的负载均衡规则对所有微服务均有效 Bean
public IRule randomRule(){return new RandomRule();
}配置文件方式(针对某个服务)在order-service中的application.yml文件中添加新的配置也可以修改规则 user-service: ## 给某个微服务配置负载均衡规则这里是user-service服务ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule ## 负载均衡规则 注意一般使用默认的负载均衡规则不做修改 饥饿加载
Ribbon默认是采用懒加载即第一次访问时才会去创建LoadBalanceClient请求时间会很长而饥饿加载在则会在项目启动时创建降低第一次访问的耗时通过下面配置开启饥饿加载
ribbon:eager-load:enabled: true ## 开启饥饿加载clients: user-service ## 指定对user-service这个服务进行饥饿加载可以指定多个服务小结 Ribbon负载均衡规则 规则接口是IRule默认实现是ZoneAvoidanceRule根据zone选择服务列表然后轮询 负载均衡自定义方式 代码方式配置灵活但修改时需要重新打包发布配置方式直观方便无需重新打包发布但是无法做全局配置只能指定某一个微服务 饥饿加载 开启饥饿加载 enable: true指定饥饿加载的微服务名称可以配置多个 clients: - user-service- xxx-service Nacos注册中心
国内公司一般都推崇阿里巴巴的技术比如注册中心SpringCloud Alibaba也推出了一个名为Nacos的注册中心
认识和安装Nacos
Nacos是阿里巴巴的产品现在是SpringCloud中的一个组件相比于Eureka功能更加丰富在国内受欢迎程度较高在Nacos的GitHub页面提供有下载链接可以下载编译好的Nacos服务端或者源代码 GitHub主页https://github.com/alibaba/nacosGitHub的Release下载页https://github.com/alibaba/nacos/releases 下载好了之后将文件解压到非中文路径下的任意目录目录说明 bin启动脚本conf配置文件 Nacos的默认端口是8848如果你电脑上的其它进程占用了8848端口请先尝试关闭该进程。 如果无法关闭占用8848端口的进程也可以进入nacos的conf目录修改配置文件application.properties中的server.port Nacos的启动非常简单进入bin目录打开cmd窗口执行以下命令即可
startup.cmd -m standalone #单机启动之后在浏览器访问http://localhost:8848/nacos 即可默认的登录账号和密码都是nacos
服务注册到Nacos
Nacos是SpringCloudAlibaba的组件而SpringCloud Alibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos与使用Eureka对于微服务来说并没有太大区别主要差异在于 依赖不同服务地址不同
引入依赖
在cloud-demo父工程的pom.xml文件中引入SpringCloudAlibaba的依赖
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-alibaba-dependencies/artifactIdversion2.2.6.RELEASE/versiontypepom/typescopeimport/scope
/dependency然后在user-service和order-service中的pom文件引入nacos-discovery依赖
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency注意同时也要将eureka的依赖注释/删除掉 配置Nacos地址
在user-service和order-service的application.yml中添加Nacos地址
spring:cloud:nacos:server-addr: localhost:8848注意同时也要将eureka的地址注释掉
重启服务
重启微服务后登录nacos的管理页面可以看到微服务信息 服务分级存储模型
一个服务可以有多个实例例如我们的user-service可以有 127.0.0.1:8081127.0.0.1:8082127.0.0.1:8083 假如这些实例分布于全国各地的不同机房例如 127.0.0.1:8081在杭州机房127.0.0.1:8082在杭州机房127.0.0.1:8083在上海机房 Nacos就将在同一机房的实例划分为一个集群也就是说user-service是服务一个服务可以包含多个集群例如在杭州上海每个集群下可以有多个实例形成分级模型微服务相互访问时应该尽可能访问同集群实例因为本地访问速度更快房本集群内不可用时才去访问其他集群 例如杭州机房内的order-service应该有限访问同机房的user-service若无法访问则去访问上海机房的user-service
给user-service配置集群
修改user-service的application.yml文件添加集群配置
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ ## 集群名称杭州重启两个user-service实例之后我们再复制一个user-service的启动配置端口号设为8083之后修改application.yml文件将集群名称设为上海之后启动该服务
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: SH ## 集群名称上海那么我们现在就启动了两个集群名称为HZ的user-service一个集群名称为SH的user-service在Nacos控制台看到如下结果 Nacos服务分级存储模型 一级是服务例如user-service二级是集群例如杭州或上海三级是实例例如杭州机房的某台部署了user-service的服务器 如何设置实例的集群属性 修改application.yml文件添加spring.cloud.nacos.discovery.cluster-name属性即可
同集群优先的负载均衡 默认的ZoneAvoidanceRule并不能根据同集群优先来实现负载均衡 因此Nacos中提供了一个NacosRule的实现可以优先从同集群中挑选实例 给order-service配置集群信息修改其application.yml文件将集群名称配置为HZ spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZ ## 集群名称杭州修改负载均衡规则 user-service: ## 给某个微服务配置负载均衡规则这里是user-service服务ribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule ## 负载均衡规则那我们现在访问http://localhost:8080/order/101 同时观察三个user-service的日志输出集群名称为HZ的两个user-service可以看到日志输出而集群名称为SH的user-service则看不到日志输出 那我们现在将集群名称为HZ的两个user-service服务停掉那么现在访问http://localhost:8080/order/101 则集群名称为SH的user-service会输出日志 NacosRule负载均衡策略 优先选择同集群服务实例列表本地集群找不到提供者才去其他集群寻找并且会报警告确定了可用实例列表后再采用随机负载均衡挑选实例
权重配置
实际部署中肯定会出现这样的场景 服务器设备性能有差异部分实例所在的机器性能较好而另一些较差我么你希望性能好的机器承担更多的用户请求但默认情况下NacosRule是统计群内随机挑选不会考虑机器性能的问题 因此Nacos提供了权重配置来控制访问频率权重越大则访问频率越高在Nacos控制台找到user-service的实例列表点击编辑即可以修改权重 注意若权重修改为0则该实例永远不会被访问 我们可以将某个服务的权重修改为0然后进行更新然后也不会影响到用户的正常访问别的服务集群之后我们可以给更新后的该服务设置一个很小的权重这样就会有一小部分用户来访问该服务测试该服务是否稳定类似于灰度测试 环境隔离
Nacos提供了namespace来实现环境隔离功能 nacos中可以有多个namespacenamespace下可以由group、service等不同的namespace之间相互隔离例如不同的namespace的服务互相不可见
创建namespace
默认情况下所有的service、data、group都是在同一个namespace名为public我们点击命名空间 - 新建命名空间 - 填写表单可以创建一个新的namespace 给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现例如修改order-service的application.yml文件
spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: f5c56ca7-1bd6-40a4-b5af-75c1ff283594 ## 命名空间填上图中的命名空间ID重启order-service后访问Nacos控制台可以看到下面的结果此时访问order-service因为namespace不同会导致找不到user-service若访问http://localhost:8080/order/101 则会报错
Nacos和Eureka的区别
Nacos的服务实例可以分为两种类型 临时实例如果实例宕机超过一定时间会从服务列表剔除默认的类型非临时实例如果实例宕机不会从服务列表剔除也可以叫永久实例 配置一个服务实例为永久实例
spring:cloud:nacos:discovery:ephemeral: false ## 设置为非临时实例Nacos和Eureka整体结构类似服务注册、服务拉取、心跳等待但是也存在一些差异
Nacos与Eureka的共同点 都支持服务注册和服务拉取都支持服务提供者心跳方式做健康监测 Nacos与Eureka的区别 Nacos支持服务端主动检测提供者状态临时实例采用心跳模式非临时实例采用主动检测模式但是对服务器压力比较大不推荐临时实例心跳不正常会被剔除非临时实例则不会被剔除Nacos支持服务列表变更的消息推送模式服务列表更新更及时Nacos集群默认采用AP方式当集群中存在非临时实例时采用CP模式Eureka采用AP方式
Nacos配置管理
Nacos除了可以做注册中心同样还可以做配置管理来使用
统一配置管理
当微服务部署的实例越来越多达到数十、数百时逐个修改微服务配置就会让人抓狂而且容易出错所以我们需要一种统一配置管理方案可以集中管理所有实例的配置Nacos一方面可以将配置集中管理另一方面可以在配置变更时及时通知微服务实现配置的热更新
在Nacos中添加配置文件
如何在Nacos中管理配置呢 配置列表 - 点击右侧加号 在弹出的表单中填写配置信息
pattern:dateformat: yyyy-MM-dd HH:mm:ss注意只有需要热更新的配置才有放到Nacos管理的必要基本不会变更的一些配置还是保存到微服务本地比较好例如数据库连接配置等 从微服务拉取配置 微服务要拉取Nacos中管理的配置并且与本地的application.yml配置合并才能完成项目启动 但如果尚未读取application.yml又如何得知Nacos地址呢 Spring引入了一种新的配置文件bootstrap.yml文件会在application.yml之前被读取流程如下 项目启动加载bootstrap.yml文件获取Nacos地址配置文件id根据配置文件id读取Nacos中的配置文件读取本地配置文件application.yml与Nacos拉取到的配置合并创建Spring容器加载bean 引入nacos-config依赖 首先在user-service服务中引入nacos-config的客户端依赖 !--nacos配置管理依赖--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactId
/dependency添加bootstrap.yml 然后在user-service中添加一个bootstrap.yml文件内容如下 spring:application:name: user-service ## 服务名称profiles:active: dev #开发环境这里是dev cloud:nacos:server-addr: localhost:8848 ## Nacos地址config:file-extension: yaml ## 文件后缀名这里会根据spring.cloud.nacos.server-addr获取Nacos地址再根据${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id来读取配置。在本例中就是读取user-service-dev.yaml测试是否真的读取到了我们在user-service的UserController中添加业务逻辑读取nacos中的配置信息pattern.dateformat配置 Value(${pattern.dateformat})
private String dateformat;GetMapping(/test)
public String test() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}打开浏览器访问http://localhost:8081/user/test 看到如下结果则说明确实读取到了配置信息 配置热更新
我们最终的目的是修改Nacos中的配置后微服务中无需重启即可让配置生效也就是配置热更新要实现配置热更新可以使用两种方式
方式一
在Value注入的变量类上添加注解RefreshScope刷新作用域
Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Value(${pattern.dateformat})private String dateformat;GetMapping(/test)public String test() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));}
}测试是否热更新 启动服务打开浏览器访问http://localhost:8081/user/test 由于我们之前配置的dateformat是yyyy-MM-dd MM:hh:ss所以看到的日期格式为2024-05-30 18:42:39那我们现在直接在Nacos中编辑配置信息并保存 pattern:dateformat: yyyy年MM月dd日 HH:mm:ss无需重启服务器直接刷新页面看到的日期格式为2024年05月30日18:50:07说明确实是热更新
方式二
使用ConfigurationProperties注解代替Value注解在user-service服务中添加一个类读取pattern.dateformat属性
Component
Data
ConfigurationProperties(prefix pattern)
public class PatternProperties {private String dateformat;
}在UserController中用这个类来代替Value
Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Autowiredprivate PatternProperties patternProperties;GetMapping(/test)public String test() {return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));}
}使用同样的方法进行测试这里就不赘述了
配置共享
其实微服务启动时会去Nacos读取多个配置文件例如 [spring.application.name]-[spring.profiles.active].yaml例如user-service-dev.yaml[spring.application.name].yaml例如userservice.yaml 而[spring.application.name].yaml不包含环境因此可以被多个环境共享那下面我们通过案例来测试配置共享
添加一个环境共享配置
我们在Nacos中添加一个Data ID为user-service.yml文件编写的配置内容如下
pattern:envSharedValue: 多环境共享属性值修改user-service-dev.yml文件
pattern:dateformat: yyyy/MM/dd HH:mm:ssenv: user-service开发环境配置在user-service中读取共享配置
修改我们的PatternProperties类添加envSharedValue和env属性
Component
Data
ConfigurationProperties(prefix pattern)
public class PatternProperties {private String dateformat;private String envSharedValue;private String env;
}同时修改UserController添加一个方法
Slf4j
RestController
RequestMapping(/user)
RefreshScope
public class UserController {Autowiredprivate PatternProperties patternProperties;GetMapping(/prop)public PatternProperties prop(){return patternProperties;}
}修改UserApplication2的启动项改变其profile值为test改变环境同时新建一个user-service-test.yml配置
pattern:dateformat: yyyy-MM-dd HH:mm:ssenv: user-service测试环境配置那现在我们的UserApplication加载的是user-service-dev.yml和user-service.yml这两个配置文件我们的UserApplication2加载的是user-service-test.yml和user-service.yml这两个配置文件启动这两个服务打开浏览器分别访问http://localhost:8081/user/prop 和http://localhost:8082/user/prop看到的结果如下
//dev 环境下
{dateformat: yyyy-MM-dd HH:mm:ss,envSharedValue: 多环境共享属性值,env: user-service开发环境配置
}//test 环境下
{dateformat: yyyy-MM-dd HH:mm:ss,envSharedValue: 多环境共享属性值,env: user-service测试环境配置
}可以看出不管是dev还是test环境都读取到了envSharedValue这个属性的值且dev和test也都有自己特有的属性值
配置共享的优先级
当Nacos、服务本地同时出现相同属性时优先级也有高低之分服务名-profile.yaml 独有 服务名.yaml共享 本地配置 例user-service-dev.yaml user-service.yaml application.yaml
搭建Nacos集群
集群结构图 Nacos生产环境下一定要部署为集群状态 官方给出的Nacos集群图 其中包含3个Nacos节点然后一个负载均衡器代理3个Nacos。这里的负载均衡器可以使用Nginx。 我们计划的集群结构 3个Nacos节点的地址
节点ipportnacos1192.168.150.18845nacos2192.168.150.18846nacos3192.168.150.18847
搭建集群 搭建集群的基本步骤 搭建数据库初始化数据库表结构 CREATE DATABASE IF NOT EXISTS nacos_config;
USE nacos_config;
CREATE TABLE config_info (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT id,
data_id VARCHAR(255) NOT NULL COMMENT data_id,
group_id VARCHAR(255) DEFAULT NULL,
content LONGTEXT NOT NULL COMMENT content,
md5 VARCHAR(32) DEFAULT NULL COMMENT md5,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 修改时间,
src_user TEXT COMMENT source user,
src_ip VARCHAR(50) DEFAULT NULL COMMENT source ip,
app_name VARCHAR(128) DEFAULT NULL,
tenant_id VARCHAR(128) DEFAULT COMMENT 租户字段,
c_desc VARCHAR(256) DEFAULT NULL,
c_use VARCHAR(64) DEFAULT NULL,
effect VARCHAR(64) DEFAULT NULL,
type VARCHAR(64) DEFAULT NULL,
c_schema TEXT,
PRIMARY KEY (id),
UNIQUE KEY uk_configinfo_datagrouptenant (data_id,group_id,tenant_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENTconfig_info;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 config_info_aggr */
/******************************************/
CREATE TABLE config_info_aggr (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT id,
data_id VARCHAR(255) NOT NULL COMMENT data_id,
group_id VARCHAR(255) NOT NULL COMMENT group_id,
datum_id VARCHAR(255) NOT NULL COMMENT datum_id,
content LONGTEXT NOT NULL COMMENT 内容,
gmt_modified DATETIME NOT NULL COMMENT 修改时间,
app_name VARCHAR(128) DEFAULT NULL,
tenant_id VARCHAR(128) DEFAULT COMMENT 租户字段,
PRIMARY KEY (id),
UNIQUE KEY uk_configinfoaggr_datagrouptenantdatum (data_id,group_id,tenant_id,datum_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENT增加租户字段;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 config_info_beta */
/******************************************/
CREATE TABLE config_info_beta (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT id,
data_id VARCHAR(255) NOT NULL COMMENT data_id,
group_id VARCHAR(128) NOT NULL COMMENT group_id,
app_name VARCHAR(128) DEFAULT NULL COMMENT app_name,
content LONGTEXT NOT NULL COMMENT content,
beta_ips VARCHAR(1024) DEFAULT NULL COMMENT betaIps,
md5 VARCHAR(32) DEFAULT NULL COMMENT md5,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 修改时间,
src_user TEXT COMMENT source user,
src_ip VARCHAR(50) DEFAULT NULL COMMENT source ip,
tenant_id VARCHAR(128) DEFAULT COMMENT 租户字段,
PRIMARY KEY (id),
UNIQUE KEY uk_configinfobeta_datagrouptenant (data_id,group_id,tenant_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENTconfig_info_beta;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 config_info_tag */
/******************************************/
CREATE TABLE config_info_tag (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT id,
data_id VARCHAR(255) NOT NULL COMMENT data_id,
group_id VARCHAR(128) NOT NULL COMMENT group_id,
tenant_id VARCHAR(128) DEFAULT COMMENT tenant_id,
tag_id VARCHAR(128) NOT NULL COMMENT tag_id,
app_name VARCHAR(128) DEFAULT NULL COMMENT app_name,
content LONGTEXT NOT NULL COMMENT content,
md5 VARCHAR(32) DEFAULT NULL COMMENT md5,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 修改时间,
src_user TEXT COMMENT source user,
src_ip VARCHAR(50) DEFAULT NULL COMMENT source ip,
PRIMARY KEY (id),
UNIQUE KEY uk_configinfotag_datagrouptenanttag (data_id,group_id,tenant_id,tag_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENTconfig_info_tag;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 config_tags_relation */
/******************************************/
CREATE TABLE config_tags_relation (
id BIGINT(20) NOT NULL COMMENT id,
tag_name VARCHAR(128) NOT NULL COMMENT tag_name,
tag_type VARCHAR(64) DEFAULT NULL COMMENT tag_type,
data_id VARCHAR(255) NOT NULL COMMENT data_id,
group_id VARCHAR(128) NOT NULL COMMENT group_id,
tenant_id VARCHAR(128) DEFAULT COMMENT tenant_id,
nid BIGINT(20) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (nid),
UNIQUE KEY uk_configtagrelation_configidtag (id,tag_name,tag_type),
KEY idx_tenant_id (tenant_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENTconfig_tag_relation;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 group_capacity */
/******************************************/
CREATE TABLE group_capacity (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID,
group_id VARCHAR(128) NOT NULL DEFAULT COMMENT Group ID空字符表示整个集群,
quota INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 配额0表示使用默认值,
usage INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 使用量,
max_size INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 单个配置大小上限单位为字节0表示使用默认值,
max_aggr_count INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 聚合子配置最大个数0表示使用默认值,
max_aggr_size INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 单个聚合数据的子配置大小上限单位为字节0表示使用默认值,
max_history_count INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 最大变更历史数量,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 修改时间,
PRIMARY KEY (id),
UNIQUE KEY uk_group_id (group_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENT集群、各Group容量信息表;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 his_config_info */
/******************************************/
CREATE TABLE his_config_info (
id BIGINT(64) UNSIGNED NOT NULL,
nid BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
data_id VARCHAR(255) NOT NULL,
group_id VARCHAR(128) NOT NULL,
app_name VARCHAR(128) DEFAULT NULL COMMENT app_name,
content LONGTEXT NOT NULL,
md5 VARCHAR(32) DEFAULT NULL,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
src_user TEXT,
src_ip VARCHAR(50) DEFAULT NULL,
op_type CHAR(10) DEFAULT NULL,
tenant_id VARCHAR(128) DEFAULT COMMENT 租户字段,
PRIMARY KEY (nid),
KEY idx_gmt_create (gmt_create),
KEY idx_gmt_modified (gmt_modified),
KEY idx_did (data_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENT多租户改造;/******************************************/
/* 数据库全名 nacos_config */
/* 表名称 tenant_capacity */
/******************************************/
CREATE TABLE tenant_capacity (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID,
tenant_id VARCHAR(128) NOT NULL DEFAULT COMMENT Tenant ID,
quota INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 配额0表示使用默认值,
usage INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 使用量,
max_size INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 单个配置大小上限单位为字节0表示使用默认值,
max_aggr_count INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 聚合子配置最大个数,
max_aggr_size INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 单个聚合数据的子配置大小上限单位为字节0表示使用默认值,
max_history_count INT(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 最大变更历史数量,
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 修改时间,
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_id (tenant_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENT租户容量信息表;CREATE TABLE tenant_info (
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT id,
kp VARCHAR(128) NOT NULL COMMENT kp,
tenant_id VARCHAR(128) DEFAULT COMMENT tenant_id,
tenant_name VARCHAR(128) DEFAULT COMMENT tenant_name,
tenant_desc VARCHAR(256) DEFAULT NULL COMMENT tenant_desc,
create_source VARCHAR(32) DEFAULT NULL COMMENT create_source,
gmt_create BIGINT(20) NOT NULL COMMENT 创建时间,
gmt_modified BIGINT(20) NOT NULL COMMENT 修改时间,
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_info_kptenantid (kp,tenant_id),
KEY idx_tenant_id (tenant_id)
) ENGINEINNODB DEFAULT CHARSETutf8 COLLATEutf8_bin COMMENTtenant_info;CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(500) NOT NULL,
enabled BOOLEAN NOT NULL
);CREATE TABLE roles (
username VARCHAR(50) NOT NULL,
role VARCHAR(50) NOT NULL,
UNIQUE INDEX idx_user_role (username ASC, role ASC) USING BTREE
);CREATE TABLE permissions (
role VARCHAR(50) NOT NULL,
resource VARCHAR(255) NOT NULL,
action VARCHAR(8) NOT NULL,
UNIQUE INDEX uk_role_permission (role,resource,action) USING BTREE
);INSERT INTO users (username, PASSWORD, enabled) VALUES (nacos, $2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu, TRUE);INSERT INTO roles (username, role) VALUES (nacos, ROLE_ADMIN);配置Nacos 我们进入Nacos的conf目录修改配置文件cluster.conf.example重命名为cluster.conf然后添加内容如果后面启动报错了就把这里的127.0.0.1换成本机真实IP 127.0.0.1:8845
127.0.0.1:8846
127.0.0.1:8847然后修改application.properties文件添加数据库配置 spring.datasource.platformmysqldb.num1db.url.0jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueuseUnicodetrueuseSSLfalseserverTimezoneUTC
db.user.0root
db.password.0root启动Nacos集群 将nacos文件夹复制3份分别命名为nacos1、nacos2、nacos3 然后分别修改这三个文件夹中的application.properties nacos1 server.port8845nacos2 server.port8846nacos3 server.port88474.Nginx反向代理 修改conf/nginx.conf文件将下面的配置粘贴到http块中 upstream nacos-cluster {server 127.0.0.1:8845;server 127.0.0.1:8846;server 127.0.0.1:8847;
}server {listen 80;server_name localhost;location /nacos {proxy_pass http://nacos-cluster;}
}启动nginx然后在浏览器访问http://localhost/nacos 即可同时将bootstrap.yml中的Nacos地址修改为localhost:80user-service和order-service中都改 spring:cloud:nacos:server-addr: localhost:80 ## Nacos地址 Ngnix做了监听重启服务在Nacos中可以看到管理的服务
Feign远程调用
先来看看我们以前利用RestTemplate发起远程调用的代码
String url http://user-service/user/ order.getUserId();
User user restTemplate.getForObject(url, User.class);存在以下问题 代码可读性差编程体验不统一参数复杂的URL难以维护百度随便搜一个中文名词然后看一下url有多长有多少参数 我们可以利用Feign来解决上面提到的问题Feign是一个声明式的http客户端官网地址https://github.com/OpenFeign/feign 其作用就是帮助我们优雅的实现http请求的发送
Feign替代RestTemplate Feign的使用步骤如下 引入依赖 我们在order-service服务的pom文件中引入Feign的依赖 dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId
/dependency添加注解 在order-service的启动类上添加EnableFeignClients注解开启Feign的功能 编写Feign客户端 在order-service中新建com.itcast.order.client包然后新建一个接口内容如下 FeignClient(user-service)
public interface UserClient {GetMapping(/user/{id})User findById(PathVariable(id) Long id);
}这个客户端主要是基于SpringMVC的注解来声明远程调用的信息比如 服务名称user-service请求方式GET请求路径/user/{id}请求参数Long id返回值类型User 这样Feign就可以帮助我们发送http请求无需自己使用RestTemplate来发送了 测试 修改order-service中的OrderService类中的queryOrderById方法使用Feign客户端代替RestTemplate //修改后的代码
Service
public class OrderService {Autowiredprivate OrderMapper orderMapper;Autowiredprivate UserClient userClient;public Order queryOrderById(Long orderId) {// 1. 查询订单Order order orderMapper.findById(orderId);// 2. 利用Feign发起http请求查询用户User user userClient.findById(order.getUserId());// 3. 封账user到orderorder.setUser(user);// 4. 返回return order;}
}总结 使用Feign的步骤 引入依赖主启动类添加EnableFeignClients注解编写FeignClient接口使用FeignClient中定义的方法替代RestTemplate
自定义配置
Feign可以支持很多的自定义配置如下表所示
类型作用说明feign.Logger.Level修改日志级别包含四种不同的级别NONE、BASIC、HEADERS、FULLfeign.codec.Decoder响应结果的解析器http远程调用的结果做解析例如解析json字符串为java对象feign.codec.Encoder请求参数编码将请求参数编码便于通过http请求发送feign. Contract支持的注解格式默认是SpringMVC的注解feign. Retryer失败重试机制请求失败的重试机制默认是没有不过会使用Ribbon的重试
一般情况下默认值就能满足我们的使用如果需要自定义只需要创建自定义的Bean覆盖默认的Bean即可下面以日志为例来演示如何自定义配置
配置文件方式
基于配置文件修改Feign的日志级别可以针对单个服务
feign: client:config: userservice: ## 针对某个微服务的配置loggerLevel: FULL ## 日志级别也可以针对所有服务
feign: client:config: default: ## 这里用default就是全局配置如果是写服务名称则是针对某个微服务的配置loggerLevel: FULL ## 日志级别 而日志的级别分为四种 NONE不记录任何日志信息这是默认值BASIC仅记录请求的方法URL以及响应状态码和执行时间HEADERS在BASIC的基础上额外记录了请求和响应头的信息FULL记录所有请求和响应的明细包括头信息、请求体、元数据
Java代码方式
也可以基于Java代码修改日志级别先声明一个类然后声明一个Logger.Level的对象
public class DefaultFeignConfiguration {Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC; //日志级别设置为 BASIC}
}如果要全局生效将其放到启动类的EnableFeignClients这个注解中
EnableFeignClients(defaultConfiguration DefaultFeignConfiguration.class)如果是局部生效则把它放到对应的FeignClient注解中
FeignClient(value user-service, configuration DefaultFeignConfiguration.class)Feign使用优化 Feign底层发起http请求依赖于其他框架其底层客户端实现包括 URLConnection默认实现不支持连接池Apache HttpClient支持连接池OKHttp支持连接池 因此提高Frign的性能主要手段就是使用连接池代替默认的URLConnection 这里我们使用Apache的HttpClient来演示 引入依赖 在order-service的pom文件中引入Apache的HttpClient依赖 !--httpClient的依赖 --
dependencygroupIdio.github.openfeign/groupIdartifactIdfeign-httpclient/artifactId
/dependency配置连接池 在order-service的application.yml中添加配置 feignclient:config:default: ## default全局的配置logger-level: BASIC ## 日志级别BASIC就是基本的请求和响应信息httpclient:enabled: true ## 开启feign对HttpClient的支持max-connections: 200 ## 最大的连接数max-connections-per-route: 50 ## 每个路径的最大连接数小结Feign的优化 日志级别尽量使用BASIC使用HttpClient或OKHttp代替URLConnection 引入feign-httpclient依赖配置文件中开启httpclient功能设置连接池参数
最佳实践
所谓最佳实践就是使用过程中总结的经验最好的一种使用方式仔细观察发现Feign的客户端与服务提供者的controller代码十分相似
{% tabs test4 %} FeignClient(value user-service,configuration DefaultFeignConfiguration.class)
public interface UserClient {GetMapping(/user/{id})User findById(PathVariable(id) Long id);
}RestController
RequestMapping(/user)
RefreshScope
public class UserController {Autowiredprivate UserService userService;GetMapping(/{id})public User queryById(PathVariable(id) Long id) {return userService.queryById(id);}
}{% endtabs %}
除了方法名其余代码几乎一模一样那有没有一种方法简化这种重复的代码编写呢
继承方式 这两部分相同的代码可以通过继承来共享 定义一个API接口利用定义方法并基于SpringMVC注解做声明 public interface UserAPI{GetMapping(/user/{id})User findById(PathVariable(id) Long id);
}Feign客户端和Controller都继承该接口 {% tabs test4 %} FeignClient(value user-service)
public interface UserClient extends UserAPI{}RestController
public class UserController implents UserAPI{public User findById(PathVariable(id) Long id){// ...实现业务逻辑}
}{% endtabs %} 优点 简单实现了代码共享 缺点 服务提供方、服务消费方紧耦合参数列表中的注解映射并不会继承所以Controller中必须再次声明方法、参数列表、注解
抽取方式
将Feign的Client抽取为独立模块并且把接口有关的POJO、默认的Feign配置都放到这个模块中提供给所有消费者使用例如将UserClient、User、Feign的默认配置都抽取到一个feign-api包中所有微服务引用该依赖包即可直接使用
实现基于抽取的最佳实践 抽取 首先创建一个新的module命名为feign-api然后在pom文件中引入feign的starter依赖 dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-openfeign/artifactId
/dependency然后将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中 在order-service中使用feign-api 首先将order-service中的UserClient、User、DefaultFeignConfiguration等类或接口删除掉然后在order-service中的pom文件中引入我们自己编写的feign-api依赖 dependencygroupIdcn.itcast.demo/groupIdartifactIdfeign-api/artifactIdversion1.0/version
/dependency接着修改order-service中涉及到以上三个组件的代码爆红部分 解决包扫描问题 现在UserClient在cn.itcast.feign.clients包下而order-service的EnableFeignClients注解是在cn.itcast.order包下不在同一个包无法扫描到UserClient 方式一指定Feign应该扫描的包 EnableFeignClients(basePackages cn.itcast.feign.clients)方式二指定需要加载的Client接口 EnableFeignClients(clients {UserClient.class})Gateway服务网关
SpringCloudGateway是SpringCloud的一个全新项目该项目是基于Spring 5.0SpringBoot2.0和ProjectReactor等响应式办成和事件流技术开发的网关它旨在为微服务框架提供一种简单有效的统一的API路由管理方式
为什么需要网关
Gateway网关是我们服务的守门神是所有微服务的统一入口网关的核心功能特性 请求路由权限控制限流 架构图如下 路由和负载均衡一切请求都必须先经过gateway但网关不处理业务而是根据某种规则把请求转发到某个微服务这个过程叫路由。当然路由的目标服务有多个时还需要做负载均衡权限控制网关作为微服务的入口需要校验用户是否有请求资格如果没有则拦截限流当请求量过高时在网关中按照微服务能够接受的速度来放行请求避免服务压力过大在SpringCloud中网关的实现包括两种 gatewayzuul Zuul是基于Servlet的实现属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux属于响应式编程的实现具备更好的性能
gateway快速入门 下面我们就来演示一下网关的基本路由功能基本步骤如下 创建SpringBoot工程gateway引入网关依赖 创建一个maven工程就行引入依赖如下 !--网关--
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactId
/dependency
!--nacos服务发现依赖--
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId
/dependency编写启动类 SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class,args);}
}编写基础配置和路由规则 server:port: 10010 ## 网关端口
spring:application:name: gateway ## 服务名称cloud:nacos:server-addr: localhost:80 ## nacos地址我这里还是用的nginx反向代理你们可以启动一个单体的nacos用8848端口gateway:routes:- id: user-service ## 路由id自定义只需要唯一即可uri: lb://user-service ## 路由的目标地址lb表示负载均衡后面跟服务名称## uri: http://localhost:8081 ## 路由的目标地址http就是固定地址predicates: ## 路由断言也就是判断请求是否符合路由规则的条件- Path/user/** ## 这个是按照路径匹配只要是以/user开头的就符合规则- id: order-service ## 按照上面的写法再配置一下order-serviceuri: lb://order-service predicates: - Path/order/** ##注意Path是大写启动网关服务进行测试 重启网关访问http://localhost:10010/user/1 时符合/user/**规则请求转发到http://user-service/user/1结果如下 {id: 1,username: 柳岩,address: 湖南省衡阳市
}访问http://localhost:10010/order/101 时符合/order/**规则请求转发到http://order-service/order/101结果如下 {id: 101,price: 699900,name: Apple 苹果 iPhone 12 ,num: 1,userId: 1,user: {id: 1,username: 柳岩,address: 湖南省衡阳市}
}网关陆游的流程图 总结 网关搭建的步骤 创建项目引入nacos和gateway依赖配置application.yml包括服务基本信息nacos地址、路由 路由配置包括 路由id路由的唯一表示路由目标uri路由的目标地址http代表固定地址lb代表根据服务名称负载均衡路由断言predicates判断路由的规则路由过滤器filters对请求或相应做处理 接下来我们就来重点学习路由断言和路由过滤器的详细知识
断言工厂
我们在配置文件中写的断言规则只是字符串这些字符串会被Predicate Factory读取并处理转变为路由判断的条件例如Path/user/**是按照路径匹配这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的像这样的断言工厂在SpringCloudGatewway还有十几个
名称说明示例After是某个时间点后的请求- After2037-01-20T17:42:47.789-07:00[America/Denver]Before是某个时间点之前的请求- Before2031-04-13T15:14:47.43308:00[Asia/Shanghai]Between是某两个时间点之前的请求- Between2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]Cookie请求必须包含某些cookie- Cookiechocolate, ch.pHeader请求必须包含某些header- HeaderX-Request-Id, \dHost请求必须是访问某个host域名- Host.somehost.org,.anotherhost.orgMethod请求方式必须是指定方式- MethodGET,POSTPath请求路径必须符合指定规则- Path/red/{segment},/blue/**Query请求参数必须包含指定参数- Queryname, Jack或者- QuerynameRemoteAddr请求者的ip必须是指定范围- RemoteAddr192.168.1.1/24Weight权重处理
关于更详细的使用方法可以参考官方文档
过滤器工厂
GatewayFilter是网关中提供的一种过滤器可以对进入网关的请求和微服务返回的响应做处理
路由过滤器的种类
Spring提供了31中不同的路由过滤器工厂例如
名称说明AddRequestHeader给当前请求添加一个请求头RemoveRequestHeader移除请求中的一个请求头AddResponseHeader给响应结果中添加一个响应头RemoveResponseHeader从响应结果中移除有一个响应头RequestRateLimiter限制请求的流量
官方文档的使用举例
spring:cloud:gateway:routes:- id: add_request_header_routeuri: https://example.orgfilters:- AddRequestHeaderX-Request-red, blueThis listing adds X-Request-red:blue header to the downstream request’s headers for all matching requests.关于更详细的使用方法可以参考官方文档
请求头过滤器
下面我们以AddRequestHeader为例作为讲解 需求给所有进入user-service的请求都添加一个请求头TruthHello World! 只需要修改gateway服务的application.yml文件添加路由过滤即可
server:port: 10010 ## 网关端口
spring:application:name: gateway ## 服务名称cloud:nacos:server-addr: localhost:8848 ## nacos地址gateway:routes:- id: user-serviceuri: lb://user-servicepredicates:- Path/user/**filters:- AddRequestHeaderTruth,Hello World! ## 添加请求头当前过滤器写在user-service路由下因此仅仅对访问user-service的请求有效我们在UserController中编写对应的方法来测试
GetMapping(/test)
public void test(RequestHeader(Truth) String word) {System.out.println(word);
}重启网关和user-service打开浏览器访问http://localhost:10010/user/test 控制台会输出Hello World!证明我们的配置已经生效
默认过滤器
如果要对所有的路由都生效则可以将过滤器工厂写到default下格式如下
YMLserver:port: 10010 ## 网关端口
spring:application:name: gateway ## 服务名称cloud:nacos:server-addr: localhost:80 ## nacos地址gateway:routes:- id: user-serviceuri: lb://user-servicepredicates:- Path/user/**default-filters: - AddRequestHeaderTruth, Hello World! ## 添加请求头重启网关服务打开浏览器访问http://localhost:10010/user/test 控制台依旧会输出Hello World!!证明我们的配置已经生效
小结
过滤器的作用是什么 对路由的请求或响应做加工处理比如添加请求头配置在路由下的过滤器只对当前路由请求生效 default-filters的作用是什么 对所有路由都生效的过滤器
全局过滤器
上面提到的31中过滤器的每一种的作用都是固定的如果我们希望拦截请求做自己的业务逻辑则无法实现这就要用到我们的全局过滤器了
全局过滤器的作用
全局过滤器的作用也是处理一切进入网关的请求和微服务响应与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义处理的逻辑是固定的而GlobalFilter的逻辑需要我们自己编写代码实现定义的方式就是实现GlobalFilter接口
public interface GlobalFilter {/*** 处理当前请求有必要的话通过{link GatewayFilterChain}将请求交给下一个过滤器处理** param exchange 请求上下文里面可以获取Request、Response等信息* param chain 用来把请求委托给下一个过滤器 * return {code MonoVoid} 返回标示当前过滤器业务结束*/MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain);
}在filter中编写自定义逻辑可以实现下列功能 登录状态判断权限校验请求限流等
自定义全局过滤器 需求定义全局过滤器拦截请求判断请求参数是否满足下面条件 参数中是否有authorizationauthorization参数值是否为admin 如果同时满足则放行否则拦截 具体实现如下 在gateway模块下新建cn.itcast.gateway.filter包然后在其中编写AuthorizationFilter类实现GlobalFilter接口重写其中的filter方法 Component
Order(-1)//执行优先级
public class AuthorizationFilter implements GlobalFilter {Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 获取请求参数MultiValueMapString, String params exchange.getRequest().getQueryParams();// 2. 获取authorization参数String authorization params.getFirst(authorization);// 3. 校验if (admin.equals(authorization)) {// 4. 满足需求则放行return chain.filter(exchange);}// 5. 不满足需求设置状态码这里的常量底层就是401在restFul中401表示未登录exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);// 6. 结束处理return exchange.getResponse().setComplete();}
}重启网关测试我们的拦截器是否生效打开浏览器访问http://localhost:10010/user/1无法正常访问加上需要的请求参数访问http://localhost:10010/user/1?authorizationadmin 可以看到正常数据
{id: 1,username: 柳岩,address: 湖南省衡阳市
}过滤器执行顺序 请求进入网关会碰到三类过滤器当前路由的过滤器、DefaultFilter、GlobalFilter 请求路由后会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链集合中排序后依次执行每个过滤器 那么排序的规则是什么呢 每个过滤器都必须指定一个int类型的order值order值越小优先级越高执行顺序越靠前默认值为2147483647即int最大值 GlobalFilter通过实现Ordered接口或者添加Order注解来指定order值(像上面我们自定义全局过滤器那样)需要我们自己指定 实现Ordered接口 使用 public class AuthorizationFilter implements GlobalFilter, Ordered {Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1. 获取请求参数MultiValueMapString, String params exchange.getRequest().getQueryParams();// 2. 获取authorization参数String authorization params.getFirst(authorization);// 3. 校验if (admin.equals(authorization)) {// 4. 满足需求则放行return chain.filter(exchange);}// 5. 不满足需求设置状态码这里的常量底层就是401在restFul中401表示未登录exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);// 6. 结束处理return exchange.getResponse().setComplete();}Overridepublic int getOrder() {return -1;}
}路由过滤器和defaultFilter的order由Spring指定默认是按照声明顺序从1递增 当过滤器的order值一样时会按照defaultFilter 路由过滤器 GlobalFilter的顺序执行 例如下面这种情况下的order值就会相同如果我们在自定义全局过滤器中设定的order也为1那么也会冲突 server:port: 10010 ## 网关端口
spring:application:name: gateway ## 服务名称cloud:nacos:server-addr: localhost:80 ## nacos地址gateway:routes:- id: user-serviceuri: lb://user-servicepredicates:- Path/user/**filters:- AddRequestHeaderTruth, Hello World! ## 1- AddRequestHeaderTruth, Hello World! ## 2- AddRequestHeaderTruth, Hello World! ## 3default-filters:- AddRequestHeaderTruth, Hello World! ## 1- AddRequestHeaderTruth, Hello World! ## 2- AddRequestHeaderTruth, Hello World! ## 3详细内容可以查看源码 org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters然后再加载某个route的filters然后合并。org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器与前面的过滤器合并后根据order排序组织过滤器链
跨域问题
什么是跨域问题
跨域域名不一致就是跨域主要包括 域名不同www.baidu.com和www.baidu.orgwww.js.com和miaosha.js.com域名相同端口不同localhost:8080和localhost:8081 跨域问题浏览器禁止请求的发起者与服务端发生跨域ajax请求请求被浏览器拦截的问题解决方案CORS CORS是一个W3C标准全称是跨域资源共享Cross-origin resource sharing。它允许浏览器向跨源服务器发出XMLHttpRequest请求从而克服了AJAX只能同源使用的限制。
解决跨域问题
在gateway服务的application.yml文件中添加下面的配置
spring:cloud:gateway:globalcors: ## 全局的跨域处理add-to-simple-url-handler-mapping: true ## 解决options请求被拦截问题corsConfigurations:[/**]:allowedOrigins: ## 允许哪些网站的跨域请求 - http://localhost:1234allowedMethods: ## 允许的跨域ajax的请求方式- GET- POST- DELETE- PUT- OPTIONSallowedHeaders: * ## 允许在请求中携带的头信息allowCredentials: true ## 是否允许携带cookiemaxAge: 360000 ## 这次跨域检测的有效期配置完成后重启网关成功访问到参数解决跨域问题 Docker笔记导航