Soul网关源码学习【第六篇】-数据同步(websocket)

本文深入探讨Soul网关的数据同步机制,重点讲解WebSocket同步方式。内容包括数据同步的需求、原理、支持的方式,以及WebSocket同步的启动、连接管理和数据处理过程,揭示了数据如何在内存中实时更新并保证高性能。
摘要由CSDN通过智能技术生成

Soul网关数据同步

  • 为什么需要数据同步?
    Soul作为独立的网关系统,有自己的管理后台soul-admin,通过页面操作就可以控制插件数据,选择器,规则数据,元数据,签名数据等等,并且所有插件的选择器,规则都是动态配置,立即生效,不需要重启服务。这样便捷的操作当然需要一套完善的数据同步机制。又考虑到性能问题,不可能每次更改了设置都要通过数据库来获取,所以soul设计了一套数据同步机制,保证了数据始终在JVM的内存中,且是最新的数据,这样也就大大提高了性能。

  • Soul的数据同步原理是什么?
    详细的介绍可以参考官方社区的文档【Soul数据同步原理
    我这里概括以下就是用户通过后台页面修改了设置,Soul就会分别将数据同步到数据库,和同步给我们的网关系统。然后我们的网关系统就会更新本地的缓存,当又相应的流量过来的时候,不需要去我们的数据库再去拿数据。对应的就是下面这张图:
    在这里插入图片描述

  • Soul支持哪些数据同步方式?
    到目前为止,Soul支持以下四种数据同步方式:

    • websocket同步
    • zookeeper同步
    • http长轮询同步
    • nacos同步

    今天 我们主要看的是websocket同步方式。

websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

以上摘抄自百度百科

代码跟踪

