基于 SOFAJRaft 实现注册中心

news2024/11/20 11:36:13

文章目录

    • 1.前言
    • 2.git 示例地址
    • 3.官网示例分析
    • 3.SOFAJRAFT 注册中心实现(服务端)
      • 3.1 核心功能
      • 3.2 模块设计
      • 3.3 请求消息数据结构设计
        • 3.3.1 Registration 注册消息
        • 3.3.2 GetServiceInstancesRequest 获取服务实例请求
        • 3.3.3 GetServiceInstancesResponse 获取服务实例返回对象
        • 3.3.4 Response 返回对象
        • 3.3.5 HeartBeat 心跳
    • 4.实现步骤
      • 4.1 ServiceDiscoveryOuter 自动生成 proto 类
      • 4.2 服务注册发现接口
      • 4.3 ServiceDiscoveryServer 服务发现服务端
      • 4.4 ServiceDiscoveryStateMachine 状态机
        • 功能点1
        • 功能点2
        • 功能点3
        • 功能点4
          • 服务注册请求处理者
          • 服务注销请求处理者
      • 4.5 RpcProcessor 请求处理器
        • GetServiceInstancesRequestRpcProcessor
        • RegistrationRpcProcessor
      • 4.6 ServiceDiscoveryClient 客户端
      • 4.7 实现 ServiceDiscovery 接口
      • 4.8 Spring Web 项目集成
    • 5.测试

1.前言

通过本文的学习可以帮助大家了解 SOFAJRaft 的使用方式和集成步骤;参考本文的实现步骤,可以来完成自己工作中相关产品的一致性协议 raft 集成,从而实现应用的高可用。
学习本文的前提

  • 需要了解 Raft 一致性协议
  • 对 SOFAJRaft 框架有一定的了解
  • 可以参考:SOFAJRaft 日志复制共识算法 https://blog.csdn.net/xiewenfeng520/article/details/133775751

本文主要讨论的内容如下

  • 注册中心的实现
  • 如何集成 Raft 协议来实现注册中心高可用

2.git 示例地址

本文完整代码地址:https://github.com/huajiexiewenfeng/eval-discovery

3.官网示例分析

截图中的报错,是对官网示例做了一定的改造,不影响,忽略即可;大家也可以自己来扩展官方的代码来加深对 JRaft 的理解。
image.png
代码分为以下几个部分

  • CounterClient 客户端
  • CounterServer 服务端
  • CounterStateMachine 状态机
  • CounterClosure 回调
  • CounterOperation 操作
    • GET
    • INCREMENT
    • 扩展 …
  • Processor 操作对应的处理类
    • IncrementAndGetRequestProcessor -> INCREMENT
    • GetValueRequestProcessor -> GET
  • CounterSnapshotFile 快照存储
  • CounterService 计数服务接口
    • CounterServiceImpl 计数服务实现

具体的实现细节可参考,我们这里就不做赘述了

  • https://www.sofastack.tech/projects/sofa-jraft/counter-example/

3.SOFAJRAFT 注册中心实现(服务端)

3.1 核心功能

  • 服务注册(Service Registration)
  • 服务订阅(Service Subscription)
  • 数据同步(Replicas Sync)
    • SOFAJRAFT 内建支持

3.2 模块设计

按照官网 counter 示例,注册中心模块设计如下:

  • ServiceDiscoveryClient 客户端
  • ServiceDiscoveryServer 服务端
  • ServiceDiscoveryStateMachine 状态机
  • ServiceDiscoveryClosure 回调
  • ServiceDiscoveryOperation 操作
    • REGISTRATION 注册
    • DEREGISTRATION 注销
    • GET_SERVICE_INSTANCES 获取服务实例
    • BEAT 心跳
  • Processor 操作对应的处理类
    • RegistrationRpcProcessor -> REGISTRATION
    • GetServiceInstancesRequestRpcProcessor -> GET_SERVICE_INSTANCES
    • HeartBeatRpcProcessor -> BEAT
  • 注册中心一般采用内存型存储即可
  • ServiceDiscovery 服务发现注册接口
    • JRaftServiceDiscovery 基于 JRaft 服务发现注册实现

3.3 请求消息数据结构设计

3.3.1 Registration 注册消息

reversed 用于区分该消息是注册还是注销,true 表示注销,false 表示注册。

