soul网关-3-数据同步机制之websocket

本文详细探讨了Soul网关的数据同步机制,尤其是WebSocket同步方式。通过分析源码,展示了从管理员在Soul-admin更新数据到网关接收并处理数据的整个流程,包括WebSocket连接、断线重连、数据处理以及事件分发机制。相较于旧系统中的crontab定时任务,WebSocket提供了更实时、高效的同步方案。

大概一年前,我接到了一个网管系统的API管理后台的维护工作,当时那个系统的架构是这样子的:

老系统的数据同步方式

  • 1.管理员在管理后台上更新数据
  • 2.数据都存储到DB里面
  • 3.crontab定时任务从DB里面读取表数据,组成某种格式,写入配置文件,存到磁盘
  • 4.网关系统发现磁盘上的配置文件的更新时间有变化,则重新载入配置文件到内存

这个老系统使用crontab的方式定时从数据库里读取数据,肯定是不够实时的。而且还和管理后台读了同一个数据库,有一定的耦合。

今天继续来进行soul网关源码的学习,看下soul网关的数据同步机制。

首先,先看官方文档里面关于数据同步的相关介绍,找到了数据同步原理数据同步策略的介绍。

快速浏览了一下官方文档,发现soul网关2.x版本支持通过websocket、zookeeper、http长轮询、nacos四种同步方式,其中websocket是默认的方式。

WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。各大浏览器都已经支持WebSocket,最贴近我们生活的就是聊天应用。聊天应用不用http的原因是http只能由客户端发起请求,要获取最新数据的话,只能起一个定时任务来轮询,但是轮询的效率低,非常浪费资源,聊天应用使用websocket就可以实时收到别人发过来的消息了。

本文主要学习一下websocket同步方式。看到官方文档里面对websocket的介绍:

websocket 和 zookeeper 机制有点类似,将网关与 admin 建立好 websocket 连接时,admin 会推送一次全量数据,后续如果配置数据发生变更,则将增量数据通过 websocket 主动推送给 soul-web
使用websocket同步的时候,特别要注意断线重连,也叫保持心跳。soul使用java-websocket 这个第三方库来进行websocket连接。

soul网关的源码里面这么多代码,一个个看过去是不行的,太浪费时间,而且很容易就放弃了。我们要找一个切入点。

soul使用java-websocket 这个第三方库来进行websocket连接这个官方文档里面的信息,就是我本次的切入点。

首先在IDEA里面全局搜索一下java-websocket这个关键字,发现在主pom和soul-sync-data-center这个子模块的pom里面出现了。在第一篇笔记里面已经对soul源码的模块的大致作用分析了一下,soul-sync-data-center这个模块主要是负责数据同步的。

java-websocket全局搜索
soul-sync-data-center里面找到websocket相应的位置,看到里面文件不多,可以挨个点开快速看一下

soul-sync-data-websocket目录结构

其中最主要的文件是WebsocketSyncDataService,这个里面进行了websocket连接和断线重连(30s)。与官方文档上的示例代码有一点出入,应该是官方文档更新不及时。不过变动不大,并不影响我们继续看下去。

在这里插入图片描述

可以看到使用了ScheduledThreadPoolExecutor这个线程池,每个websocket连接是把java-websocket这个第三方库提供的WebSocketClient封装了一下的SoulWebsocketClient

我没用过ScheduledThreadPoolExecutor,对它不是很了解,快速上网搜了一下,了解到它可以实现定时任务。

Java以前有Timer可以实现定时任务,ScheduledThreadPoolExecutor比Timer的好处就在于:Timer可以做的事情,通过ScheduledThreadPoolExecutor都可以做到,并且修复了其中Timer的部分设计上的缺陷。并且实现了多线程的执行,相对来说功能是比Timer强大了一个等级。

