本文最后更新于:24 天前
                  
                
              
            
            
              
                
                前言 欢迎阅读本篇博客!在当前大规模分布式系统的开发中,构建可靠的服务架构是至关重要的一环。为了实现远程方法调用、服务注册与发现以及配置管理等功能,使用  Dubbo  和注册中心(ZooKeeper、Nacos)成为了主流选择
本文将带领您一步步搭建一个强大的分布式服务架构,通过深入探索 Dubbo 和注册中心的使用方式,帮助您轻松实现高效的远程调用和服务管理。我们将详尽介绍如何安装、配置和集成Nacos和ZooKeeper作为注册中心,并结合 Dubbo 框架搭建完整的微服务架构
无论您是刚开始接触分布式架构,还是已经有一定经验,本文都将为您提供实用的技巧和最佳实践,以确保您的服务架构在性能、可靠性和扩展性方面都达到最佳状态
一起来构建强大而灵活的分布式服务架构吧!让我们从安装配置开始,逐步探索 Dubbo 与注册中心的集成,实现高效远程调用和服务管理的愉悦体验
Dubbo 架构实现了什么?
像调用本地方法一样,调用远程方法(2023/08/24午) 以下是本文的行文思路: 
详细指南:使用ZooKeeper和Nacos搭建注册中心 
架构实践:结合 Dubbo 实现灵活的远程方法调用 
分布式协作:探索服务注册与发现的最佳实践 
高效管理:利用注册中心进行配置管理的技巧与策略 
 
 
正文 搭建注册中心 
我们接下来谈论的下载安装以及配置管理,都是在windows系统下进行的(2023/08/22晚)  
下载安装 
启动服务 
在ZooKeeper的bin目录下,双击zkServer.cmd即可快速启动注册中心:  
同理,在Nacos的bin目录下,双击startup.cmd即可快速启动注册中心:  
1 startup.cmd  -m standalone
远程方法调用 
1 2 3 4 5 6 7 @DubboService public  class  DemoServiceImpl  implements  DemoService  {@Override public  String sayHello (String name)  {return  "Hello "  + name;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Component public  class  Task  implements  CommandLineRunner  {@DubboReference private  DemoService demoService;@Override public  void  run (String... args)  throws  Exception {String  result  =  demoService.sayHello("world" );"Receive result ======> "  + result);new  Thread (()-> {while  (true ) {try  {1000 );new  Date () + " Receive result ======> "  + demoService.sayHello("world" ));catch  (InterruptedException e) {
服务注册与发现 
1 2 3 4 5 <dependency > <groupId > org.apache.Dubbo</groupId > <artifactId > Dubbo</artifactId > <version > 3.1.8</version > </dependency > 
1 2 3 4 5 <dependency > <groupId > com.alibaba.nacos</groupId > <artifactId > nacos-client</artifactId > <version > 2.2.1</version > </dependency > 
1 2 3 4 5 6 7 8 9 Dubbo: application: name:   Dubbo-springboot-demo-provider protocol: name:   Dubbo  port:  -1 registry: id:  nacos-registry address:  nacos://localhost:8848 
1 2 3 4 5 6 7 8 9 Dubbo: application: name:   Dubbo-springboot-demo-client protocol: name:   Dubbo port:  -1 registry: id:  nacos-registry address:  nacos://localhost:8848 
添加@DubboService注解,作为服务提供者,将该service方法注册到注册中心:  
1 2 3 4 5 6 7 8 9 @DubboService public  class  InnerInterfaceInfoServiceImpl  implements  InnerInterfaceInfoService  {
添加@DubboReference注解,作为消费者,从注册中心拉取对应服务,并成功调用:  
1 2 @DubboReference private  InnerUserService innerUserService;
1 2 3 4 5 6 7 8 9 10 if  (accessKey == null  || !accessKey.equals("memory" )) {return  handleNoAuth(response);User  invokeUser  =  innerUserService.getInvokeUser(accessKey);if  (invokeUser == null ) {return  handleNoAuth(response);
配置管理 
在服务提供者和消费者中,分别添加如下配置:(2023/08/24午)  
1 2 3 4 5 6 7 8 9 Dubbo: application: name:  Dubbo-springboot-demo-provider protocol: name:  Dubbo port:  -1 registry: id:  nacos-registry address:  nacos://localhost:8848 
1 2 3 4 5 6 7 8 9 Dubbo: application: name:  Dubbo-springboot-demo-client protocol: name:  Dubbo port:  -1 registry: id:  nacos-registry address:  nacos://localhost:8848 
Nacos 
2024年4月26日
 
🍖 推荐阅读:
2024年6月7日
 
很久没有真正实战过微服务了,基本上都是学了些理论基础,最近一次都是去年学习黑马教程微服务时写了 Demo练习。
今天正式实战微服务,以 API 接口开放平台为例。
子服务中导入依赖:
1 2 3 4 5 6 7 8 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-config</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</artifactId > </dependency > 
一定要注意版本兼容问题啊,这点就不细说了,直接去官网查就行:
🍖 推荐阅读:版本发布说明 | https://sca.aliyun.com 
我还是建议自己总结出一套最正确常用的依赖集合,这样自己做项目就会方便很多。
Nacos 作为 注册中心和配置中心,来看下配置文件:
1 2 3 4 5 6 7 cloud: nacos: discovery: server-addr:  127.0 .0 .1 :8848 config: import-check: enabled:  false 
很清晰,以 本地启动的 nacos 为注册中心,服务名称为 memory-client。
启动项目后,看到这样的提示,那就说明已经该服务注册成功了:
查看 Nacos :
将 memory-core,memory-gateway,memory-client三个服务启动
其中,memory-core 作为 bubbo 服务提供者,其下三个 dubbo 服务也成功注册到 Nacos 注册中心:
1 2 3 4 5 6 7 8 9 10 dubbo: application: name:  api-invoke-server protocol: name:  dubbo port:  -1 registry: id:  api-invoke address:  nacos://localhost:8848 
OpenFeign 
2024年6月12日
 
1 2 3 4 5 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > 
nested exception is java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0-CSDN博客 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @FeignClient(name = "memory-core", path = "/api/user" ) public  interface  UserFeignClient  {@GetMapping("/get/invoke") getInvokeUser (@RequestParam(value = "accessKey")  String accessKey) ;
启动服务报错 For ‘xxx’ URL not provided. Will try picking an instance via load-balancing-CSDN博客 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package  com.memory.gateway;import  org.junit.jupiter.api.Test;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.ActiveProfiles;  import  java.util.List;import  static  org.junit.jupiter.api.Assertions.*;@SpringBootTest   @ActiveProfiles("dev")  public  class  NacosDiscoveryTest  {  @Autowired   private  DiscoveryClient discoveryClient;  @Test   public  void  testServiceDiscovery ()  {  "Services list should not be null" );  "Services list should not be empty" );  boolean  containsMemoryCore  =  services.contains("memory-core" );  "'memory-core' should be in the services list" );  "memory-core" );  "Instances list for 'memory-core' should not be null" );  "Instances list for 'memory-core' should not be empty" );  for  (ServiceInstance instance : instances) {  "Service ID: "  + instance.getServiceId());  "Host: "  + instance.getHost());  "Port: "  + instance.getPort());  
2024年6月14日
 
特么踩了六天的坑,总算搞定了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @FeignClient(name = "memory-core", path = "/api/user/inner") public  interface  UserFeignClient  {@GetMapping("/get/invoke") getInvokeUser (@RequestParam(value = "accessKey")  String accessKey) ;
就这么简单的几行客户端代码,折腾了我好几天,总是报错还找不到解决方法。
首先,@RequestParam(value = “accessKey”),这行代码必须加上。原因:
nested exception is java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0-CSDN博客 
1 2 3 在 SpringMVC 和 Springboot 中都可以使用 @RequestParam  注解,不指定 value 的用法,为什么到了 Spring cloud 中的 Feign 这里就不行了呢?
接下来就是 path = “/api/user/inner” 的配置了,这行代码要是不配置,直接找不到对应服务,报这个错:
feign.RetryableException: Connection refused: connect executing GET http://memory-core/api/user/get/invoke?accessKey=memory 
看到这个恶心的错误没,没能在注册中心里找到对应服务。
为了解决这个问题,我还编写了 demo 测试代码,查看了下注册中心里到底有没有注册成功这个服务,都在上面👆
最后是这个:
No qualifying bean of type ‘org.springframework.boot.autoconfigure.http.HttpMessageConverters‘_no qualifying bean of type ‘org.springframework.bo-CSDN博客 
解决方案就在上面,我在 memory-backend-gateway 模块下面写了一个配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package  com.memory.gateway.config;import  org.springframework.beans.factory.ObjectProvider;import  org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import  org.springframework.boot.autoconfigure.http.HttpMessageConverters;import  org.springframework.context.annotation.Bean;import  org.springframework.context.annotation.Configuration;import  org.springframework.http.converter.HttpMessageConverter;import  java.util.stream.Collectors;@Configuration public  class  HttpMesConConfiguration  {@Bean @ConditionalOnMissingBean public  HttpMessageConverters messageConverters (ObjectProvider<HttpMessageConverter<?>> converters)  {return  new  HttpMessageConverters (converters.orderedStream().collect(Collectors.toList()));
这个问题就完美解决了。
搞定了。
步骤 memory-backend-gateway 和 memory-backend-core-service 模块引入依赖:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > 
编写客户端,这边我新增了一个公共模块:memory-backend-server-client,客户端代码写这里了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @FeignClient(name = "memory-core", path = "/api/user/inner") public  interface  UserFeignClient  {@GetMapping("/get/invoke") getInvokeUser (@RequestParam(value = "accessKey")  String accessKey) ;
memory-backend-gateway 启动类:
1 @EnableFeignClients(basePackages = "com.memory.client.feignClient") 
接下来还有什么好说的,直接调用就行了:
1 2 3 @Resource private  UserFeignClient userFeignClient;
1 2 3 4 5 6 7 8 9 10 11 if  (accessKey == null  || !accessKey.equals("memory" )) {return  handleNoAuth(response);User  invokeUser  =  userFeignClient.getInvokeUser(accessKey);if  (invokeUser == null ) {return  handleNoAuth(response);
真正调用到的是这个接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController @RequestMapping("/user/inner") public  class  UserInnerController  implements  UserFeignClient  {@Resource private  UserService userService;@Override @GetMapping("/get/invoke") public  User getInvokeUser (@RequestParam(value = "accessKey")  String accessKey)  {return  userService.getInvokeUser(accessKey);
2024年6月16日
 
特么真服了,还有这样的问题:
加这行注解吧:
1 2 3 main: web-application-type:  reactive allow-bean-definition-overriding:  true 
好,所有 Feign 客户端下的抽象方法中参数都必须这么写:
1 2 3 4 5 6 7 8 9 10 11 12 @FeignClient(name = "memory-core", path = "/api/interface/inner") public  interface  InterfaceInfoFeignClient  {@GetMapping("/get/interfaceInfo") getInterfaceInfo (@RequestParam(value = "path")  String path, @RequestParam(value = "method")  String method) ;
1 2 3 4 5 6 7 8 9 10 11 12 @FeignClient(name = "memory-core", path = "/api/user/inner") public  interface  UserFeignClient  {@GetMapping("/get/invoke") getInvokeUser (@RequestParam(value = "accessKey")  String accessKey) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 @FeignClient(name = "memory-core", path = "/api/user/interface/inner") public  interface  UserInterfaceInfoFeignClient  {@GetMapping("/get/invoke/count") boolean  invokeCount (@RequestParam(value = "interfaceInfoId")  long  interfaceInfoId, @RequestParam(value = "userId")  long  userId) ;
Gateway 接口路由 权限校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if  (!IP_WHITE_HOSTS.contains(sourceAddress)) {return  response.setComplete();if  (antPathMatcher.match("/ **inner** /" , path)) {return  response.setComplete();if  (SWAGGER_API_DOC_HOST.contains(sourceAddress)) {"聚合 API 文档访问,不需要校验" );return  chain.filter(exchange);
聚合文档 Spring Cloud Gateway网关聚合 | Knife4j (xiaominfo.com) 
分别在 memory-backend-core-service 模块和 memory-backend-application 模块中,导入以下坐标:
1 2 3 4 5 6 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-openapi2-spring-boot-starter</artifactId > <version > 4.3.0</version > </dependency > 
配置文件:
在网关模块 memory-backend-gateway 中,导入以下坐标:
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-gateway-spring-boot-starter</artifactId > <version > 4.3.0</version > </dependency > 
配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 knife4j: gateway: enabled:  true strategy:  discover discover: enabled:  true version :  swagger2 excluded-services: -  memory-gateway 
到这里,使用 Gateway 实现聚合文档就很轻松的完成了,可以根据需要按需配置,推荐在这里研究学习:
🥣 Spring Cloud Gateway网关聚合 | Knife4j (xiaominfo.com) 
访问:localhost ,即可查看到聚合 API 文档,十分方便。
如果这里引用了 Gateway 请求过滤器,可能会有无权限访问的情况发生:
在权限校验中,选择直接放行聚合 API 文档访问:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if  (!IP_WHITE_HOSTS.contains(sourceAddress)) {return  response.setComplete();if  (antPathMatcher.match("/ **inner** /" , path)) {return  response.setComplete();if  (SWAGGER_API_DOC_HOST.contains(sourceAddress)) {"聚合 API 文档访问,不需要校验" );return  chain.filter(exchange);
分布式 session 登录 1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.session</groupId > <artifactId > spring-session-data-redis</artifactId > </dependency > 
1 2 3 4 5 6 7 8 9 10 11 server: address:  0.0 .0 .0 port:  8101 servlet: context-path:  /api session: cookie: max-age:  2592000 path:  /api 
跨域解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class  CorsConfig {WebFilter()  {new  CorsConfiguration() ;AllowedMethod("*" ) ;AllowCredentials(true ) ;AllowedOriginPatterns(Collections.singletonList ("*" ) );ExposedHeader("*" ) ;new  UrlBasedCorsConfigurationSource(new  PathPatternParser() );CorsConfiguration("/**" , config ) ;new  CorsWebFilter(source ) ;
请求限流 总结