目录

1.前情回顾

2.配置

2.1 soul-admin

2.2 soul-bootstrap

3.启动

3.1 启动 zookeeper

3.2 启动 soul-admin

3.2.1 注入 ZookeeperDataChangedListener 对象

3.2.2 注入 ZookeeperDataInit 对象


1.前情回顾

紧接着昨天的 websocket,今天来看下 zookeeper。

想知道websocket 相关的出门左转【Soul源码阅读】9.soul-admin 与 soul-bootstrap 同步机制之 websocket 解析

2.配置

数据同步策略官网链接 https://dromara.org/zh-cn/docs/soul/user-dataSync.html

因为数据同步方式是双方规定的,必须保持一致,所以修改配置也是 soul-admin 和 soul-bootstrap 都要改。

其实配置都是类似的,之前默认是 websocket,需要把 websocket 相关的配置注释掉,打开 zookeeper 相关配置。

2.1 soul-admin

# 修改前
soul:
  sync:
    websocket:
      enabled: true
#      zookeeper:
#          url: localhost:2181
#          sessionTimeout: 5000
#          connectionTimeout: 2000

# 修改后
soul:
  sync:
#    websocket:
#      enabled: true
    zookeeper:
      url: localhost:2181
      sessionTimeout: 5000
      connectionTimeout: 2000

配置文件对应 Bean:

@Data
@ConfigurationProperties(prefix = "soul.sync.zookeeper")
public class ZookeeperProperties {

    private String url;

    private Integer sessionTimeout;

    private Integer connectionTimeout;

    private String serializer;
}

2.2 soul-bootstrap

application-local.yml

# 修改前
soul:
    sync:
        websocket :
             urls: ws://localhost:9095/websocket
#        zookeeper:
#             url: localhost:2181
#             sessionTimeout: 5000
#             connectionTimeout: 2000

# 修改后
soul:
    sync:
#        websocket :
#             urls: ws://localhost:9095/websocket
        zookeeper:
             url: localhost:2181
             sessionTimeout: 5000
             connectionTimeout: 2000

配置文件对应 Bean:ZookeeperConfig

@Data
@ConfigurationProperties(prefix = "soul.sync.zookeeper")
public class ZookeeperConfig {

    private String url;

    private Integer sessionTimeout;

    private Integer connectionTimeout;

    private String serializer;
}

3.启动

3.1 启动 zookeeper

3.2 启动 soul-admin

基于昨天的分析,启动前可以在 DataSyncConfiguration.ZookeeperListener 里2个方法都打上断点,启动时一定会进。

// DataSyncConfiguration.java
@Configuration
public class DataSyncConfiguration {

...

    /**
     * The type Zookeeper listener.
     */
    @Configuration
    @ConditionalOnProperty(prefix = "soul.sync.zookeeper", name = "url")
    @Import(ZookeeperConfiguration.class)
    static class ZookeeperListener {
        @Bean
        @ConditionalOnMissingBean(ZookeeperDataChangedListener.class)
        public DataChangedListener zookeeperDataChangedListener(final ZkClient zkClient) {
            // 1.返回基于 zookeeper 的监听器对象
            return new ZookeeperDataChangedListener(zkClient);
        }
        @Bean
        @ConditionalOnMissingBean(ZookeeperDataInit.class)
        public ZookeeperDataInit zookeeperDataInit(final ZkClient zkClient, final SyncDataService syncDataService) {
            // 2.返回 zookeeper 初始化对象
            return new ZookeeperDataInit(zkClient, syncDataService);
        }
    }

...

}

3.2.1 注入 ZookeeperDataChangedListener 对象

1.返回基于 zookeeper 的监听器对象,并且注册到 spring 容器中,方便我们后续处理监听事件时使用。

2. ZookeeperDataInit,看名字是 zookeeper 数据的初始化,进到类里一探究竟。

3.2.2 注入 ZookeeperDataInit 对象