基本的概念了解清楚后,接下来就打开源码来学习吧。
因为Soul默认使用的就是websocket方式进行数据同步,所以这里就不再赘述了,不清楚的同学可以参考官方文档【Soul数据同步策略】。

  • 启动soul-admin

  • 启动网关soul-bootstrap
    启动成功后我们分别再soul-admin的后台和soul-bootstrap的后台分别看到如下log信息。这就说明我们的后台管理系统和网关系统使用websocket建立了连接。

    2021-01-20 19:57:59.428  INFO 19448 --- [0.0-9095-exec-1] o.d.s.a.l.websocket.WebsocketCollector   : websocket on open successful....
    
    2021-01-20 19:57:59.411  INFO 19576 --- [ocket-connect-1] o.d.s.p.s.d.w.WebsocketSyncDataService   : websocket reconnect is successful.....
    
  • 先从这两个log入手,看一下源码,先看soul-admin的。这里使用的是websocket的OnOpen监听方法,当有新的连接的时候,会把该连接的session放入到一个Set中。

    	private static final Set<Session> SESSION_SET = new CopyOnWriteArraySet<>();
    	/**
         * On open.
         *
         * @param session the session
         */
        @OnOpen
        public void onOpen(final Session session) {
            log.info("websocket on open successful....");
            SESSION_SET.add(session);
        }
    

    这里主要处理的是建立连接的时候处理的一些逻辑。

    1. 如果配置了多个url,通过“,”分割(这个再配置的时候有说明)。
    2. 创建WebSocketClient,并添加到List中去
    3. 针对于List中的Client创建连接,并且创建定时任务监视是否处于连接中,如果断开了连接,则尝试重新连接(如果在executor.scheduleAtFixedRate(() -> {})里面打断点,会发现每隔30面进来一次)。
    /**
         * Instantiates a new Websocket sync cache.
         *
         * @param websocketConfig      the websocket config
         * @param pluginDataSubscriber the plugin data subscriber
         * @param metaDataSubscribers  the meta data subscribers
         * @param authDataSubscribers  the auth data subscribers
         */
        public WebsocketSyncDataService(final WebsocketConfig websocketConfig,
                                        final PluginDataSubscriber pluginDataSubscriber,
                                        final List<MetaDataSubscriber> metaDataSubscribers,
                                        final List<AuthDataSubscriber> authDataSubscribers) {
            String[] urls = StringUtils.split(websocketConfig.getUrls(), ",");
            executor = new ScheduledThreadPoolExecutor(urls.length, SoulThreadFactory.create("websocket-connect", true));
            for (String url : urls) {
                try {
                    clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers));
                } catch (URISyntaxException e) {
                    log.error("websocket url({}) is error", url, e);
                }
            }
            try {
                for (WebSocketClient client : clients) {
                    boolean success = client.connectBlocking(3000, TimeUnit.MILLISECONDS);
                    if (success) {
                        log.info("websocket connection is successful.....");
                    } else {
                        log.error("websocket connection is error.....");
                    }
                    executor.scheduleAtFixedRate(() -> {
                        try {
                            if (client.isClosed()) {
                                boolean reconnectSuccess = client.reconnectBlocking();
                                if (reconnectSuccess) {
                                    log.info("websocket reconnect is successful.....");
                                } else {
                                    log.error("websocket reconnection is error.....");
                                }
                            }
                        } catch (InterruptedException e) {
                            log.error("websocket connect is error :{}", e.getMessage());
                        }
                    }, 10, 30, TimeUnit.SECONDS);
                }
                /* client.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxyaddress", 80)));*/
            } catch (InterruptedException e) {
                log.info("websocket connection...exception....", e);
            }
    
        }
    
  • 通过这两个log只是看到了创建WebSocket连接的过程,没有发现数据同步的地方。别着急,我们会发现WebsocketSyncDataService的所在包里有个handler的包,里面有WebsocketDataHandler,按照猫大人的假设想猜想,应该是在这里进行数据同步的。在里面的executor方法出打个断点,然后去后台点一下数据同步按钮,果不其然就进来了。
    在这里插入图片描述

  • 然后就可以跟踪调试了,这个类比较简单,我们每次进来会执行executor方法,这个方法里面胡根据我们的type进行不同的处理,根据代码就可以看出有五种不同的处理,分别是PluginDataHandlerSelectorDataHandlerRuleDataHandlerAuthDataHandlerMetaDataHandler

    private static final EnumMap<ConfigGroupEnum, DataHandler> ENUM_MAP = new EnumMap<>(ConfigGroupEnum.class);
    
    /**
     * Instantiates a new Websocket data handler.
     *
     * @param pluginDataSubscriber the plugin data subscriber
     * @param metaDataSubscribers  the meta data subscribers
     * @param authDataSubscribers  the auth data subscribers
     */
    public WebsocketDataHandler(final PluginDataSubscriber pluginDataSubscriber,
                                final List<MetaDataSubscriber> metaDataSubscribers,
                                final List<AuthDataSubscriber> authDataSubscribers) {
        ENUM_MAP.put(ConfigGroupEnum.PLUGIN, new PluginDataHandler(pluginDataSubscriber));
        ENUM_MAP.put(ConfigGroupEnum.SELECTOR, new SelectorDataHandler(pluginDataSubscriber));
        ENUM_MAP.put(ConfigGroupEnum.RULE, new RuleDataHandler(pluginDataSubscriber));
        ENUM_MAP.put(ConfigGroupEnum.APP_AUTH, new AuthDataHandler(authDataSubscribers));
        ENUM_MAP.put(ConfigGroupEnum.META_DATA, new MetaDataHandler(metaDataSubscribers));
    }
    
    /**
     * Executor.
     *
     * @param type      the type
     * @param json      the json
     * @param eventType the event type
     */
    public void executor(final ConfigGroupEnum type, final String json, final String eventType) {
        ENUM_MAP.get(type).handle(json, eventType);
    }
    

    然后根据eventType判断调用哪个方法。这里可以看到目前有三种方法doRefreshdoUpdatedoDelete

    @Override
        public void handle(final String json, final String eventType) {
            List<T> dataList = convert(json);
            if (CollectionUtils.isNotEmpty(dataList)) {
                DataEventTypeEnum eventTypeEnum = DataEventTypeEnum.acquireByName(eventType);
                switch (eventTypeEnum) {
                    case REFRESH:
                    case MYSELF:
                        doRefresh(dataList);
                        break;
                    case UPDATE:
                    case CREATE:
                        doUpdate(dataList);
                        break;
                    case DELETE:
                        doDelete(dataList);
                        break;
                    default:
                        break;
                }
            }
        }
    
    
  • 因为我们是在divide插件页面
    执行的操作,可以看到这里的type分别是PLUGINSELECTORRULE,也就是分别调用了PluginDataHandler.doUpdate()SelectorDataHandler.doRefresh()RuleDataHandler.doRefresh()

  • 接下来再看一下这三个方法是怎么处理的,因为每个DataHandler的处理逻辑都一样,所以这里就拿SelectorDataHandler这一个为例子看一下。

    	private final PluginDataSubscriber pluginDataSubscriber;
    
    	@Override
    	public List<SelectorData> convert(final String json) {
        	return GsonUtils.getInstance().fromList(json, SelectorData.class);
    	}
    	@Override
        protected void doRefresh(final List<SelectorData> dataList) {
            pluginDataSubscriber.refreshSelectorDataSelf(dataList);
            dataList.forEach(pluginDataSubscriber::onSelectorSubscribe);
        }
    
        @Override
        protected void doUpdate(final List<SelectorData> dataList) {
            dataList.forEach(pluginDataSubscriber::onSelectorSubscribe);
        }
    
        @Override
        protected void doDelete(final List<SelectorData> dataList) {
            dataList.forEach(pluginDataSubscriber::unSelectorSubscribe);
        }
    
  • 接着跟着调试走,就会来到了我们通用的一个数据同步处理的方法中。可以看到Soul通过UPDATEDELETE两种操作类型完成了数据的更新。并且可以看出我们的数据都是放在了BaseDataCache这样一个容器中,该容器针对于不同的插件类型和不同的操作类型,提供了如下几个方法用来做数据操作。

    • cachePluginData //更新指定插件的数据缓存
    • removePluginData //删除指定插件的数据缓存
    • cacheSelectData//更新指定选择器的数据缓存
    • removeSelectData // 删除指定选择器的数据缓存
    • cacheRuleData// 更新指定规则的数据缓存
    • removeRuleData // 删除指定规则的数据缓存
    @Override
    public void onSelectorSubscribe(final SelectorData selectorData) {
        subscribeDataHandler(selectorData, DataEventTypeEnum.UPDATE);
    }
    
    @Override
    public void unSelectorSubscribe(final SelectorData selectorData) {
        subscribeDataHandler(selectorData, DataEventTypeEnum.DELETE);
    }
    
    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);
                    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));
                }
            }
        });
    }
    
  • 知道了数据存在哪,接下来就看一下我们的容器是怎么存数据的。

  • 我们的容器定义了四个属性,INSTANCE :作为全局的一个整体数据容器使用,PLUGIN_MAPSELECTOR_MAPRULE_MAP :使用ConcurrentMap分别存储插件数据,选择器数据和规则数据。那么它的update和delete就很简单了,对于的就是Map的putremove方法。

    	private static final BaseDataCache INSTANCE = new BaseDataCache();
        
        /**
         * pluginName -> PluginData.
         */
        private static final ConcurrentMap<String, PluginData> PLUGIN_MAP = Maps.newConcurrentMap();
        
        /**
         * pluginName -> SelectorData.
         */
        private static final ConcurrentMap<String, List<SelectorData>> SELECTOR_MAP = Maps.newConcurrentMap();
        
        /**
         * selectorId -> RuleData.
         */
        private static final ConcurrentMap<String, List<RuleData>> RULE_MAP = Maps.newConcurrentMap();
    

小结

OK,到此为止我们知道了我们网关中数据是保存在哪的了,对于插件,选择器以及Rule的数据,放在一个BaseDataCache的容器中,并且分别用Map存储。
对于元数据单独放在一个MetaDataCache的容器中,也是使用Map存储。
当数据发生变化,或者我们在后台管理画面点了同步数据的按钮时,就会触发我们数据同步的方法。这个方法的流程大致如下:

  1. 所有操作调用WebsocketDataHandlerexecutor方法,这里需要把操作类型和数据类型以及数据详细信息传递过来,这里的数据信息是以json传递过来的,使用com.google.gson进行数据解析。
  2. 然后就是根据不同的数据类型和操作类型调用实际的操作方式,即往map里put数据或者remove数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值