当前位置: 首页 > article >正文

抽丝剥茧 分布式服务框架设计 实战落地篇

1、概述

前面我们已经设计了一款分布式服务框架 Cheese,文章通过一些图文的形式给大家描绘出了Cheese的骨架和脉络,相信大家脑海里肯定还有一张这样的图

今天我们就将上面的架构图落地成可运行的程序,打造出一款简洁优雅的分布式服务框架

2、项目环境

2.1、前置要求

 1、在此之前需要准备好Zookeeper环境,关于Zookeeper环境的搭建大家可以参考之前的文章

Zookeeper 快速入门到实战

Zookeeper实战 集群环境部署

2、另外最好还需要具备Apache Curator 的基本使用的能力,同样的可以参考前面的文章

Zookeeper客户端工具 Apache Curator 最佳实践

2.2、相关的依赖

SpringBoot3.2.9
Zookeeper3.9.1
Apache Curator  

5.7.0

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.wcan</groupId>
    <artifactId>cheese</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.7.0</version> <!-- 请检查最新版本 -->
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

2.3、 搭建项目

我们新建一个项目 cheese 项目结构如下图所示

其中最外层我们设计3个接口,分别对应架构图中的服务发现、服务注册、远程调用这三个核心功能,接下来我们详细的看看 如何落地

3、ApplicationListener开发 

我们先来开发项目中的全局监听器,他的功能就是在程序启动后找到需要对外暴露的服务然后交给RegistServer组件,这里我们使用Spring的ApplicationListener 去实现,我们将这部分代码放在event包下,相关代码实现如下:

@Component
public class ServerLister implements ApplicationListener<ApplicationReadyEvent> {

    private final RegisterEvent registerEvent;

    private final CheeseConfig cheeseConfig;

    public ServerLister(RegisterEvent registerEvent,CheeseConfig cheeseConfig) {
        this.registerEvent = registerEvent;
        this.cheeseConfig = cheeseConfig;
    }


    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("所有的 Controller 和它们的请求映射:");
        Map<String, Object> serverMap = new HashMap<String, Object>();
        Map<String, Object> controllers = event.getApplicationContext().getBeansWithAnnotation(Controller.class);
        String serverPackage = cheeseConfig.getServerPackage();
        if(null == serverPackage || "".equals(serverPackage))
            return;
        controllers.forEach((key, value) -> {
            if (value.toString().contains(serverPackage)) {

                serverMap.put(key.toString(), value.getClass().getName());
            }
        });
        registerEvent.registerServer(serverMap);
    }

}

 我们实现ApplicationListener接口,然后在onApplicationEvent中实现对应的逻辑,需要注意的是我们还需要获取到本机的ip和端口,因此我们还需要一个Environment 对象,这里我们在新建一个RegisterEvent组件,目的是组合RegistServer组件,相关代码如下

@Component
public class RegisterEvent {

    private Environment environment;

    private RegistServer registerServer;


    public RegisterEvent(Environment environment, RegistServer registerServer){
        this.environment = environment;
        this.registerServer = registerServer;
    }


    private  String getServerAddress() {
        String ip = "无法获取本机 IP 地址";
        try {
            ip = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }

        String port = environment.getProperty("server.port", "8080");
        return ip + ":" + port;
    }

    public void registerServer(Map<String, Object> serverMap)  {
        String ip = getServerAddress();
        for (Map.Entry<String, Object> entry : serverMap.entrySet()) {
            Object value = entry.getValue();
            String serverName = "/" + value.toString();
            try {
                registerServer.register(serverName, ip);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

        }
    }
}

我们通过Environment 对象可以获取到服务所在的主机以及端口,然后ServerLister将需要注册的服务传递过来后就可以一起交给RegisterServer 组件了。此时项目结构是这样的

4、服务注册与服务发现

4.1、服务注册功能开发

我们先在最外层的 定义 RegistServer 接口,该接口里先声明一个方法。

public interface RegistServer {

