前提摘要
- 服务端与客户端通信的host与port是预先就确定好的,每一个客户端都必须知道对应服务的ip与端口号, 并且如果服务挂了或者换地址了,就很麻烦。扩展性也不强, 所以我们需要引入注册中心
- 在RPC架构中,一般一个服务会有多个提供者支持,如何分散服务提供者的压力?引入负载均衡
RPC三大要素客户端和服务端我们都已经有了,现在就只剩下注册中心这最后一块拼图了!Let's go!🥳
- Zookeeper
注册中心的地址是固定的(为了高可用一般是集群,我们看做黑盒即可),服务端上线时,在注册中心注册自己的服务与对应的地址,而客户端调用服务时,去注册中心根据服务名找到对应的服务端地址。
- Curator
Curator是一套Zookeeper客户端框架,解决了很多 Zookeeper 客户端非常底层的细节开发工作,这次项目的升级便基于此。
Zookeeper注册中心
首先在本机安装Zookeeper,默认端口为2181。
定义服务注册接口
// 服务注册接口,两大基本功能,注册:保存服务与地址。 查询:根据服务名查找地址
public interface ServiceRegister {
void register(String serviceName, InetSocketAddress serverAddress);
InetSocketAddress serviceDiscovery(String serviceName);
}
服务注册接口的实现类为
public class ZkServiceRegister implements ServiceRegister {
// curator 提供的zookeeper客户端
private CuratorFramework client;
// zookeeper根路径节点
private static final String ROOT_PATH = "MyRPC";
// 这里负责zookeeper客户端的初始化,并与zookeeper服务端建立连接
public ZkServiceRegister(){
// 指数时间重试
RetryPolicy policy = new ExponentialBackoffRetry(1000, 3);
// zookeeper的地址固定,不管是服务提供者还是,消费者都要与之建立连接
// sessionTimeoutMs 与 zoo.cfg中的tickTime 有关系,
// zk还会根据minSessionTimeout与maxSessionTimeout两个参数重新调整最后的超时值。默认分别为tickTime 的2倍和20倍
// 使用心跳监听状态
this.client = CuratorFrameworkFactory.builder().connectString("127.0.0.1:2181")
.sessionTimeoutMs(40000).retryPolicy(policy).namespace(ROOT_PATH).build();
this.client.start();
System.out.println("zookeeper 连接成功");
}
@Override
public void register(String serviceName, InetSocketAddress serverAddress){
try {
// serviceName创建成永久节点,服务提供者下线时,不删服务名,只删地址
if(client.checkExists().forPath("/" + serviceName) == null){
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath("/" + serviceName);
}
// 路径地址,一个/代表一个节点
String path = "/" + serviceName +"/"+ getServiceAddress(serverAddress);
// 临时节点,服务器下线就删除节点
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
} catch (Exception e) {
System.out.println("此服务已存在");
}
}
// 根据服务名返回地址
@Override
public InetSocketAddress serviceDiscovery(String serviceName) {
try {
List<String> strings = client.getChildren().forPath("/" + serviceName);
// 这里默认用的第一个,后面加负载均衡
String string = strings.get(0);
return parseAddress(string);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
// 地址 -> XXX.XXX.XXX.XXX:port 字符串
private String getServiceAddress(InetSocketAddress serverAddress) {
return serverAddress.getHostName() +
":" +
serverAddress.getPort();
}
// 字符串解析为地址
private InetSocketAddress parseAddress(String address) {
String[] result = address.split(":");
return new InetSocketAddress(result[0], Integer.parseInt(result[1]));
}
}
客户端升级
实例客户端的时候不再需要传入host,post的值了,实例在发送请求即sendRequest的过程中会自动通过ZkServiceRegister获取到zookeeper注册中心获取到当前服务需要发送到地址(服务提供端的所在地。
\\不再需要传入host,post
RPCClient rpcClient = new NettyRPCClient();
\\获取注册中心存储的信息
InetSocketAddress address = serviceRegister.serviceDiscovery(request.getInterfaceName());
host = address.getHostName();
port = address.getPort();
服务端升级
客户端不需要再写入host,post了,相应的服务端就需要给注册中心传它能提供的服务以及它的host,port。
ServiceProvider serviceProvider = new ServiceProvider("127.0.0.1", 8899);
在服务提供类加入注册的功能
public class ServiceProvider {
private Map<String, Object> interfaceProvider;
private String host;
private int port;
private ServiceRegister serviceRegister;
public ServiceProvider(String host, int port){
this.host = host;
this.port = port;
this.serviceRegister = new ZkServiceRegister();
this.interfaceProvider = new HashMap<>();
}
public void provideServiceInterface(Object service){
Class<?>[] interfaces = service.getClass().getInterfaces();
for(Class clazz : interfaces){
interfaceProvider.put(clazz.getName(),service);
// 在注册中心注册服务
serviceRegister.register(clazz.getName(),new InetSocketAddress(host,port));
}
}
public Object getService(String interfaceName){
return interfaceProvider.get(interfaceName);
}
}
负载均衡
public InetSocketAddress serviceDiscovery(String serviceName) {
try {
List<String> strings = client.getChildren().forPath("/" + serviceName);
// 这里默认用的第一个,后面加负载均衡
String string = strings.get(0);
return parseAddress(string);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
上面的代码中其实还存在一个小问题,每次我们客户端问注册中心要服务对应的地址的时候,永远是给第一个服务者的地址,这显然是不合理的,我们需要分散服务提供者的压力。
- 负载均衡接口
// 给服务器地址列表,根据不同的负载均衡策略选择一个
public interface LoadBalance {
String balance(List<String> addressList);
}
/**
* 随机负载均衡
*/
public class RandomLoadBalance implements LoadBalance{
@Override
public String balance(List<String> addressList) {
Random random = new Random();
int choose = random.nextInt(addressList.size());
System.out.println("负载均衡选择了" + choose + "服务器");
return addressList.get(choose);
}
}
/**
* 轮询负载均衡
*/
public class RoundLoadBalance implements LoadBalance{
private int choose = -1;
@Override
public String balance(List<String> addressList) {
choose++;
choose = choose%addressList.size();
return addressList.get(choose);
}
}
本次升级就告一段落了🥳🥳🥳,现代码已经上传至github.com/zhangyuxuan…