message Registration {
  string id = 1;
  string serviceName = 2;
  string host = 3;
  int32 port = 4;
  map<string, string> metadata = 5;
  optional bool reversed = 6;
}
3.3.2 GetServiceInstancesRequest 获取服务实例请求

通过 serviceName 获取服务实例集合

message GetServiceInstancesRequest {
  string serviceName = 1;
}
3.3.3 GetServiceInstancesResponse 获取服务实例返回对象

返回服务实例集合

message GetServiceInstancesResponse {
  repeated Registration value = 1;
}
3.3.4 Response 返回对象
message Response {
  int32 code = 1;
  optional string message = 2;
}
3.3.5 HeartBeat 心跳
message HeartBeat {
  string id = 1;
  string serviceName = 2;
  string host = 3;
  int32 port = 4;
}

4.实现步骤

我们按照上面的设计的模块,将类都创建好,然后将官网示例中对应的代码 copy 到类中,慢慢再根据我们的需求来进行改造。

4.1 ServiceDiscoveryOuter 自动生成 proto 类

1.proto 文件

syntax = "proto3";

package service.discovery;

option java_package = "com.csdn.eval.discovery.jraft.proto";
option java_outer_classname = "ServiceDiscoveryOuter";

message Registration {
  string id = 1;
  string serviceName = 2;
  string host = 3;
  int32 port = 4;
  map<string, string> metadata = 5;
}

message HeartBeat {
  string id = 1;
  string serviceName = 2;
  string host = 3;
  int32 port = 4;
}

message Response {
  int32 code = 1;
  string message = 2;
}

message GetServiceInstancesRequest {
  string serviceName = 1;
}

message GetServiceInstancesResponse {
  repeated Registration value = 1;
}

2.生成 proto 对应的 java 类
pom 中新增 proto 插件和依赖

  <properties>
    <jraft.version>1.3.12</jraft.version>
    <protobuf-java.version>3.22.4</protobuf-java.version>
  </properties>

 <dependencies>
    <!-- SOFA JRAFT -->
    <dependency>
      <groupId>com.alipay.sofa</groupId>
      <artifactId>jraft-core</artifactId>
      <version>${jraft.version}</version>
    </dependency>

    <dependency>
      <groupId>com.alipay.sofa</groupId>
      <artifactId>jraft-rheakv-core</artifactId>
      <version>${jraft.version}</version>
    </dependency>

    <dependency>
      <groupId>com.google.protobuf</groupId>
      <artifactId>protobuf-java</artifactId>
      <version>${protobuf-java.version}</version>
    </dependency>
 </dependencies>

  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>1.7.1</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>0.6.1</version>
        <configuration>
          <protoSourceRoot>src/main/resources/proto</protoSourceRoot>
          <protocArtifact>com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.54.1:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

3.编译完成之后,将类拷贝到 src 目录下面
image.png

4.2 服务注册发现接口

  • 初始化
  • 注册
  • 注销
  • 获取服务实例集合
  • 关闭
public interface ServiceDiscovery {

    ServiceDiscovery DEFAULT = loadDefault(ServiceDiscovery.class);

    void initialize(Map<String, Object> config);

    void register(ServiceInstance serviceInstance);

    void deregister(ServiceInstance serviceInstance);

    List<ServiceInstance> getServiceInstances(String serviceName);

    void close();

}

4.3 ServiceDiscoveryServer 服务发现服务端

将示例代码 com.alipay.sofa.jraft.example.counter.CounterServer 整个 copy 到我们的类中,再将下图中所有的报错处理完

  • 将相关的组件替换成我们自己的组件
    • CounterStateMachine -> ServiceDiscoveryStateMachine
    • GetValueRequestProcessor -> RegistrationRpcProcessor
    • IncrementAndGetRequestProcessor -> GetServiceInstancesRequestRpcProcessor
    • 新增 HeartBeatRpcProcessor
  • 将没有用的代码注释掉或者删除掉

image.png

4.4 ServiceDiscoveryStateMachine 状态机

按照上面的操作将代码复制,我在重要代码中增加了中文注释,从下图中可以看到大概有三个改造点
image.png
我们要实现功能点如下

功能点1
  • 从 ServiceDiscoveryClosure 获取到当前请求的类型,这里我们可以建一个枚举类,类型如下:
    • REGISTRATION 注册
    • DEREGISTRATION 注销
    • GET_SERVICE_INSTANCES 获取实例
    • BEAT 心跳
    public enum Kind {

        REGISTRATION,

        DEREGISTRATION,

        GET_SERVICE_INSTANCES,

        BEAT;
    }