    public void register(String serviceName, String serviceAddress) throws Exception;

}

接着我们在 server 包下新建一个实现类 HttpRegistServer 。这块代码的职责是负责注册事件。

@Service
public class HttpRegistServer implements RegistServer {

    private CuratorExecute curatorExecute;

    public HttpRegistServer(CuratorExecute curatorExecute) {
        this.curatorExecute = curatorExecute;
    }

    @Override
    public void register(String serviceName, String serviceAddress)throws Exception {
        curatorExecute.setServerData(serviceName, serviceAddress);

    }
}

4.2、服务发现功能开发

同样的 DiscoveryServer 组件 相关的代码如下

public interface DiscoveryServer {
    public List<String> discovery() throws Exception;

    public String getSingleServer(String serviceName) throws Exception;
}
@Service
public class HttpDiscoveryServer implements DiscoveryServer {

    private CuratorExecute curatorExecute;

    public HttpDiscoveryServer(CuratorExecute curatorExecute) {
        this.curatorExecute = curatorExecute;
    }

    @Override
    public List<String> discovery() throws Exception {
        List<String> list =new ArrayList<String>();
        String serverData = curatorExecute.getServerData();
        list.add(serverData);
        return list;
    }

    @Override
    public String getSingleServer(String serviceName) throws Exception {
        return curatorExecute.getServerData(serviceName);
    }


}

服务发现组件中我们能定义了2个方法,分别是拉取服务列表和根据服务名拉取指定的服务。这部分完成后项目结构如下

5、CuratorExecute 组件开发

CuratorExecute 组件主要是和Zookeeper交互的,并且需要承载 HttpRegistServer 和HttpDiscoveryServer  实现服务注册和服务发现的基础能力 。我们在execute包下新建CuratorExecute类。相关代码如下

@Service
public class CuratorExecute {

    private CheeseConfig cheeseConfig;
    private CuratorFramework curatorFramework;

    public CuratorExecute(CheeseConfig cheeseConfig) {
        this.cheeseConfig = cheeseConfig;
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(cheeseConfig.getBaseSleepTimeMs(), cheeseConfig.getMaxRetries());
         curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(cheeseConfig.getZkUrl())
                .sessionTimeoutMs(cheeseConfig.getTimeout())
                .retryPolicy(retryPolicy)
                .build();
        //开启连接
        curatorFramework.start();
    }

    public CuratorFramework getCuratorFramework() {
        return curatorFramework;
    }
    public String getServerData() throws Exception{
        return getServerData(cheeseConfig.getNodePath());
    }

    public String getServerData(String path) throws Exception {
        GetDataBuilder data = this.curatorFramework.getData();
        if (!path.startsWith("/"))
            path = "/" + path;
        byte[] bytes = data.forPath(path);
        return new String(bytes);
    }

    public void setServerData(String path, String data) throws Exception {
        if (curatorFramework.checkExists().forPath(path) == null) {
            curatorFramework.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
        }
        curatorFramework.setData().forPath(path, data.getBytes());
    }


    public void close() {
        if (curatorFramework != null) {
            curatorFramework.close();
        }
    }

}

6、RemoteServer开发

最后还需要一个能够实现远程调用的组件,也就是架构图中的 RemoteServer组件,

接口设计两个方法,一个用于根据服务名远程调用,另外一个是带参数的。关于返回值这里分别使用了String 和Map ,大家可以自己定义合适的返回类型。

public interface RemoteServer {
    String execute(String serviceName) throws Exception;

    Map<String, Object> execute(String serviceName, String params) throws Exception;

}

接着我们将它的实现类放在execute包下,相关代码如下

@Service
public class HttpRemoteExecute implements RemoteServer {

    private DiscoveryServer discoveryServer;

    private RestTemplate restTemplate;

