Soul 提供了多种数据同步的策略,默认且推荐的方式是使用 websocket。为了追求极致的响应速度,所有的配置没有选择存到redis,MySQL或是其他存储方式中,而是直接在HashMap中缓存,可以达到极低的延迟。
下图是三种策略下,Soul 网关与 soul-admin 数据同步的流程:
借助websocket的特性,DataChangedEventDispatcher 得以主动向 Soul 网关推送数据变更。当 soul-admin 与 soul-bootstrap 首次建立连接时,会向soul-bootstrap 推送一次全量的配置数据,增量的数据也可以由 soul-admin 主动推送给 soul-bootstrap。
soul-admin 会根据配置加载以下的 bean:
/**
* The WebsocketListener(default strategy).
*/
@Configuration
@ConditionalOnProperty(name = "soul.sync.websocket.enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(WebsocketSyncProperties.class)
static class WebsocketListener {
/**
* 监听数据变化的listener.
*
* @return the data changed listener
*/
@Bean
@ConditionalOnMissingBean(WebsocketDataChangedListener.class)
public DataChangedListener websocketDataChangedListener() {
return new WebsocketDataChangedListener();
}
/**
* websocket服务端,负责websocket 数据的 OnOpen,OnMessage,OnClose,OnError等生命周期的处理和数据推送。
*
* @return the websocket collector
*/
@Bean
@ConditionalOnMissingBean(WebsocketCollector.class)
public WebsocketCollector websocketCollector() {
return new WebsocketCollector();
}
/**
* Server endpoint exporter server endpoint exporter.
*
* @return the server endpoint exporter
*/
@Bean
@ConditionalOnMissingBean(ServerEndpointExporter.class)
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
其中,WebsocketDataChangedListener 封装了对一系列数据的监听,以插件变更监听为例:
@Override
public void onPluginChanged(final List<PluginData> pluginDataList, final DataEventTypeEnum eventType) {
// 先封装 websocket 数据,再使用 WebsocketCollector 来发送json序列化后的数据到 soul-bootstrap.
WebsocketData<PluginData> websocketData =
new WebsocketData<>(ConfigGroupEnum.PLUGIN.name(), eventType.name(), pluginDataList);
WebsocketCollector.send(GsonUtils.getInstance().toJson(websocketData), eventType);
}
/**
* The type Websocket data changed listener.
*
* @author xiaoyu(Myth)
* @author huangxiaofeng
* @since 2.0.0
*/
@Slf4j
@ServerEndpoint("/websocket")
public class WebsocketCollector {
private static final Set<Session> SESSION_SET = new CopyOnWriteArraySet<>();
private static final String SESSION_KEY = "sessionKey";
/** 略 **/
/**
* On message.
*
* @param message the message
* @param session the session
*/
@OnMessage
public void onMessage(final String message, final Session session) {
// 只有 message 的类型是 MYSELF时才处理
if (message.equals(DataEventTypeEnum.MYSELF.name())) {
try {
//将 websocket session 保存到 ThreadLocal 中。
ThreadLocalUtil.put(SESSION_KEY, session);
SpringBeanUtils.getInstance().getBean(SyncDataService.class).syncAll(DataEventTypeEnum.MYSELF);
} finally {
ThreadLocalUtil.clear();
}
}
}
/**
* Send.
*
* @param message the message
* @param type the type
*/
public static void send(final String message, final DataEventTypeEnum type) {
if (StringUtils.isNotBlank(message)) {
if (DataEventTypeEnum.MYSELF == type) {
try {
Session session = (Session) ThreadLocalUtil.get(SESSION_KEY);
if (session != null) {
session.getBasicRemote().sendText(message);
}
} catch (IOException e) {
log.error("websocket send result is exception: ", e);
}
return;
}
//向所有存在的session 发送消息
for (Session session : SESSION_SET) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("websocket send result is exception: ", e);
}
}
}
}
}
我们可以看到当 DataEventTypeEnum 是 MYSELF 时,也就是客户端发来 MYSELF 消息时,服务端会把所有的配置数据同步给客户端,具体的代码实现在 WebsocketSyncDataConfiguration 这个类加载、创建 WebsocketSyncDataService bean 时,在 WebsocketSyncDataService 类中:
@Slf4j
public class WebsocketSyncDataService implements SyncDataService, AutoCloseable {
private final List<WebSocketClient> clients = new ArrayList<>();
private final ScheduledThreadPoolExecutor executor;
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 {
//遍历所有url,将 SoulWebsocketClient 实例加到 clients中
clients.add(new SoulWebsocketClient(new URI(url), Objects.requireNonNull(pluginDataSubscriber), metaDataSubscribers, authDataSubscribers));
} catch (URISyntaxException e) {
log.error("websocket url({}) is error", url, e);
}
}
/** 略 **/
而在实例化 SoulWebsocketClient时,会调用如下的函数:
@Override
public void onOpen(final ServerHandshake serverHandshake) {
if (!alreadySync) {
send(DataEventTypeEnum.MYSELF.name());
alreadySync = true;
}
}
client会在 websocket open 的时候 发送 MYSELF 消息到服务端,验证了上面我们的想法。其他类型的 DataEventTypeEnum 发送则是通过 onMessage 处理。
总结
soul 巧妙得运用了 websocket 进行数据同步,将数据直接放到JVM的方法也体现了对性能的极致追求。