Nacos中的事件驱动模型

860 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

最近在阅读一些框架源码的时候,会发现越来越多的基于事件驱动的代码实现。并且在上一篇nacos注册中心源码的最后,关于服务发现功能的订阅是如何接收到实例变化的消息并触发onEvent方法只是做了简单是描述。这块源码的实现其实就涉及到了事件驱动模型的代码实现。

1-事件驱动简述

事件驱动一般包含几个要素:事件、事件发布者、订阅者。发布者和订阅者之间的事件通道一般会使用队列来实现。下面直接通过源码来看看在Nacos中是如何实现的。

2-NacosNamingService 

我们在写订阅者demo的时候在一开始就先创建了一个NacosNamingService。

private void init(Properties properties) throws NacosException {
	ValidatorUtils.checkInitParam(properties);
	this.namespace = InitUtils.initNamespaceForNaming(properties);
	InitUtils.initSerialization();
	InitUtils.initWebRootContext(properties);
	initLogName(properties);

	this.notifierEventScope = UUID.randomUUID().toString();
        //定义事件相关的要素
	this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
	NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
	NotifyCenter.registerSubscriber(changeNotifier);
	this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, properties);
	this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
}

上面的代码通NotifyCenter来完成了两个注册:

1-registerToPublisher:注册事件发布者

InstancesChangeEvent这个事件类型注册了一个EventPublisher:NamingEventPublisher。并且维护在了单列模式NotifyCenter中的publisherMap中。

private final Map<String, EventPublisher> publisherMap = new ConcurrentHashMap<>(16);

2-registerSubscriber:注册订阅者

这一步最终就是给NamingEventPublisher注册相关的订阅者:

InstancesChangeNotifier extends Subscriber<InstancesChangeEvent> 

最终在其map结构中去维护,key则是订阅事件的类型InstancesChangeEvent。

public void addSubscriber(Subscriber subscriber, Class<? extends Event> subscribeType) {
	subscribes.computeIfAbsent(subscribeType, inputType -> new ConcurrentHashSet<>());
	subscribes.get(subscribeType).add(subscriber);
}

private final Map<Class<? extends Event>, Set<Subscriber<? extends Event>>> subscribes = new ConcurrentHashMap<>();

3-PushReceiver

在上一篇已经知道了Nacos服务端是基于PushService来通知订阅者,底层是基于UDP的推送。

那么在客户端就会有一个Receiver,就是PushReceiver。当接收到消息时会调用:

serviceInfoHolder.processServiceInfo(pushPacket.data);

public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
	String serviceKey = serviceInfo.getKey();
	if (serviceKey == null) {
		return null;
	}
	ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
	if (isEmptyOrErrorPush(serviceInfo)) {
		//empty or error push, just ignore
		return oldService;
	}
	serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
	boolean changed = isChangedServiceInfo(oldService, serviceInfo);
	if (StringUtils.isBlank(serviceInfo.getJsonFromServer())) {
		serviceInfo.setJsonFromServer(JacksonUtils.toJson(serviceInfo));
	}
	MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
	if (changed) {
		NAMING_LOGGER.info("current ips:({}) service: {} -> {}", serviceInfo.ipCount(), serviceInfo.getKey(),
				JacksonUtils.toJson(serviceInfo.getHosts()));
		NotifyCenter.publishEvent(new InstancesChangeEvent(notifierEventScope, serviceInfo.getName(), serviceInfo.getGroupName(),
				serviceInfo.getClusters(), serviceInfo.getHosts()));
		DiskCache.write(serviceInfo, cacheDir);
	}
	return serviceInfo;
}

上面的代码中如果判断了changed则会调用NotifyCenter的publishEvent方法,并且event类型就是InstancesChangeEvent

private static boolean publishEvent(final Class<? extends Event> eventType, final Event event) {
	if (ClassUtils.isAssignableFrom(SlowEvent.class, eventType)) {
		return INSTANCE.sharePublisher.publish(event);
	}
	
	final String topic = ClassUtils.getCanonicalName(eventType);
	
	EventPublisher publisher = INSTANCE.publisherMap.get(topic);
	if (publisher != null) {
		return publisher.publish(event);
	}
	if (event.isPluginEvent()) {
		return true;
	}
	LOGGER.warn("There are no [{}] publishers for this event, please register", topic);
	return false;
}

这里通过InstancesChangeEvent类型获取的EventPublisher就是在上面注册的NamingEventPublisher,最终调用了其publish方法**.**

3-NamingEventPublisher

publish方法的实现:

this.queue = new ArrayBlockingQueue<>(bufferSize);