    public HttpRemoteExecute(DiscoveryServer discoveryServer,RestTemplate restTemplate){
        this.discoveryServer = discoveryServer;
        this.restTemplate = restTemplate;
    }

    @Override
    public String execute(String params) throws Exception {
        return null;
    }

    @Override
    public Map<String, Object> execute(String serviceName, String params) throws Exception {
        System.out.println("HttpRemoteExecute execute");
        String[] serviceNames = serviceName.split("#");

        String serverIp = discoveryServer.getSingleServer(serviceNames[0]);
        String url =  PROTOCOL_HTTP+serverIp+"/"+serviceNames[1];

        // 发送 GET 请求并获取响应
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        // 输出响应状态码和响应体
        System.out.println("Status Code: " + response.getStatusCode());
        System.out.println("Response Body: " + response.getBody());
        String body = response.getBody();
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("data", body);
        return map;
    }
}

另外,我们还需要配置一个RestTemplate 组件到容器里

@Configuration
public class ServiceConfig {
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}

7、关于项目配置的设计与实现

最后我们需要设计一个配置类,用来实现动态管理Zookeeper的配置以及cheese 运行时需要的一些配置。我们将这块功能 设计成 一个 CheeseConfig 类,他的功能是 动态的维护一些配置信息。比如 Zookeeper的链接串,超时时间以及默认节点的路径和尝试次数等等

@Service
public class CheeseConfig {

    @Value("${zkUrl}")
    private String zkUrl;
    @Value("${timeout:3000}")
    private int timeout;
    @Value("${zkPort:2181}")
    private String zkPort;
    @Value("${nodePath}")
    private String nodePath;
    @Value("${nodeValue:tom and jerry}")
    private String nodeValue;
    @Value("${baseSleepTimeMs:1000}")
    private int baseSleepTimeMs;
    @Value("${maxRetries:3}")
    private int maxRetries;
    @Value("${serverPackage:''}")
    private String serverPackage;

//getter   setter   .......
}

我们在common包下再新建一个常量类

public class Constant {
     public static final String PROTOCOL_HTTP =  "http://";
     
}

 8、项目发布

cheese 的目前的代码就这么多了,最后我们来看看整个项目的结构吧

我们通过idea工具就可以完成打包的工作了

9、cheese 的使用

9.1、服务提供者 tom-store

完成了开发后我们可以来测试一下,我们先来新建一个项目 ,项目结构如下

项目依赖 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.tom</groupId>
    <artifactId>tom-store</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>org.wcan</groupId>
            <artifactId>cheese</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

 我们编写一个简单的Controller 模拟Tom 返回给Jerry 的消息

@RestController
public class CheeseController {

    @RequestMapping("/getCheese")
    public Map<String, Object> getCheese(){
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("cheese", "A piece of cheese on the small table in the room");
        map.put("msg","我正在约会 不要打扰我");
        return map;
    }
}

 主启动类需要注意,我们要扫描cheese框架的类

@SpringBootApplication
@ComponentScan(basePackages = {"org.tom","org.wcan.cheese"})
public class TomStoreApplication {

    public static void main(String[] args) {
        SpringApplication.run(TomStoreApplication.class, args);
    }
    
}

最后我们需要配置zk

spring.application.name=tom-store
server.port=9000

zkUrl=192.168.200.100:9002,192.168.200.100:9003,192.168.200.100:9004
zookeeperBasePath=/tom
nodePath=/config
timeout=5000
serverPackage=org.tom

 9.2、服务提供者测试

我们启动 tom-store 后 查看Zookeeper的控制台

我们发现 tom-store 的 ip和地址已经注册上去了。

同样的我们访问 http://192.168.200.1:9000/getCheese

 此致服务注册的功能已经初步的实现了,接下来我们试试 服务发现和服务远程调用的功能

9.3、服务消费者 jerry-store

新建一个项目,取名 jerry-store 

controller 代码如下: 

@RestController
public class CheeseController {