功能点2

ServiceDiscoveryOperation 序列化和反序列化的实现,也就是 改造点2 的部分,移到 ServiceDiscoveryOperation 类中来实现,减少主流程的冗余代码。
image.png

功能点3

通过请求类型来完成对应请求方法的调用,这里我们可以采用策略模式来实现
image.png
创建策略工厂类
ServiceDiscoveryRequestHandlerFactory

  • RegistrationRequestHandler 注册处理者
  • DeRegistrationRequestHandler 注销处理者
  • GetServiceInstancesRequestHandler 获取服务实例处理者
  • HeartBeatRequestHandler 心跳处理者
public class ServiceDiscoveryRequestHandlerFactory {

  /**
   * 服务名称与服务实例列表(List)映射
   */
  private final Map<String, Map<String, ServiceInstance>> serviceNameToInstancesStorage = new ConcurrentHashMap<>();

  private final Map<Kind, ServiceDiscoveryRequestHandler> handlers = new HashMap<>();

  public void init() {
    handlers.put(Kind.REGISTRATION, new RegistrationRequestHandler(this));
    handlers.put(Kind.DEREGISTRATION, new DeRegistrationRequestHandler(this));
    handlers.put(Kind.GET_SERVICE_INSTANCES, new GetServiceInstancesRequestHandler(this));
    handlers.put(Kind.BEAT, new HeartBeatRequestHandler(this));
  }

  public ServiceDiscoveryRequestHandlerFactory() {

  }

  public ServiceDiscoveryRequestHandler getHandler(Kind kind) {
    return handlers.get(kind);
  }

  public synchronized void storage(String id, String serviceName, ServiceInstance serviceInstance) {
    Map<String, ServiceInstance> serviceInstancesMap = serviceNameToInstancesStorage
        .computeIfAbsent(serviceName, n -> new LinkedHashMap<>());
    serviceInstancesMap.put(id, serviceInstance);
  }

  public synchronized void delete(String id, String serviceName) {
    Map<String, ServiceInstance> instanceMap = getServiceInstancesMap(serviceName);
    instanceMap.remove(id);
  }

  public Map<String, ServiceInstance> getServiceInstancesMap(String serviceName) {
    return serviceNameToInstancesStorage.computeIfAbsent(serviceName, n -> new LinkedHashMap<>());
  }

}

在 ServiceDiscoveryServer 创建的过程中,通过构造函数来创建 Factory,并传入到 Fsm 状态机中
改造完成之后状态机代码如下

  @Override
  public void onApply(Iterator iter) {
    while (iter.hasNext()) {
      long current = 0;
      ServiceDiscoveryOperation operation = null;

      ServiceDiscoveryClosure closure = null;
      if (iter.done() != null) {
        // 从当前 Leader 节点获取 Closure
        closure = (ServiceDiscoveryClosure) iter.done();
        // 从 Closure 获取服务操作的类型
        operation = closure.getServiceDiscoveryOperation();
        logger.info("The closure with operation[{}] at the Leader node[{}]", operation, node);
      } else {
        // 在 Follower 节点通过 日志反序列化得到 ServiceDiscoveryOperation
        final ByteBuffer data = iter.getData();
        operation = ServiceDiscoveryOperation.deserialize(data);
        logger.info("The closure with operation[{}] at the Follower node[{}]", operation, node);
      }
      // 根据服务操作类型的不同来进行不同的业务操作
      if (operation != null) {
        ServiceDiscoveryRequestHandlerFactory instanceFactory = ServiceDiscoveryRequestHandlerFactory
            .getInstance();
        instanceFactory.init();
        instanceFactory.getHandler(kind)
            .doHandle(closure, (ServiceInstance) operation.getData());
        if (closure != null) {
          closure.run(Status.OK());
        }
      }
      iter.next();
    }
  }

关键代码:
instanceFactory.getHandler(kind).doHandle(closure, (ServiceInstance) operation.getData());
通过 kind 获取到对应的 handler,处理对应的请求方法。

功能点4

分别实现注册,注销,获取实例,心跳方法

服务注册请求处理者
public class RegistrationRequestHandler implements ServiceDiscoveryRequestHandler {

  private static final Logger logger = LoggerFactory.getLogger(RegistrationRequestHandler.class);

  private ServiceDiscoveryRequestHandlerFactory factory;