我们再看一下上文提到的SoulWebsocketClient,里面Override了websocket的一些基本方法,如onOpenonMessageonCloseonError。我们看一下onMessage,看下它在收到数据的时候是怎么处理数据的。我们知道websocket协议可以传文本数据,也可以传二进制数据。在SoulWebsocketClient这里,它传的是文本数据,是String类型的。onMessage里面调用了一个handleResult方法,如下所示

    @SuppressWarnings("ALL")
    private void handleResult(final String result) {
        WebsocketData websocketData = GsonUtils.getInstance().fromJson(result, WebsocketData.class);
        ConfigGroupEnum groupEnum = ConfigGroupEnum.acquireByName(websocketData.getGroupType());
        String eventType = websocketData.getEventType();
        String json = GsonUtils.getInstance().toJson(websocketData.getData());
        websocketDataHandler.executor(groupEnum, json, eventType);
    }

这个handleResult方法做了以下工作:

  • 首先将收到的string使用Gson转成自定义的对象类型WebsocketData
    • 看一下WebsocketData.class里面的东西,它只有三个属性,分别是groupTypeeventTypedata
  • 从WebsocketData里面解析出groupTypeeventTypedata,传递给websocketDataHandler的executor方法

先了解一下groupType是什么,我们可以去ConfigGroupEnum里面看下,它是一个枚举类。我们就知道了groupType是数据的分类,例如插件、选择器、规则、元数据等。那么eventType就可以猜测是数据的变动,例如删除、更新等。data里面就是具体的数据。

看一下websocketDataHandler的executor方法,它的传入参数是groupTypeeventTypedata,它是怎么具体处理的。

websocketdatahandler

可以看到WebsocketDataHandler在初始化的时候,将groupType与对应的DataHandler放到了一个map里面,websocketDataHandler的executor就是从这个map里面取出groupType对应的DataHandler来执行它的handle方法。

我们看下各种groupType类型的的DataHandler的继承关系,如下图所示

各种类型的DataHandler的继承关系

大家都继承自AbstractDataHandler,它是一个抽象类,它实现了DataHandler接口的handle方法。看下它的handle方法里面做了什么,代码如下所示

	@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;
            }
        }
    }

就是根据不同的eventType,来调用相应的处理方法。例如eventType是REFRESH刷新,那么就调用doRefresh方法。

随便选一个DataHandler,例如PluginDataHandler,看下它的doRefresh方法,里面调用了PluginDataSubscriber的方法

    @Override
    protected void doRefresh(final List<PluginData> dataList) {
        pluginDataSubscriber.refreshPluginDataSelf(dataList);
        dataList.forEach(pluginDataSubscriber::onSubscribe);
    }

PluginDataSubscriber是一个接口,全局搜索一下,发现只有soul-plugin-base模块里面的CommonPluginDataSubscriber实现了该接口。上文提到PluginDataHandleronRefresh方法里面,调用了pluginDataSubscriber的refreshPluginDataSelfonSubscribe方法。我们来看一下CommonPluginDataSubscriber里面关于这两个方法的实现。

    @Override
    public void refreshPluginDataSelf(final List<PluginData> pluginDataList) {
        if (CollectionUtils.isEmpty(pluginDataList)) {
            return;
        }
        BaseDataCache.getInstance().cleanPluginDataSelf(pluginDataList);
    }

    @Override
    public void onSubscribe(final PluginData pluginData) {
        subscribeDataHandler(pluginData, DataEventTypeEnum.UPDATE);
    }

    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));
                }
            }
        });
    }

从上方的代码可以看到,操作了BaseDataCache这个内存里的对象,根据数据类型和操作类型,对内存里的数据进行了相应操作。例如数据类型是SelectorData,操作类型是UPDATE,就更新了BaseDataCache里面缓存的selector数据。在上一篇学习divide插件的笔记里提过,在进行http请求代理的时候,dividePlugin里的数据PluginData(选择器、规则等)就是从BaseDataCache这个内存对象里取出来的。那么当下次dividePlugin插件进行http请求代理的时候,从缓存里拿到的就是更新后的数据了。


上半部分已经大致了解了soul网关是怎么收到数据更新的,下面来了解一下当管理员在soul-admin更新了数据的时候,数据更新这个事件是怎么被发出来的。即websocket两端通信的一端(gateway)”收消息“我们已经大致了解了,下面看下另一端(admin)”发消息“是怎样的。

