Soul网关(7) - 同步数据之Zookeeper

381 阅读7分钟

话不多说,上来就干

上一篇Soul网关—WebSocket同步数据—源码分析(六),我们通过源码,分析了soul网关的管理后台和soul网关之间的数据同步,这一篇,我们一起来分析一下通过ZooKeeper做数据同步的源码。

了解具体的数据同步之前,需要先了解同步配置和服务启动的时候,soul为了后续的同步数据都提前做了什么(提前剧透:就是提前准备好监听器,等进入使用状态时,网关可配置数据一旦有所更改,各司其职的监听器立刻开始处理自己的该做的事情)

环境配置

根据soul的官方文档,需要配置soul-admin模块的同步配置

soul:
  sync:  //同步策略配置
    zookeeper:  //同步数据采用zookeeper
        url: localhost:2181  //zookeeper地址
        sessionTimeout: 5000  //session超时时间
        connectionTimeout: 2000  //连接超时时间

admin服务初始化

由于我也是第一次读soul的源码,我们来看看soul-admin模块哪里用到了这个配置项,通过idea搜索,我们找到这个类 org.dromara.soul.admin.config.DataSyncConfiguration

/**
 * The type Data sync configuration.
 */
@Configuration
public class DataSyncConfiguration {

    /**
     * http long polling(default strategy).
     */
    @Configuration
    @ConditionalOnProperty(name = "soul.sync.http.enabled", havingValue = "true")
    @EnableConfigurationProperties(HttpSyncProperties.class)
    static class HttpLongPollingListener {...}

    /**
     * The type Zookeeper listener.
     */
    @Configuration
    @ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url")
    @Import(ZookeeperConfiguration.class)
    static class ZookeeperListener {...}

    /**
     * The type Nacos listener.
     */
    @Configuration
    @ConditionalOnProperty(prefix = "soul.sync.nacos", name = "url")
    @Import(NacosConfiguration.class)
    static class NacosListener {...}

    /**
     * The WebsocketListener(default strategy).
     */
    @Configuration
    @ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
    @EnableConfigurationProperties(WebsocketSyncProperties.class)
    static class WebsocketListener {...}
}

这是一个bean初始化配置类,我们可以看到,凡是在yml文件中soul.sync下可供配置的几种同步策略,都可以在这个类中找到使用的地方

类说明表示这是一个数据同步监听器初始化类,在soul-admin项目启动的时候,通过配置文件中不同的策略,将同步数据的初始化监听器注册到spring容器,这次我们关注的是zookeeper监听器初始化bean方法

    /**
     * The type Zookeeper listener.
     */
    @Configuration
    @ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url")
    @Import(ZookeeperConfiguration.class)
    static class ZookeeperListener {

        /**
         * Config event listener data changed listener.
         *
         * @param zkClient the zk client
         * @return the data changed listener
         */
        @Bean
        @ConditionalOnMissingBean(ZookeeperDataChangedListener.class)
        public DataChangedListener zookeeperDataChangedListener(final ZkClient zkClient) {
            return new ZookeeperDataChangedListener(zkClient);
        }

        /**
         * Zookeeper data init zookeeper data init.
         *
         * @param zkClient        the zk client
         * @param syncDataService the sync data service
         * @return the zookeeper data init
         */
        @Bean
        @ConditionalOnMissingBean(ZookeeperDataInit.class)
        public ZookeeperDataInit zookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
            return new ZookeeperDataInit(zkClient, syncDataService);
        }
    }

初始化了两个bean,一个是zookeeperDataInit,另一个是 zookeeperDataChangedListener

我们先来看zookeeperDataInit这个类

public class ZookeeperDataInit implements CommandLineRunner {

    private final ZkClient zkClient;

    private final SyncDataService syncDataService;

    /**
     * Instantiates a new Zookeeper data init.
     *
     * @param zkClient        the zk client
     * @param syncDataService the sync data service
     */
    public ZookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
        this.zkClient = zkClient;
        this.syncDataService = syncDataService;
    }

    @Override
    public void run(final String... args) {
        String pluginPath = ZkPathConstants.PLUGIN_PARENT;
        String authPath = ZkPathConstants.APP_AUTH_PARENT;
        String metaDataPath = ZkPathConstants.META_DATA;
        if (!zkClient.exists(pluginPath) && !zkClient.exists(authPath) && !zkClient.exists(metaDataPath)) {
            syncDataService.syncAll(DataEventTypeEnum.REFRESH);
        }
    }
}

实现了CommandLineRunner这个类,重写了run()方法,接口CommandLineRunner是SpringBoot内置接口,表明该接口的用意就是,表明凡是实现了该接口的实现类,当被spring初始为bean放在spring容器中时,显示的申明调用run方法