  public RegistrationRequestHandler(
      ServiceDiscoveryRequestHandlerFactory factory) {
    this.factory = factory;
  }

  @Override
  public void doHandle(ServiceDiscoveryClosure closure, ServiceInstance serviceInstance) {
    if (null == serviceInstance) {
      return;
    }
    String serviceName = serviceInstance.getServiceName();
    String id = serviceInstance.getId();

    factory.storage(id, serviceName, serviceInstance);

    logger.info("{} has been registered at the node", serviceInstance);
  }
}
服务注销请求处理者
public class DeRegistrationRequestHandler implements ServiceDiscoveryRequestHandler {

  private static final Logger logger = LoggerFactory.getLogger(DeRegistrationRequestHandler.class);

  private ServiceDiscoveryRequestHandlerFactory factory;

  public DeRegistrationRequestHandler(
      ServiceDiscoveryRequestHandlerFactory factory) {
    this.factory = factory;
  }

  @Override
  public void doHandle(ServiceDiscoveryClosure closure, ServiceInstance serviceInstance) {
    if (null == serviceInstance) {
      return;
    }
    String serviceName = serviceInstance.getServiceName();
    String id = serviceInstance.getId();

    factory.delete(id, serviceName);

    logger.info("{} has been deregistered at the node", serviceInstance);
  }
}

4.5 RpcProcessor 请求处理器

调用流程
image.png
RpcProcessor 实现的是 client -> apply(task) 这一部分的功能,具体的调用流程可以看官网 Counter 示例中的
com.alipay.sofa.jraft.example.counter.CounterServiceImpl#incrementAndGet 方法
image.png
此方法是在 IncrementAndGetRequestProcessor 中来调用的
image.png

GetServiceInstancesRequestRpcProcessor

参考 com.alipay.sofa.jraft.example.counter.rpc.IncrementAndGetRequestProcessor 示例来看需要完成两步

  • 构造 Closure 回调
  • 构造 task 对象,调用 Node().apply(task);

将对应的代码 copy 到 GetServiceInstancesRequestRpcProcessor 中,核心代码如下:
image.png
构造 Closure 回调,还有一个 getValueResponse() 方法需要实现,我们拿到返回结果;
getValueResponse() ->ServiceDiscoveryClosure#getResult()

  @Override
  public void handleRequest(RpcContext rpcContext, GetServiceInstancesRequest request) {
    String serviceName = request.getServiceName();

    ServiceDiscoveryOperation op = new ServiceDiscoveryOperation(Kind.GET_SERVICE_INSTANCES,
        serviceName);

    final ServiceDiscoveryClosure closure = new ServiceDiscoveryClosure(op) {
      @Override
      public void run(Status status) {
        rpcContext.sendResponse(getResult());
      }
    };

    if (!isLeader()) {
      handlerNotLeaderError(closure);
      return;
    }

    final Task task = new Task();
    task.setData(op.serialize());
    task.setDone(closure);
    this.serviceDiscoveryServer.getNode().apply(task);

  }

与 Counter 示例对比,发现 rpcContext.sendResponse(getResult()) 与原示例语义不同,rpcContext.sendResponse 需要的是 Response 对象,而我们这里返回的是 result 对象,是服务实例集合。
image.png
我们需要实现一个类似于 ValueResponse 的封装,返回 ServiceDiscoveryOuter.GetServiceInstancesResponse 对象。response 方法将 result 对象转换成 GetServiceInstancesResponse 对象,核心代码如下:

  private ServiceDiscoveryOuter.GetServiceInstancesResponse response(Object result) {
    Collection<ServiceInstance> serviceInstances = (Collection<ServiceInstance>) result;
    GetServiceInstancesResponse response = GetServiceInstancesResponse.newBuilder()
        .addAllValue(convertRegistrations(serviceInstances)).build();
    return response;
  }
RegistrationRpcProcessor

按照上面的方式我们再实现注册功能,核心代码如下:

public class RegistrationRpcProcessor implements RpcProcessor<ServiceDiscoveryOuter.Registration> {
  ...
  @Override
  public void handleRequest(RpcContext rpcContext, Registration registration) {
    ServiceInstance serviceInstance = convertServiceInstance(registration);

    String serviceName = registration.getServiceName();

    final Kind kind = Kind.REGISTRATION;

    ServiceDiscoveryOperation op = new ServiceDiscoveryOperation(kind, serviceInstance);

    final ServiceDiscoveryClosure closure = new ServiceDiscoveryClosure(op) {
      @Override
      public void run(Status status) {
        if (!status.isOk()) {
          logger.warn("Closure status is : {} at the {}", status, serviceDiscoveryServer.getNode());
          return;
        }
        rpcContext.sendResponse(response(status));
        logger.info("'{}' has been handled ,serviceName : '{}' , result : {} , status : {}",
            kind, serviceName, getResult(), status);
      }
    };

    if (!isLeader()) {
      handlerNotLeaderError(closure);
      return;
    }

    final Task task = new Task();
    task.setData(op.serialize());
    task.setDone(closure);
    this.serviceDiscoveryServer.getNode().apply(task);

  }
  ...
}

里面有很多重复代码,我们再进行重构。
将通用的代码抽取到 RpcProcessorImpl 中

public class RpcProcessorImpl implements RpcProcessorService {

  private static final Logger logger = LoggerFactory.getLogger(RpcProcessorImpl.class);

  private final ServiceDiscoveryServer serviceDiscoveryServer;

  public RpcProcessorImpl(ServiceDiscoveryServer serviceDiscoveryServer) {
    this.serviceDiscoveryServer = serviceDiscoveryServer;
  }

  @Override
  public Node getNode() {
    return this.serviceDiscoveryServer.getNode();
  }

  @Override
  public void applyOperation(ServiceDiscoveryClosure closure) {
    if (!isLeader()) {
      handlerNotLeaderError(closure);
      return;
    }
    final Task task = new Task();
    // 写入本地日志,将作为 AppendEntries RPC 请求的来源 -> Followers
    task.setData(closure.getServiceDiscoveryOperation().serialize());
    // 触发 Leader 节点上的状态机 onApply 方法
    task.setDone(closure);
    this.serviceDiscoveryServer.getNode().apply(task);
  }

  private ServiceDiscoveryStateMachine getFsm() {
    return this.serviceDiscoveryServer.getFsm();
  }

  private boolean isLeader() {
    return getFsm().isLeader();
  }

  private void handlerNotLeaderError(final Closure closure) {
    logger.error("No Leader node : {}", getNode().getNodeId());
    closure.run(new Status(RaftError.EPERM, "Not leader"));
  }

}

RegistrationRpcProcessor 核心代码如下:

public class RegistrationRpcProcessor implements RpcProcessor<ServiceDiscoveryOuter.Registration> {
  ...
  @Override
  public void handleRequest(RpcContext rpcContext, Registration registration) {
    ServiceInstance serviceInstance = convertServiceInstance(registration);

    String serviceName = registration.getServiceName();

    final Kind kind = Kind.REGISTRATION;

    ServiceDiscoveryOperation op = new ServiceDiscoveryOperation(kind, serviceInstance);

    final ServiceDiscoveryClosure closure = new ServiceDiscoveryClosure(op) {
      @Override
      public void run(Status status) {
        if (!status.isOk()) {
          logger.warn("Closure status is : {} at the {}", status, rpcProcessorService.getNode());
          return;
        }
        rpcContext.sendResponse(response(status));
        logger.info("'{}' has been handled ,serviceName : '{}' , result : {} , status : {}",
            kind, serviceName, getResult(), status);
      }
    };

    this.rpcProcessorService.applyOperation(closure);
  }
  ...
}

4.6 ServiceDiscoveryClient 客户端

同样我们先将官网的示例 copy。
官网的示例大概做了以下动作

  • 1.更新 Configuration
  • 2.初始化 CliClientService
  • 3.通过 RouteTable 获取到 Leader 节点
  • 3.执行对应的请求方法

我们可以将 main 方法改造成 init 方法,因为我们后面会采用 spring web 的方式来进行注册的测试,而不是采用 main 方式来启动 client。
将两个配置参数 groupId,registerAddress 以构造函数的方式进行注入。

public class ServiceDiscoveryClient {

  private String groupId = "service-discovery";

  /**
   * 127.0.0.1:8083
   */
  private String registerAddress;

  private CliClientService cliClientService;

  private RpcClient rpcClient;

  public ServiceDiscoveryClient(String groupId, String registerAddress) {
    this.groupId = groupId;
    this.registerAddress = registerAddress;
  }

