一、实现动态上下线
当有服务提供方动态上下线,我们如何进行感知呢?服务上线,首先会在注册中心进行注册,调用方是无法实时感知的,合理的方式只有两种:
1、调用方定时的去主动的拉。
2、注册中心主动的推送。
在我们当前的项目中zk提供了watcher机制,我们正好可以利用他来实现动态上下线,具体步骤如下:
1、调用方拉取服务列表时,注册一个watcher关注该服务节点的变化。
2、当服务提供方上线或线下时会触发watcher机制(节点发生了变化)。
3、通知调用方,执行动态上下线的操作。
代码如下:
@Override
public List<InetSocketAddress> lookup(String serviceName,String group) {
// 1、找到服务对应的节点
String serviceNode = Constant.BASE_PROVIDERS_PATH + "/" + serviceName + "/" +group;
// 2、从zk中获取他的子节点,这里需要注册一个watcher
List<String> children = ZookeeperUtils.getChildren(zooKeeper, serviceNode,new UpAndDownWatcher());
// 获取了所有的可用的服务列表
List<InetSocketAddress> inetSocketAddresses = children.stream().map(ipString -> {
String[] ipAndPort = ipString.split(":");
String ip = ipAndPort[0];
int port = Integer.parseInt(ipAndPort[1]);
return new InetSocketAddress(ip, port);
}).toList();
if(inetSocketAddresses.size() == 0){
throw new DiscoveryException("未发现任何可用的服务主机.");
}
return inetSocketAddresses;
}
一旦节点发生了变化,UpAndDownWatcher就会被触发,会触发reloadbalance(重新进行负载均衡),代码如下:
public class UpAndDownWatcher implements Watcher {
@Override
public void process(WatchedEvent event) {
// 当前的阶段是否发生了变化
if (event.getType() == Event.EventType.NodeChildrenChanged){
if (log.isDebugEnabled()){
log.debug("检测到服务【{}】下有节点上/下线,将重新拉取服务列表...",event.getPath());
}
String serviceName = getServiceName(event.getPath());
Registry registry = YrpcBootstrap.getInstance().getConfiguration().getRegistryConfig().getRegistry();;
List<InetSocketAddress> addresses = registry.lookup(serviceName,
YrpcBootstrap.getInstance().getConfiguration().getGroup());
// 处理新增的节点
for (InetSocketAddress address : addresses) {
// 新增的节点 会在address 不在CHANNEL_CACHE
// 下线的节点 可能会在CHANNEL_CACHE 不在address
if(!YrpcBootstrap.CHANNEL_CACHE.containsKey(address)){
// 根据地址建立连接,并且缓存
Channel channel = null;
try {
channel = NettyBootstrapInitializer.getBootstrap()
.connect(address).sync().channel();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
YrpcBootstrap.CHANNEL_CACHE.put(address,channel);
}
}
// 处理下线的节点 可能会在CHANNEL_CACHE 不在address
for (Map.Entry<InetSocketAddress,Channel> entry: YrpcBootstrap.CHANNEL_CACHE.entrySet()){
if(!addresses.contains(entry.getKey())){
YrpcBootstrap.CHANNEL_CACHE.remove(entry.getKey());
}
}
// 获得负载均衡器,进行重新的loadBalance
LoadBalancer loadBalancer = YrpcBootstrap.getInstance().getConfiguration().getLoadBalancer();
loadBalancer.reLoadBalance(serviceName,addresses);
}
}
private String getServiceName(String path) {
String[] split = path.split("/");
return split[split.length - 1];
}
}
我们对于reloadbalance的实现十分简单,就是根据新的服务列表,生成一个新的选择器,将原有的替换即可。
@Override
public synchronized void reLoadBalance(String serviceName,List<InetSocketAddress> addresses) {
// 我们可以根据新的服务列表生成新的selector
cache.put(serviceName,getSelector(addresses));
}
**注:**服务上线,下线均可以依赖watcher机制,但是对于下线而言也可以通过心跳探活来实现,我们将两者皆保留。
二、添加配置类
到目前为止我们的所有的配置相关的内容全部定义在了启动引导程序中,这样其实有一些不合理,事实上全局配置我们应该统一放在一个类中,如下,让这个类将会成为我们当前工程的上下文环境:
@Data
@Slf4j
public class Configuration {
// 配置信息-->端口号
private int port = 8094;
// 配置信息-->应用程序的名字
private String appName = "default";
// 分组信息
private String group = "default";
// 配置信息-->注册中心
private RegistryConfig registryConfig = new RegistryConfig("zookeeper://127.0.0.1:2181");
// 配置信息-->序列化协议
private String serializeType = "jdk";
// 配置信息-->压缩使用的协议
private String compressType = "gzip";
// 配置信息-->id发射器
public IdGenerator idGenerator = new IdGenerator(1, 2);
// 配置信息-->负载均衡策略
private LoadBalancer loadBalancer = new RoundRobinLoadBalancer();
// 为每一个ip配置一个限流器
private final Map<SocketAddress, RateLimiter> everyIpRateLimiter = new ConcurrentHashMap<>(16);
// 为每一个ip配置一个断路器,熔断
private final Map<SocketAddress, CircuitBreaker> everyIpCircuitBreaker = new ConcurrentHashMap<>(16);
// 读xml,dom4j
public Configuration() {
// 1、成员变量的默认配置项
// 2、spi机制发现相关配置项
SpiResolver spiResolver = new SpiResolver();
spiResolver.loadFromSpi(this);
// 3、读取xml获得上边的信息
XmlResolver xmlResolver = new XmlResolver();
xmlResolver.loadFromXml(this);
// 4、编程配置项,yrpcBootstrap提供
}
}
我们看到了,在配置类的构造器中,整个配置项会从三个地方加载:
1、默认项
2、spi自动发现
3、xml解析
4、编程式配置