官方文档里面提到,gateway和admin必须配置一样的数据同步方式,一方是websocket的话另一方也必须是websocket。即在soul-admin模块的application.yml配置文件和soul-bootstrap模块的application-local.yml配置文件里面,必须将soul.sync配置成同一种同步方式。

看一下soul-admin的pom文件,发现它依赖了spring-boot-starter-websocket来实现websocket相关功能。Spring boot集成websocket有多种方式,一般都涉及一些注解。我们在soul-admin模块的WebsocketCollector类上面找到了@ServerEndpoint("/websocket")注解,这个注解是一种集成websocket的方式。

WebsocketCollector这个类可以进行websocket通信,也有onOpenonMessageonCloseonError方法,它还有个send方法。我们看看是哪些地方调用了这个send方法,在IDEA里面Find Usages一下。发现在WebsocketDataChangedListener里面有调用。WebsocketDataChangedListener实现了DataChangedListener接口。

DataChangedListener接口

看下哪些类实现了DataChangedListener接口。

DataChangedListener实现关系图

正好契合官方文档说的websocket、zookeeper、http长轮询、nacos四种同步方式。,

WebsocketDataChangedListener类的命名上来看,这应该是属于设计模式中的观察者模式。xxxListener相当于是一个监听器。就好像是手机界面上的一个button,我们希望在它的点击事件的回调里面做一些事情,例如修改背景颜色等,那么我们需要注册一个listener,在listener的onButtonClick方法的回调里面修改背景颜色等,而将这个触发点击事件回调的是手机操作系统。同理,一定有地方在触发数据的更新,有地方监听到数据更新之后调用回调方法。

全局搜索一下WebsocketDataChangedListener,发现在DataSyncConfiguration里面有使用。DataSyncConfiguration是soul-admin模块的一个有@Configuration注解的类,在soul-admin的启动的时候,被@Configuration注解的类里面的bean就会被注入到spring容器中去。那么可以理解为spring容器在监听数据更新。

再全局搜索一下,发现在DataChangedEventDispatcher里面有调用listener的onAppAuthChanged、onPluginChanged、onRuleChanged、onSelectorChanged、onMetaDataChanged方法。那么就是DataChangedEventDispatcher在监听到数据更新之后调用回调方法。

DataChangedEventDispatcher

DataChangedEventDispatcher实现了Spring框架里面的ApplicationListener接口,它监听的是自定义的DataChangedEvent事件。在Spring框架里面与ApplicationListener对应的是ApplicationEventPublisher, ApplicationEventPublisher可以进行事件的发布。

全局搜一下,看下哪里在使用ApplicationEventPublisherpublishEvent进行DataChangedEvent事件的发布。找到了soul-admin模块的src/main/java/org/dromara/soul/admin/service里面的一些service(例如PluginServiceImpl、SelectorServiceImpl)里面有调用publishEvent。再往上一层就可以与管理员在admin管理后台的更新、删除等操作对应上了。

后来再看官方文档的时候,发现数据同步原理里面有一张图。里面写出了几个关键的类名,例如数据变动事件分发器DataChangeEventDispatcher、应用事件发布器ApplicationEventPublisher。这两个其实是更明确的切入点,看源码会更容易。我这次选择的切入点没有这两个好,导致我探索的链路是从下往上的,不太符合事件发生的逻辑顺序。

数据同步原理图


对比本文一开始提到的我曾经维护过API管理后台的那个网关系统的数据同步方式,总结一下soul网关里使用websocket数据同步方式的好处:

  • 利用websocket双向通信的特点,近乎实时地可以收到数据更新。(比crontab高效很多,crontab最小只能分钟级别)
  • 数据直接缓存在内存中,修改和读取内存中的数据。(避免读写磁盘,可以加快速度)
  • 利用面向对象的多态特性,当某种类型数据有变动的时候,只更新该种类型的数据。(更新部分数据而不是更新全量数据,可以加快速度)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值