  public void init(final String[] args) {
    ServiceDiscoveryGrpcHelper.initGRpc();

    final Configuration conf = new Configuration();
    if (!conf.parse(registerAddress)) {
      throw new IllegalArgumentException("Fail to parse conf:" + registerAddress);
    }
    RouteTable.getInstance().updateConfiguration(groupId, conf);

    final CliClientServiceImpl cliClientService = new CliClientServiceImpl();
    cliClientService.init(new CliOptions());
    this.cliClientService = cliClientService;
    this.rpcClient = cliClientService.getRpcClient();
  }
  ...
}

下面我们将需求 3 和 4 重构成一个通用的方法

  public <R> R invoke(Object request) throws Throwable {
    if (!RouteTable.getInstance().refreshLeader(cliClientService, groupId, 1000).isOk()) {
      throw new IllegalStateException("Refresh leader failed");
    }

    PeerId leader = RouteTable.getInstance().selectLeader(groupId);
    return (R) rpcClient.invokeSync(leader.getEndpoint(), request, TimeUnit.SECONDS.toMillis(5));
  }

4.7 实现 ServiceDiscovery 接口

以 register 注册方法为例
我们只需要实现两步
1.构造 Registration 参数对象
2.采用 RPC 的方式调用注册方法
核心代码如下:

  @Override
  public void register(ServiceInstance serviceInstance) {
    // 调用 RPC
    ServiceDiscoveryOuter.Registration registration = buildRegistration(serviceInstance, false);
    try {
      serviceDiscoveryClient.invoke(registration);
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }

4.8 Spring Web 项目集成

构建一个 Spring Web 的工程,在 Spring Application 启动的时候,初始化 JRaftServiceDiscovery,将 web 服务信息注册到注册中心即可。
pom 文件引入依赖

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.0.RELEASE</version>
  </parent>

  <dependencies>
    ...
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
     ...
 </dependencies>

启动类 MyApplication,这里为了测试方便,配置都是写死的,实际可以从 Spring Environment 获取 Spring 中的环境变量

  • 在 Spring Application 启动中,初始化 JRaftServiceDiscovery
  • 再调用 register 方法注册服务实例即可
@RestController
@SpringBootApplication
public class MyApplication implements ApplicationListener<ApplicationReadyEvent> {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

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

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        JRaftServiceDiscovery jRaftServiceDiscovery = new JRaftServiceDiscovery();
        environment.getSystemProperties().put("service.discovery.jraft.registry.address","127.0.0.1:9081,127.0.0.1:9082,127.0.0.1:9083");
        jRaftServiceDiscovery.initialize(environment.getSystemProperties());
        DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
        serviceInstance.setHost("127.0.0.1");
        serviceInstance.setId("1");
        serviceInstance.setPort(8080);
        serviceInstance.setServiceName("test1");
        jRaftServiceDiscovery.register(serviceInstance);
    }
}

5.测试

为了日志观察方便,我们可以在 ServiceDiscoveryRequestHandlerFactory 类中增加一个打印方法,每次操作之后打印 serviceNameToInstancesStorage 集合的元素

public class ServiceDiscoveryRequestHandlerFactory {
 ...
 public synchronized void storage(String id, String serviceName, ServiceInstance serviceInstance) {
    Map<String, ServiceInstance> serviceInstancesMap = serviceNameToInstancesStorage
        .computeIfAbsent(serviceName, n -> new LinkedHashMap<>());
    serviceInstancesMap.put(id, serviceInstance);
    print();
  }

  private void print() {
    serviceNameToInstancesStorage.forEach((k, v) -> {
      logger.info(" key :{}", k);
      v.forEach((nk, nv) -> {
        logger.info(" n_key :{} + n_value:{}", nk, nv);
      });
    });
  }
...
}

编辑服务端启动参数三个服务端参数分别为

  • /tmp/server1 service-discovery 127.0.0.1:9081 127.0.0.1:9081,127.0.0.1:9082,127.0.0.1:9083
  • /tmp/server2 service-discovery 127.0.0.1:9082 127.0.0.1:9081,127.0.0.1:9082,127.0.0.1:9083
  • /tmp/server3 service-discovery 127.0.0.1:9083 127.0.0.1:9081,127.0.0.1:9082,127.0.0.1:9083

image.png
依次启动 ServiceDiscoveryServer 服务。
再启动 Spring MyApplication,为了测试方便,我们可以直接采用写死配置的方式来进行调试,利用 debug 模式,来模拟多个 web 服务的注册
image.png
每次执行完,再替换参数,再次执行即可
核心代码如下:

DefaultServiceInstance serviceInstance = new DefaultServiceInstance();
serviceInstance.setHost("127.0.0.1");
serviceInstance.setId("2");
serviceInstance.setPort(8082);
serviceInstance.setServiceName("test2");
jRaftServiceDiscovery.register(serviceInstance);

观察每个注册中心服务端的打印信息
第一次注册
ServiceDiscoveryServer1:
image.png
ServiceDiscoveryServer2:
image.png
第二次注册
ServiceDiscoveryServer1:
image.png
ServiceDiscoveryServer2:
image.png
ServiceDiscoveryServer3:
image.png
可以看到每次服务注册的信息,都被同步到了三个服务端节点中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1081390.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android中级——ListView和RecycleView解析

ListView和RecycleView ListViewRecycleView ListView 使用步骤可看Android基础——ListView&#xff0c;其setAdapter()如下&#xff0c;回调getCount()获取Item个数 Override public void setAdapter(ListAdapter adapter) {if (mAdapter ! null && mDataSetObserv…

v-model绑定input、textarea、checkbox、radio、select

1.input <div><!-- v-model绑定input --><input type"text" v-model"message"><h2>{{message}}</h2></div><script>const App{template:#my-app,data() {return {message:Hello World,}},}Vue.createApp(App).…

Java:设计模式之结构型-装饰者模式(decorator pattern)

装饰者模式(decorator pattern): 动态地将责任附加到对象上 意图&#xff1a;为对象动态添加功能 类图 实现 设计不同种类的饮料&#xff0c;饮料可以添加配料&#xff0c;比如可以添加牛奶&#xff0c;并且支持动态添加新配料。每增加一种配料&#xff0c;该饮料的价格就…

微信怎么查看名下所有微信号?

一般大家都会有两个或者更多的微信号&#xff0c;那怎么知道自己名下有几个微信号呢&#xff1f; 微信号要使用支付或者其他一些功能是要实名才可以使用的&#xff0c;有时候不知道自己的名下绑定了多少微信号&#xff0c;怎么查询呢&#xff1f; 微信最近出了开通小号的功能&…

当下测试行业中UI自动化面临的难点及如何解决

经常有人会问&#xff0c;什么样的项目才适合进行UI自动化测试呢&#xff1f;UI自动化测试相当于模拟手工测试&#xff0c;通过程序去操作页面上的控件。而在实际测试过程中&#xff0c;经常会遇到无法找到控件&#xff0c;或者因控件定义变更而带来的维护成本等问题。 哪些场…

Python数组添加元素append的时间复杂度分析

由于数组需要连续的存储空间&#xff0c;append&#xff08;&#xff09;函数的时间复杂度可能为1也可能为n&#xff0c;主要看在后面添加元素时&#xff0c;当前位置是否可以添加&#xff0c;有位置可以添加的话&#xff0c;则直接在后面添加&#xff0c;此时时间复杂度为1&am…

工学云打卡签到自动完成在异地的问题就解决了|蘑菇钉

工学云打卡助手&#xff0c;能解决你在异地时每天不间断签到的问题&#xff0c;仔细看图哦 1.自动签到 2.自定义打卡地区 3.生成日周月报与总结自动发表 4.支持随机通用内容 5.支持打卡结果推送 对于许多即将步入职场的新人来说&#xff0c;实习是一个非常重要的阶段。实习…

【算法挨揍日记】day13—— DP34 【模板】前缀和、DP35 【模板】二维前缀和

DP34 【模板】前缀和 【模板】前缀和_牛客题霸_牛客网 题目描述&#xff1a; 给定一个长度为n的数组. 接下来有q次查询, 每次查询有两个参数l, r. 对于每个询问, 请输出 输入描述: 第一行包含两个整数n和q.第二行包含n个整数, 表示.接下来q行,每行包含两个整数 l和r. …

聊聊Android签名检测7种核心检测方案详解

聊聊Android签名检测总结与反思 背景&#xff1a; 这篇文章只讲Android端签名检测&#xff0c;安卓发展到现在&#xff0c;因为国内环境没有谷歌市场&#xff0c;所以很多官方推荐的Api没法使用 &#xff0c;所以国内的签名检测方式也是“千奇百怪” 。发展至今每种方法都有一…

智安网络|揭开云服务的神秘面纱:其含义和功能的综合指南

随着信息技术的不断发展&#xff0c;云服务已经成为了我们生活中的一个不可或缺的部分。无论是在个人生活中还是在商业领域&#xff0c;云服务都具有广泛的应用。 什么是云服务&#xff1f; 云服务是一种基于互联网的计算和存储资源提供方式&#xff0c;它允许用户通过互联网访…

CTF Misc(3)流量分析基础以及原理

前言 流量分析在ctf比赛中也是常见的题目&#xff0c;参赛者通常会收到一个网络数据包的数据集&#xff0c;这些数据包记录了网络通信的内容和细节。参赛者的任务是通过分析这些数据包&#xff0c;识别出有用的信息&#xff0c;例如登录凭据、加密算法、漏洞利用等等 工具安装…

智能优化算法常用指标一键导出为EXCEL,CEC2017函数集最优值,平均值,标准差,最差值,中位数,秩和检验,箱线图...

声明&#xff1a;对于作者的原创代码&#xff0c;禁止转售倒卖&#xff0c;违者必究&#xff01; 之前出了一篇关于CEC2005函数集的智能算法指标一键统计&#xff0c;然而后台有很多小伙伴在询问其他函数集该怎么调用。今天采用CEC2017函数集为例&#xff0c;进行展示。 为了突…

手动下载/安装Xcode的simulator

目录 前言解决方案1.获取simulator包下载地址1.1 Apple后台1.2 手动 2.使用三方下载工具下载3.使用命令安装simulator 前言 Xcode某个版本更新之后不带iOS的Simulator,导致全新下载一个Xcode后没法编译项目.公司的网又很坑,每次断掉点重试都重新下载,导致完全没法下下来.特别影…

lazada商品列表数据接口,关键词搜索lazada商品数据接口

在网页抓取方面&#xff0c;可以使用 Python、Java 等编程语言编写程序&#xff0c;通过模拟 HTTP 请求&#xff0c;获取lazada网站上的商品页面。在数据提取方面&#xff0c;可以使用正则表达式、XPath 等方式从 HTML 代码中提取出有用的信息。值得注意的是&#xff0c;lazada…

大规模语言模型人类反馈对齐--强化学习

​OpenAI 推出的 ChatGPT 对话模型掀起了新的 AI 热潮&#xff0c; 它面对多种多样的问题对答如流&#xff0c; 似乎已经打破了 机器和人的边界。这一工作的背后是大型语言模型 (Large Language Model&#xff0c;LLM) 生成领域的新训练范式&#xff1a;RLHF (Reinforcement Le…

专题二:二叉树的深搜【递归、搜索、回溯】

深度优先遍历&#xff08;DFS&#xff0c;全称为DepthFirstTraversal&#xff09;&#xff0c;是我们树或者图这样的数据结构中常用的⼀种遍历算法。这个算法会尽可能深的搜索树或者图的分⽀&#xff0c;直到⼀条路径上的所有节点都被遍历完毕&#xff0c;然后再回溯到上⼀层&a…

为什么要做CRM?

客户管理的痛点&#xff1a; 1、销售经常性漏跟错跟客户&#xff0c;客户转化率低造成资源浪费 2、客户信息繁杂&#xff0c;难整理和查找 3、销售离职带走客户资源&#xff0c;损失大 4、传统报价审批流程长&#xff0c;效率低 企业做CRM系统有以下几点好处&#xff1a; …

纸、纸板和纸制品 有效回收组分的测定

声明 本文是学习GB-T 42944-2023 纸、纸板和纸制品 有效回收组分的测定. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件描述了纸、纸板和纸制品中有效回收组分的测定方法。 本文件适用于各种纸、纸板和纸制品&#xff0c;也适用于铝箔…

Paddle GPU版本需要安装CUDA、CUDNN

完整的教程 深度学习环境配置&#xff1a;linuxwindows系统下的显卡驱动、Anaconda、Pytorch&Paddle、cuda&cudnn的安装与说明 - 知乎这篇文档的内容是尽量将深度学习环境配置(使用GPU)所需要的内容做一些说明&#xff0c;由于笔者只在windows和linux下操作过&#xf…

Zookeeper分布式一致性协议ZAB源码剖析

文章目录 1、ZAB协议介绍2、消息广播 1、ZAB协议介绍 ZAB 协议全称&#xff1a;Zookeeper Atomic Broadcast&#xff08;Zookeeper 原子广播协议&#xff09;。 Zookeeper 是一个为分布式应用提供高效且可靠的分布式协调服务。在解决分布式一致性方面&#xff0c;Zookeeper 并…