说的简单的就是当 ZookeeperDataInit这个类被初始化为bean的时候,会有一个初始化调用run方法,我们可以在run方法中初始化我们想预装配的数据

    @Override
    public void run(final String... args) {
        String pluginPath = ZkPathConstants.PLUGIN_PARENT;
        String authPath = ZkPathConstants.APP_AUTH_PARENT;
        String metaDataPath = ZkPathConstants.META_DATA;
        
        // 当zookeeper检测 plugin/auth/metaData 三个节点是否都不存在,都不存在的时候,进行数据初始化同步
        if (!zkClient.exists(pluginPath) && !zkClient.exists(authPath) && !zkClient.exists(metaDataPath)) {
            syncDataService.syncAll(DataEventTypeEnum.REFRESH);
        }
    }

点进syncAll()方法,我们可以看到这个方法就是从数据库读取全量的数据,然后发布出去

    @Override
    public boolean syncAll(final DataEventTypeEnum type) {
        appAuthService.syncData();

        //从数据库捞插件的全量数据
        List<PluginData> pluginDataList = pluginService.listAll();
        //发布插件的初始化全量数据
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, type, pluginDataList));

        //从数据库捞插件的全量数据
        List<SelectorData> selectorDataList = selectorService.listAll();
        //发布插件的初始化全量数据
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, type, selectorDataList));

        //从数据库捞插件的全量数据
        List<RuleData> ruleDataList = ruleService.listAll();
        //发布插件的初始化全量数据
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, type, ruleDataList));

        //发布从数据库捞出的全量元数据
        metaDataService.syncData();

        return true;
    }

其实有兴趣的小伙伴可以一直跟着eventPublisher.publishEvent()这个方法点进去,这个应用事件发布器,是spring内置的类,一直点进去,其实最终还是调用的监听者的监听方法,所以各个监听事件器可以监听到。这不就形成了发布者和订阅者的消息闭环了吗。

这是spring的发布器直接调用监听器的代码,最重要的一行listener.onApplicationEvent(event),这里的监听器,开发者自己先实现类继承spring的应用事件监听器

	@SuppressWarnings({"rawtypes", "unchecked"})
	private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
		try {
        
        //调用监听器的监听事件方法,用户可以重写onApplicationEvent(event)方法,在里面实现用户自己监听到事件后的处理逻辑
			listener.onApplicationEvent(event);
		}
		catch (ClassCastException ex) {
			String msg = ex.getMessage();
			if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
				// Possibly a lambda-defined listener which we could not resolve the generic event type for
				// -> let's suppress the exception and just log a debug message.
				Log logger = LogFactory.getLog(getClass());
				if (logger.isTraceEnabled()) {
					logger.trace("Non-matching event type for listener: " + listener, ex);
				}
			}
			else {
				throw ex;
			}
		}
	}

我们再回来看ZookeeperDataChangedListener这个类,这个类在项目启动的时候,并没做啥具体的事情,但是这个接口是后面增量数据更新的核心监听器。这个监听类实现了接口DataChangedListener,而这个接口看名字就知道是数据更新监听接口,看了一下这个接口在soul中的实现类,一共有四种,正好对应官方文档中定义的四种数据同步通知策略:

  • http长连接
  • websocket
  • zookeeper
  • nacos 这个接口同样需要实现四个维度数据改变后,监听器需要执行的具体操作,分别是权限、插件、选择器还有元数据
public interface DataChangedListener {