    @Autowired
    private CheeseService cheeseService;

    @RequestMapping("/getCheese")
    public Object getCheese() throws Exception {
        Map<String, Object> cheese = cheeseService.getCheese();
        return cheese.get("data");
    }
}

 service 代码如下

public interface CheeseService {
    public Map<String, Object> getCheese() throws Exception;
}
@Service
public class CheeseServiceImpl implements CheeseService{

    @Autowired
    private RemoteServer remoteServer;

    @Value("${serviceName}")
    private String serviceName;
    @Override
    public Map<String, Object> getCheese() throws Exception {
        Map<String, Object> objectMap = remoteServer.execute(serviceName, "");
        return objectMap;
    }
}

我们再这里直接使用cheese 提供的RemoteServer 组件进行远程调用

最后添加配置

spring.application.name=jerry-store
server.port=8000

zkUrl=zkUrl=192.168.200.100:9002,192.168.200.100:9003,192.168.200.100:9004
zookeeperBasePath=/jerry
nodePath=/config
timeout=5000
serverPackage=org.jerry

##配置接口编号
serviceName=org.tom.controller.CheeseController#getCheese

这里我们需要配置一个接口编号,因为Jerry需要知道  tom 那个接口是处理奶酪的接口,可以去看看上一篇文章   理论设计篇描述的场景。

9.4、服务发现和远程调用测试

启动 jerry-store ,浏览器访问 http://192.168.200.1:8000/getCheese

到这里Jerry 就知道了 房间里的小桌子上有一块奶酪了。

10、总结和展望

我们完成了 cheese 的设计和开发,Jerry 通过它找到了想要吃的奶酪,但是你肯定会存在一些疑问。比如  Cheese 每次都会先去Zookeeper 查一下Tom的位置在进行调用,这样Tom和Jerry 之间的沟通效率会很低,并且如果多个服务这么玩  zookeeper 就会有很大访问压力。关于这些问题后面我们会进行一步一步的优化。

cheese 是一个粗糙的开始,也是一个最好的开始,我们后面一起加油


http://www.kler.cn/a/375102.html

相关文章:

  • C/C++基础错题归纳
  • 使用strimzi-kafka-operator 的mirrormake2(mm2)迁移kafka集群,去掉目标集群的topic默认前缀
  • 关于uni-forms组件的bug【提交的字段[‘*‘]在数据库中并不存在】
  • Android修行手册 - 移动端几种常用动画方案对比
  • 机器学习04-为什么Relu函数
  • UG NX二次开发(C#)-机电概念设计-UIStyler中selection块选择信号等对象的过滤器设置
  • Spring AOP(定义、使用场景、用法、3种事务、事务失效场景及解决办法、面试题)
  • Spring beanFactoryPostProcessor
  • Redis 线程控制 问题
  • 在linux中是如何运行一个应用程序的?
  • (七)JavaWeb后端开发1——Maven
  • 大语言模型驱动的跨域属性级情感分析——论文阅读笔记
  • 创造tips的秘籍——PHP回调后门
  • Redis 实战 问题
  • 【Sublime Text】格式化Json和XML
  • 线代的几何意义(一)——向量,坐标,矩阵
  • thinkphp和vue基于Workerman搭建Websocket服务实现用户实时聊天,完整前后端源码demo及数据表sql
  • Docker部署jenkins容器时,允许jenkins容器内部控制宿主机上的docker
  • 正向解析,反向解析
  • CSS3新增长度单位(二)
  • 从比亚迪超越特斯拉,看颠覆全球市场的中国力量
  • 大语言模型微调方法详解【全量微调、PEFT、LoRA、Adapter】
  • Rust 力扣 - 2090. 半径为 k 的子数组平均值
  • 低压电容补偿不用时会有电流损耗吗?
  • Lampiao靶机入侵实战
  • 计算机考研,选择西安交通大学还是哈工大?