本节分成三次提交。
将服务注册与服务实现分开
commit地址:5128986
在上一节中我们实现了Nacos服务注册中心,为了让逻辑更清晰,现在将Nacos相关操作都放在NacosUtil工具类:
public class NacosUtil {
21
22 private static final Logger logger = LoggerFactory.getLogger(NacosUtil.class);
23
24 private static final String SERVER_ADDR = "127.0.0.1:8848";
25
26 /**
27 * @description 连接到Nacos创建命名空间
31 */
32 public static NamingService getNacosNamingService() {
33 try {
34 return NamingFactory.createNamingService(SERVER_ADDR);
35 }catch (NacosException e) {
36 logger.error("连接到Nacos时有错误发生:", e);
37 throw new RpcException(RpcError.FAILED_TO_CONNECT_TO_SERVICE_REGISTRY);
38 }
39 }
40
41 /**
42 * @description 注册服务到Nacos
46 */
47 public static void registerService(NamingService namingService, String serviceName, InetSocketAddress inetSocketAddress) throws NacosException {
48 namingService.registerInstance(serviceName, inetSocketAddress.getHostName(), inetSocketAddress.getPort());
49 }
50
51 /**
52 * @description 获取所有提供该服务的服务端地址
56 */
57 public static List<Instance> getAllInstance(NamingService namingService, String serviceName) throws NacosException {
58 return namingService.getAllInstances(serviceName);
59 }
60 }
然后将服务注册与服务发现NacosServiceDiscovery分开,核心代码如下:
public class NacosServiceDiscovery implements ServiceDiscovery{
19
20 private static final Logger logger = LoggerFactory.getLogger(NacosServiceDiscovery.class);
21 private final NamingService namingService;
22
23 public NacosServiceDiscovery(){
24 namingService = NacosUtil.getNacosNamingService();
25 }
26
27 /**
28 * @description 根据服务名称从注册中心获取到一个服务提供者的地址
32 */
33 @Override
34 public InetSocketAddress lookupService(String serviceName) {
35 try {
36 //利用列表获取某个服务的所有提供者
37 List<Instance> instances = NacosUtil.getAllInstance(namingService, serviceName);
38 Instance instance = instances.get(0);
39 return new InetSocketAddress(instance.getIp(), instance.getPort());
40 }catch (NacosException e) {
41 logger.error("获取服务时有错误发生", e);
42 }
43 return null;
44 }
45 }
实现注销服务的钩子
commit地址:060270f
上一节我们实现了服务的自动注册和发现,但其实还存在一个问题,如果服务端突然关闭了,并不会自动注销Nacos中对应的服务信息,这样就会导致客户端再次向Nacos请求服务时,可能获取到已经关闭的服务端地址,最终因为连接不到服务器而调用失败。所以我们要想个办法,在服务端关闭之前自动向Nacos注销服务。其中核心要解决的问题就是,我们不知道什么时候服务器会关闭,也就不知道这个方法调用的时机,就没有办法手工去调用。这时,我们就需要钩子。
什么是钩子?是在某些事件发生后自动去调用的方法,那么我们只需要把注销服务的方法写到关闭系统的钩子方法里就行了。
首先实现向Nacos注销所有服务的方法,写在了NacosUtil工具类中:
public static void clearRegistry() {
//所有的服务名称都被存储在serviceNames中
if(!serviceNames.isEmpty() && address != null) {
String host = address.getHostName();
int port = address.getPort();
//利用迭代器迭代注销
Iterator<String> iterator = serviceNames.iterator();
while (iterator.hasNext()) {
String serviceName = iterator.next();
try {
//注销服务
namingService.deregisterInstance(serviceName, host, port);
}catch (NacosException e) {
logger.error("注销服务{}失败", serviceName, e);
}
}
}
}
接着就是实现钩子,新建一个类,ShutdownHook:(单例模式了解戳:什么是单例模式?)
public class ShutdownHook {
16
17 private static final Logger logger = LoggerFactory.getLogger(ShutdownHook.class);
18
19 private final ExecutorService threadPool = ThreadPoolFactory.createDefaultThreadPool("shutdown-hook");
20 /**
21 *单例模式创建钩子,保证全局只有这一个钩子
22 */
23 private static final ShutdownHook shutdownHook = new ShutdownHook();
24
25 public static ShutdownHook getShutdownHook(){
26 return shutdownHook;
27 }
28
29 //注销服务的钩子
30 public void addClearAllHook() {
31 logger.info("服务端关闭前将自动注销所有服务");
32 Runtime.getRuntime().addShutdownHook(new Thread(() -> {
33 NacosUtil.clearRegistry();
34 threadPool.shutdown();
35 }));
36 }
37 }
在addClearAllHook()中,Runtime对象是JVM虚拟机的运行时环境,调用其addShutdownHook()方法增加一个钩子函数,创建一个新线程调用clearRegistry()完成注销工作。这个钩子函数会在JVM关闭之前被调用。这样只需要把钩子放在服务端,启动服务端时就能注册钩子了,以NettyServer为例,启动服务端后再关闭,就会发现Nacos中的注册信息已经被注销了。
ChannelFuture future = serverBootstrap.bind(host, port).sync();
ShutdownHook.getShutdownHook().addClearAllHook();
future.channel().closeFuture().sync();
线程池优化
commit地址:f616ba8
在框架中多个地方用到线程池,因此考虑进行统一管理,利用Map,线程池前缀名作为Key,线程为Value:
public static ExecutorService createDefaultThreadPool(String threadNamePrefix, Boolean daemon){
37 //computeIfAbsent():如果key对应的value存在,则直接返回value,如果不存在则使用第二个参数(函数)计算的值作为value返回,并保存为该key的value
38 ExecutorService pool = threadPoolMap.computeIfAbsent(threadNamePrefix, k -> createThreadPool(threadNamePrefix, daemon));
39 //isShutdown():当调用shutdown()或shutdownNow()方法后返回为true
40 //isTerminated():当调用shutdown()方法后,并且所有提交的任务完成后返回为true;当调用shutdownNow()方法后,成功停止后返回为true;
41 if(pool.isShutdown() || pool.isTerminated()){
42 threadPoolMap.remove(threadNamePrefix);
43 //重新构建一个线程池并存入Map中
44 pool = createThreadPool(threadNamePrefix, daemon);
45 threadPoolMap.put(threadNamePrefix, pool);
46 }
47 return pool;
48 }
49
50 public static void shutDownAll(){
51 logger.info("关闭所有线程池……");
52 //利用parallelStream()并行关闭所有线程池
53 threadPoolMap.entrySet().parallelStream().forEach(entry -> {
54 ExecutorService executorService = entry.getValue();
55 executorService.shutdown();
56 logger.info("关闭线程池 [{}] [{}]", entry.getKey(), executorService.isTerminated());
57 try {
58 //阻塞直到关闭请求后所有任务执行完,或者发生超时,或者当前线程被中断(以先发生者为准)。
59 executorService.awaitTermination(10, TimeUnit.SECONDS);
60 }catch (InterruptedException e){
61 logger.error("关闭线程池失败");
62 //直接关闭不等任务执行完了
63 executorService.shutdownNow();
64 }
65 });
66 }
关闭所有线程池的shutDownAll()方法也在ShutdownHook钩子中调用,当服务端关闭的时候,会注销所有服务,同时关闭所有线程池。其次就是关于序列化器创建方式的优化,这部分不做展开讲解了,代码较分散逻辑简单。
本节over……