public boolean publish(Event event) {
	checkIsStart();
        //添加event到阻塞队列
	boolean success = this.queue.offer(event);
	if (!success) {
		Loggers.EVT_LOG.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
		handleEvent(event);
		return true;
	}
	return true;
}

就是像阻塞队列中添加了当前的event。那么肯定有地方去消费这个队列:

NamingEventPublisher extends Thread 

NamingEventPublisher本身继承了Thread,并且在其init方法中调用了start方法,那么我们可以直接在当前类中找到run方法:

public void run() {
	try {
		waitSubscriberForInit();
		handleEvents();
	} catch (Exception e) {
		Loggers.EVT_LOG.error("Naming Event Publisher {}, stop to handle event due to unexpected exception: ",
				this.publisherName, e);
	}
}

private void handleEvents() {
	while (!shutdown) {
		try {
                        //从队列中获取event
			final Event event = queue.take();
			handleEvent(event);
		} catch (InterruptedException e) {
			Loggers.EVT_LOG.warn("Naming Event Publisher {} take event from queue failed:", this.publisherName, e);
			// set the interrupted flag
			Thread.currentThread().interrupt();
		}
	}
}

从队列中获取到event后然后就可以通知前面注册的订阅者了:

private void handleEvent(Event event) {
	Class<? extends Event> eventType = event.getClass();
	Set<Subscriber<? extends Event>> subscribers = subscribes.get(eventType);
	if (null == subscribers) {
		if (Loggers.EVT_LOG.isDebugEnabled()) {
			Loggers.EVT_LOG.debug("[NotifyCenter] No subscribers for slow event {}", eventType.getName());
		}
		return;
	}
	for (Subscriber subscriber : subscribers) {
		notifySubscriber(subscriber, event);
	}
}

这里同样是根据event事件的类型找到对应的subscriber:InstancesChangeNotifier。而方法notifySubscriber就会调用InstancesChangeNotifier中的onEvent方法:

public void onEvent(InstancesChangeEvent event) {
	String key = ServiceInfo
			.getKey(NamingUtils.getGroupedName(event.getServiceName(), event.getGroupName()), event.getClusters());
	ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
	if (CollectionUtils.isEmpty(eventListeners)) {
		return;
	}
	for (final EventListener listener : eventListeners) {
		final com.alibaba.nacos.api.naming.listener.Event namingEvent = transferToNamingEvent(event);
		if (listener instanceof AbstractEventListener && ((AbstractEventListener) listener).getExecutor() != null) {
			((AbstractEventListener) listener).getExecutor().execute(() -> listener.onEvent(namingEvent));
		} else {
			listener.onEvent(namingEvent);
		}
	}
}

这里的EventListener就是我们一开始在demo中调用subscribe方法来添加到InstancesChangeNotifier中的listenerMap中一个监听器,所以最终就调用到了demo中我们自己实现的onEvent方法:

naming.subscribe("nacos.test.3", new AbstractEventListener() {
	//EventListener onEvent is sync to handle, If process too low in onEvent, maybe block other onEvent callback.
	//So you can override getExecutor() to async handle event.
	@Override
	public Executor getExecutor() {
		return executor;
	}
	
	@Override
	public void onEvent(Event event) {
		System.out.println("serviceName: " + ((NamingEvent) event).getServiceName());
		System.out.println("instances from event: " + ((NamingEvent) event).getInstances());
	}
});

//subscribe添加的实现往InstancesChangeNotifier中加入的listener
public void subscribe(String serviceName, String groupName, List<String> clusters, EventListener listener)
		throws NacosException {
	if (null == listener) {
		return;
	}
	String clusterString = StringUtils.join(clusters, ",");
	changeNotifier.registerListener(groupName, serviceName, clusterString, listener);
	clientProxy.subscribe(serviceName, groupName, clusterString);
}

4-总结

到此,我们基本上已经将最简单的源码进行了闭环。上面主要涉及到的类总结如下:

InstancesChangeEvent:事件类型 

NacosNamingService:注册事件相关的信息初始化

PushReceiver :接收服务端消息,基于NamingEventPublisher发布InstancesChangeEvent事件

NamingEventPublisher:发布事件者,本身是一个线程。添加发布的事件到阻塞队列,并且异步不停的轮训阻塞队列中事件交给对应的subscriber进行处理。

InstancesChangeNotifier:订阅者,根据向其注册的EventListener来调用我们自己的实现

基于这种思想,可以发现其具有可扩展,耦合性低的特点。熟悉了这一种事件的调用情况,在看源码中其他事件就可以使用相同的思路了。