/**
 * The type Zookeeper data init.
 *
 * @author xiaoyu
 */
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)) {
            // 只有3个节点都不存在时,才同步全量数据
            syncDataService.syncAll(DataEventTypeEnum.REFRESH);
        }
    }
}

这个类实现了 CommandLineRunner 这个接口,并覆写了 run 方法。

run 方法会在 spring 启动容器后被调用。

此时会判断 zk 中是否存在3个节点,只有3个节点都不存在时,会同步全量数据。(第一次使用 zk,会同步全量数据)

// SyncDataServiceImpl.java
    @Override
    public boolean syncAll(final DataEventTypeEnum type) {
        // 同步 appAuth 数据
        appAuthService.syncData();
        // 同步 plugin 数据
        // 从 plugin 表中获取所有数据
        List<PluginData> pluginDataList = pluginService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.PLUGIN, type, pluginDataList));
        // 同步 selector 数据
        // 从 selector 表中获取所有数据
        List<SelectorData> selectorDataList = selectorService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.SELECTOR, type, selectorDataList));
        // 同步 rule 数据
        // 从 rule 表中获取所有数据
        List<RuleData> ruleDataList = ruleService.listAll();
        eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.RULE, type, ruleDataList));
        // 同步 metaData 数据
        metaDataService.syncData();
        return true;
    }

1. 同步 appAuth 数据

// AppAuthServiceImpl.java
    @Override
    public SoulAdminResult syncData() {
        // 从 app_auth 表中获取所有数据
        List<AppAuthDO> appAuthDOList = appAuthMapper.selectAll();
        if (CollectionUtils.isNotEmpty(appAuthDOList)) {
            List<AppAuthData> dataList = appAuthDOList.stream().map(this::buildByEntity).collect(Collectors.toList());
            eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.APP_AUTH,
                    DataEventTypeEnum.REFRESH,
                    dataList));
        }
        return SoulAdminResult.success();
    }

如果数据不为空,发布事件(ConfigGroupEnum.APP_AUTH + DataEventTypeEnum.REFRESH)

2. 同步 pulgin 数据

发布事件(ConfigGroupEnum.PLUGIN + DataEventTypeEnum.REFRESH)

3. 同步 selector 数据

发布事件(ConfigGroupEnum.SELECTOR + DataEventTypeEnum.REFRESH)

4. 同步 rule 数据

发布事件(ConfigGroupEnum.RULE + DataEventTypeEnum.REFRESH)

5. 同步 metaData 数据

// MetaDataServiceImpl.java 
   @Override
    public void syncData() {
        // 从 meta_data 表中获取所有数据
        List<MetaDataDO> all = metaDataMapper.findAll();
        if (CollectionUtils.isNotEmpty(all)) {
            eventPublisher.publishEvent(new DataChangedEvent(ConfigGroupEnum.META_DATA, DataEventTypeEnum.REFRESH, MetaDataTransfer.INSTANCE.mapToDataAll(all)));
        }
    }

发布事件(ConfigGroupEnum.META_DATA + DataEventTypeEnum.REFRESH)

发布事件后,就又回到了昨天就分析过的事件处理分发中心 DataChangedEventDispatcher,而这里的 listeners 就包含上面 3.2 中实例化的 ZookeeperDataChangedListener 对象。

// DataChangedEventDispatcher.java
    @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());
            }
        }
    }

在这里,虽然 websocket 配置文件注释掉了,但是 websocket 作为默认策略,即使没有配置也会注入。(@ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true) )

// DataSyncConfiguration.java
    @Configuration
    @ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
    @EnableConfigurationProperties(WebsocketSyncProperties.class)
    static class WebsocketListener

所以在 listeners 里会有2个实例,一个是 websocket 的,一个是 zookeeper 的。

这里 websocket 没有长连接,所以即使处理,在最后执行时由于 Set<Session> SESSION_SET 为空,也不会发送什么信息。

我们着重看下 zookeeper 是怎么处理的,ZookeeperDataChangedListener 实现了 DataChangedListener 接口,所以对于5种数据变更的处理都有其方法。