    /**
     * invoke this method when AppAuth was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onAppAuthChanged(List<AppAuthData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * invoke this method when Plugin was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onPluginChanged(List<PluginData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * invoke this method when Selector was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onSelectorChanged(List<SelectorData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * On meta data changed.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onMetaDataChanged(List<MetaData> changed, DataEventTypeEnum eventType) {

    }

    /**
     * invoke this method when Rule was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onRuleChanged(List<RuleData> changed, DataEventTypeEnum eventType) {
    }

}

这个监听接口,应该是soul内部配置数据监听的核心顶层接口

已经有了具体策略和模块的监听器,那么一定有触发这个监听器的地方,从上一篇Soul网关—WebSocket同步数据—源码分析(六)文中可以知道,页面操作后,最终数据是通过发布者发布DataChangedEvent事件,有一个事件更新调度者DataChangedEventDispatcher通过监听DataChangedEvent,来进行更新事件调度分发,DataChangedEventDispatcher这个调度者实现了ApplicationListener<DataChangedEvent>, InitializingBean两个接口,在项目的开始就初始化完成

//事件更新调度者类
@Component
public class DataChangedEventDispatcher implements ApplicationListener<DataChangedEvent>, InitializingBean {

    private ApplicationContext applicationContext;

    private List<DataChangedListener> listeners;

    public DataChangedEventDispatcher(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }


//监听到具体时间,然后调度分发到具体的模块和监听器
    @Override
    @SuppressWarnings("unchecked")
    public void onApplicationEvent(final DataChangedEvent event) {
        for (DataChangedListener listener : listeners) {
            switch (event.getGroupKey()) {
                case APP_AUTH:
                    listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
                    break;
                case PLUGIN:
                    listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
                    break;
                case RULE:
                    listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
                    break;
                case SELECTOR:
                    listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
                    break;
                case META_DATA:
                    listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
                    break;
                default:
                    throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
            }
        }
    }

    @Override
    public void afterPropertiesSet() {
        Collection<DataChangedListener> listenerBeans = applicationContext.getBeansOfType(DataChangedListener.class).values();
        this.listeners = Collections.unmodifiableList(new ArrayList<>(listenerBeans));
    }

}

以switch到rule模块,注入的是在启动时已经初始化好的bean:ZookeeperDataChangedListener 直接调用ZookeeperDataChangedListeneronRuleChanged()方法

    @Override
    public void onRuleChanged(final List<RuleData> changed, final DataEventTypeEnum eventType) {
        if (eventType == DataEventTypeEnum.REFRESH) {
            final String selectorParentPath = ZkPathConstants.buildRuleParentPath(changed.get(0).getPluginName());
            deleteZkPathRecursive(selectorParentPath);
        }
        for (RuleData data : changed) {
            final String ruleRealPath = ZkPathConstants.buildRulePath(data.getPluginName(), data.getSelectorId(), data.getId());
            //若是删除事件,直接删除节点
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(ruleRealPath);
                continue;
            }
            final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getPluginName());
            createZkNode(ruleParentPath);
            //create or update  更新zookeeper节点信息
            upsertZkNode(ruleRealPath, data);
        }
    }

soul-bootstrap服务初始化

soul-bootstrap服务初始中有关网关数据的监听器和zookeeper策略和admin类似,订阅到相关数据进行相应的操作,订阅者CommonPluginDataSubscriber中的subscribeDataHandler()方法,就是进行插件热插拔等一些操作的方法

    private <T> void subscribeDataHandler(final T classData, final DataEventTypeEnum dataType) {
        Optional.ofNullable(classData).ifPresent(data -> {
            if (data instanceof PluginData) {
                PluginData pluginData = (PluginData) data;
                // 插件更新事件
                if (dataType == DataEventTypeEnum.UPDATE) {
                    BaseDataCache.getInstance().cachePluginData(pluginData);
                    //这里的hander实现PluginDataHandler接口,根据实现类不一样,进行区别化更新插件
                    Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.handlerPlugin(pluginData));
                    //删除插件事件
                } else if (dataType == DataEventTypeEnum.DELETE) {
                    BaseDataCache.getInstance().removePluginData(pluginData);
                    Optional.ofNullable(handlerMap.get(pluginData.getName())).ifPresent(handler -> handler.removePlugin(pluginData));
                }
            } else if (data instanceof SelectorData) {
                SelectorData selectorData = (SelectorData) data;
                if (dataType == DataEventTypeEnum.UPDATE) {
                    BaseDataCache.getInstance().cacheSelectData(selectorData);
                    Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.handlerSelector(selectorData));
                } else if (dataType == DataEventTypeEnum.DELETE) {
                    BaseDataCache.getInstance().removeSelectData(selectorData);
                    Optional.ofNullable(handlerMap.get(selectorData.getPluginName())).ifPresent(handler -> handler.removeSelector(selectorData));
                }
            } else if (data instanceof RuleData) {
                RuleData ruleData = (RuleData) data;
                if (dataType == DataEventTypeEnum.UPDATE) {
                    BaseDataCache.getInstance().cacheRuleData(ruleData);
                    Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.handlerRule(ruleData));
                } else if (dataType == DataEventTypeEnum.DELETE) {
                    BaseDataCache.getInstance().removeRuleData(ruleData);
                    Optional.ofNullable(handlerMap.get(ruleData.getPluginName())).ifPresent(handler -> handler.removeRule(ruleData));
                }
            }
        });
    }

同步数据

通过了解,我们可以清晰的明确了soul中数据同步的数据链,admin页面数据更新->数据更新事件发布者->调度器->不同监听器采取不同的数据处理逻辑->zookeeper维护节点数据->插件订阅者订阅消息->根据事件名称和插件名称不同进行热插拔负载均衡等操作