/**
 * Use zookeeper to push data changes.
 *
 * @author huangxiaofeng
 * @author xiaoyu
 */
public class ZookeeperDataChangedListener implements DataChangedListener {

    private final ZkClient zkClient;

    public ZookeeperDataChangedListener(final ZkClient zkClient) {
        this.zkClient = zkClient;
    }

    @Override
    public void onAppAuthChanged(final List<AppAuthData> changed, final DataEventTypeEnum eventType) {
        for (AppAuthData data : changed) {
            final String appAuthPath = ZkPathConstants.buildAppAuthPath(data.getAppKey());
            // delete
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(appAuthPath);
                continue;
            }
            // create or update
            upsertZkNode(appAuthPath, data);
        }
    }

    @SneakyThrows
    @Override
    public void onMetaDataChanged(final List<MetaData> changed, final DataEventTypeEnum eventType) {
        for (MetaData data : changed) {
            final String metaDataPath = ZkPathConstants.buildMetaDataPath(URLEncoder.encode(data.getPath(), "UTF-8"));
            // delete
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(metaDataPath);
                continue;
            }
            // create or update
            upsertZkNode(metaDataPath, data);
        }
    }

    @Override
    public void onPluginChanged(final List<PluginData> changed, final DataEventTypeEnum eventType) {
        for (PluginData data : changed) {
            final String pluginPath = ZkPathConstants.buildPluginPath(data.getName());
            // delete
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPathRecursive(pluginPath);
                final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(data.getName());
                deleteZkPathRecursive(selectorParentPath);
                final String ruleParentPath = ZkPathConstants.buildRuleParentPath(data.getName());
                deleteZkPathRecursive(ruleParentPath);
                continue;
            }
            //create or update
            upsertZkNode(pluginPath, data);
        }
    }

    @Override
    public void onSelectorChanged(final List<SelectorData> changed, final DataEventTypeEnum eventType) {
        if (eventType == DataEventTypeEnum.REFRESH && !changed.isEmpty()) {
            final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(changed.get(0).getPluginName());
            deleteZkPathRecursive(selectorParentPath);
        }
        for (SelectorData data : changed) {
            final String selectorRealPath = ZkPathConstants.buildSelectorRealPath(data.getPluginName(), data.getId());
            if (eventType == DataEventTypeEnum.DELETE) {
                deleteZkPath(selectorRealPath);
                continue;
            }
            final String selectorParentPath = ZkPathConstants.buildSelectorParentPath(data.getPluginName());
            createZkNode(selectorParentPath);
            //create or update
            upsertZkNode(selectorRealPath, data);
        }
    }

    @Override
    public void onRuleChanged(final List<RuleData> changed, final DataEventTypeEnum eventType) {
        if (eventType == DataEventTypeEnum.REFRESH && !changed.isEmpty()) {
            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
            upsertZkNode(ruleRealPath, data);
        }
    }
    
    private void createZkNode(final String path) {
        if (!zkClient.exists(path)) {
            zkClient.createPersistent(path, true);
        }
    }
    
    /**
     * create or update zookeeper node.
     * @param path node path
     * @param data node data 
     */
    private void upsertZkNode(final String path, final Object data) {
        if (!zkClient.exists(path)) {
            zkClient.createPersistent(path, true);
        }
        zkClient.writeData(path, data);
    }
    
    private void deleteZkPath(final String path) {
        if (zkClient.exists(path)) {
            zkClient.delete(path);
        }
    }
    
    private void deleteZkPathRecursive(final String path) { 
        if (zkClient.exists(path)) {
            zkClient.deleteRecursive(path);
        }
    }
}

逻辑基本类似,都是先把节点路径拼接好,然后判断节点路径是否存在,如果不存在就先创建节点,然后在写数据。

到这里,就把数据库中已存在的节点及数据都写到初始化到 zookeeper 里了,如下图所示:

zookeeper同步队列_源码阅读

soul-bootstrap 端分析,见下篇。