Zebra源码分析-GroupDataSource

1 简介

GroupDataSource是读写分离数据源,由多个SingleDataSource组成,其中有一个主库,用于写操作和一部分指定得读操作;多个从库之间使用负载均衡策略,根据不同的权重来路由请求,同时系统还是地理信息感知的,通过配置地理信息来更高效和灵活的路由请求

2. 配置文件

2.1 数据源配置

在使用读写分离的数据源的时候,需要在resource目录下创建{jdbcRef}.properties来对数据源进行配置,包括数据库基本信息和读写比例等

zebra.group.zebra=<groupConfig>\<singleConfig>\<name>zebra-n1</name>\<writeWeight>0</writeWeight>\<readWeight>-1</readWeight>\</singleConfig>\<singleConfig>\<name>zebra-n2</name>\<writeWeight>-1</writeWeight>\<readWeight>1</readWeight>\</singleConfig>\</groupConfig>zebra.ds.zebra-n1=<dsConfig>\<url>jdbc:mysql://127.0.0.1:3306/zebra?characterEncoding=UTF8&amp;socketTimeout=60000</url>\<driverClass>com.mysql.jdbc.Driver</driverClass>\<active>true</active>\<username>root</username>\<properties>idleConnectionTestPeriod=80&amp;acquireRetryAttempts=50&amp;acquireRetryDelay=300&amp;maxStatements=1</properties>\<password>123456</password>\</dsConfig>zebra.ds.zebra-n2=<dsConfig>\<url>jdbc:mysql://127.0.0.1:3306/zebra?characterEncoding=UTF8&amp;socketTimeout=60000</url>\<driverClass>com.mysql.jdbc.Driver</driverClass>\<active>true</active>\<username>root</username>\<properties>idleConnectionTestPeriod=80&amp;acquireRetryAttempts=50&amp;acquireRetryDelay=300&amp;maxStatements=1</properties>\<password>123456</password>\</dsConfig>

2.2 区域配置

3. 源码分析

3.1 配置处理

首先对配置服务和配置信息对GroupSource初始化,需要完成的步骤有:

  • 创建配置服务,并初始化
  • 创建配置服务的管理器,师姐的管理操作由配置服务完成
  • 创建系统配置管理器
  • 创建并汇总GroupSource需要的配置信息
protected void initConfig() {this.configService = ConfigServiceFactory.getConfigService(configManagerType, serviceConfigs);this.dataSourceConfigManager = DataSourceConfigManagerFactory.getConfigManager(jdbcRef, configService);this.dataSourceConfigManager.addListerner(new GroupDataSourceConfigChangedListener());this.systemConfigManager = SystemConfigManagerFactory.getConfigManger(configManagerType, configService);this.groupConfig = buildGroupConfig();if (this.groupConfig != null && this.useCustomRouterConfig) {this.groupConfig.setRouterStrategy(this.routerStrategy);} else if (!this.useCustomRouterConfig) {this.routerStrategy = this.groupConfig.getRouterStrategy();}}

3.1.1 创建配置服务

初始化配置服务-ConfigService

在初始化读写分离数据源时,需要对读写库的配置信息进行读取和构造,并使用配置对每一个数据源进行初始化

首先,需要对配置管理器进行加载,在Zebra中支持两种配置类型:

  • 一种是local,根据本地的文件读取数据源的信息
  • 另一种是zookeeper,zk的信息配置在reousrces/zookeeper.properties

通过ConfigServiceFactory工厂和传入的配饰类型来构造ConfigService对象,并对配置服务初始化,代码如下

public class ConfigServiceFactory {public static ConfigService getConfigService(String name, Map<String, Object> configs) {//通过ExtensionLoader加载对应的ConfigServiceConfigService configService = ExtensionLoader.getExtensionLoader(ConfigService.class).load(name);...configService.init(configs);// 初始化return configService;}
}

getExtensionLoader()中,两次对内部的loaderMap进行查询,如果没有查找到已经创建的ExtensionLoader则创建一个对应的对象,并放再map结构中,方便下一次获取,通过如下构造函数,完成对象的创建,参数包括ConfigService和类加载器

private ExtensionLoader(Class<T> service) {this(service, Thread.currentThread().getContextClassLoader());}private ExtensionLoader(Class<T> service, ClassLoader loader) {this.service = service;this.loader = loader;}

在加载对应的参数服务的时候,需要先对刚创建的ExtensionLoader进行初始化,主要的工作如查下:

  • 使用类加载器从给定的路径产生URL对象
  • 通过读取URL指示的文件内容(配置服务的类名),保存到list
  • 通过类名,加载list中所有的类
private Map<String, Class<T>> loadExtensions() {String configFiles = PREFIX + this.service.getName();List<String> classNames = null;try {//URL代表路径"META-INF/services/com.dianping.zebra.config.ConfigService"Enumeration<URL> url = null;if (this.loader != null) {url = this.loader.getResources(configFiles);} else {url = ClassLoader.getSystemResources(configFiles);}while (url.hasMoreElements()) {URL u = url.nextElement();// 对读取的文件进行解析,将类放入List中保存classNames = parseClassNames(u);}} catch (Exception e) {throw new ZebraException("loadExtensions fail, configFileName:" + configFiles, e);}//对List中所有的类进行加载return this.loadAllClasses(classNames);}

在这一步存储在list中的类名使用类加载器进行加载,在加载之前判断类加载器是否已经初始化了,并对类进行检查:

  • 是否是public类型
  • 是否实现了service接口
  • 有无默认的构造函数
private ConcurrentHashMap<String, Class<T>> loadAllClasses(List<String> classNames) {ConcurrentHashMap<String, Class<T>> classes = new ConcurrentHashMap<>();if (classNames != null && !classNames.isEmpty()) {for (String className : classNames) {try {Class<T> clazz = null;if (this.loader != null) {clazz = (Class<T>) Class.forName(className, false, this.loader);} else {clazz = (Class<T>) Class.forName(className);}checkClass(clazz);String spiName = getSpiName(clazz);classes.putIfAbsent(spiName, clazz);} catch (Exception e) {throw new ZebraException("load class fail :" + className);}}}return classes;}

根据传入的配置服务名和Spi中指定的实例化方式(单例或其他)来实例化对应的类,并返回该对象

public T load(String serviceName) {if (!init) {this.init();}Class<T> clazz = this.extensions.get(serviceName);if (clazz != null) {try {Spi spi = clazz.getAnnotation(Spi.class);if (spi.scope() == Scope.SINGLETON) {return newSingletonInstance(clazz, serviceName);} else {return clazz.newInstance();}} catch (Exception e) {throw new ZebraException("newInstance fail className : " + clazz.getName());}}return null;}

需要注意的是,单例模式通过维护一个singletonMap来判断对应的类是否有被实例化过:若有,直接从哈希表中取出,否则实例化后加入到哈希表中

private T newSingletonInstance(Class<T> clazz, String serviceName)throws IllegalAccessException, InstantiationException {synchronized (singletonMap) {T singleton = singletonMap.get(serviceName);if (singleton == null) {singleton = clazz.newInstance();singletonMap.put(serviceName, singleton);}return singleton;}}
本地配置- PropertyConfigService

本地配置配置的方式通过读取{jdbcRef}.properties下的文件内容转换成数据源的信息,对数据源进行初始化。同时,还可以配置监听器来订阅配置信息的改动,初始化的过程如下:

  1. 获取配置文件名称并创建URL对象,修正路径并保存成File对象
  2. 通过构建InputStream读取配置信息,保存成Properties对象
  3. 创建后台监控线程,每间隔5秒去读取文件修改的时间戳和保存的时间戳,如果时间和内容均发生改变则通知订阅者
public void init(Map<String, Object> serviceConfigs) {try {if (serviceConfigs == null) {throw new ZebraException("serviceConfigs is null");}String resourceName = String.valueOf(serviceConfigs.get(Constants.CONFIG_SERVICE_NAME_KEY));this.resourceFileName = resourceName + ".properties";this.resourceFile = FileUtils.getFile(resourceFileName);this.props.set(FileUtils.loadProperties(resourceFile));this.lastModifiedTime.set(getLastModifiedTime());Thread updateTask = new Thread(new ConfigPeroidCheckerTask());updateTask.setDaemon(true);updateTask.setName("Thread-" + ConfigPeroidCheckerTask.class.getName());updateTask.start();} catch (Exception e) {logger.error("fail to initilize Local Config Manager for DAL", e);throw new ZebraConfigException(e);}}// 获取配置信息
public String getProperty(String key) {return props.get().getProperty(key);}

需要注意的是,propsAtomicReference类型的,提供了一个可以原子读写的对象引用变量,是线程安全的,在获取配置信息时,只需要把成员变量的props中对应的值返回即可

ZK配置-ZookeeperConfigService

和本地配置服务不同的是,ZK的初始化使用Curator客户端框架,使用静态工厂的方式创建客户端对象,在初始化的过程中不对配置信息进行读取。

public synchronized void init(Map<String, Object> serviceConfigs) {if (!this.init) {try {this.propertiesFileName = Constants.DEFAULT_ZK_FILENAME + ".properties";String zkAddr = loadZookeeperAdderss();RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMills, maxRetries);CuratorFramework newClient = CuratorFrameworkFactory.builder().connectString(zkAddr)    .sessionTimeoutMs(connectionTimeoutMs).connectionTimeoutMs(sessionTimeoutMs).retryPolicy(retryPolicy).build();newClient.start();newClient.getZookeeperClient().blockUntilConnectedOrTimedOut();this.client = newClient;this.init = true;LOGGER.info("DAL.Zookeeper connected");} catch (Exception e) {LOGGER.error("connect to zookeeper server failed", e);throw new ZebraException("connect to zookeeper server failed", e);}}}

在获取配置时,如果本地的nodeCatchMap不存在信息,则需要在ZK注册这个节点,并设置节点内容更新的监听器和回调函数,一旦发送改变则通知订阅者

小结

在这一步主要是根据用户指定的配置服务的类型创建对应的实例,并进行初始化。

3.1.2 配置服务管理器

初始化对象

initConfig中,需要根据jdbcRefconfigService对成员对象配置服务管理器进行实例化并初始化

this.dataSourceConfigManager = DataSourceConfigManagerFactory.getConfigManager(jdbcRef, configService);

这也是一个静态的工厂,通过调用静态的方法getConfigManager来创建对应的管理器,代码如下:

public static DataSourceConfigManager getConfigManager(String jdbcref, ConfigService configService) {DataSourceConfigManager dataSourceConfigManager = new DefaultDataSourceConfigManager(jdbcref, configService);// 对manager进行初始化dataSourceConfigManager.init();return dataSourceConfigManager;}

DefaultDataSourceConfigManager这个类中有一个内部的GroupDataSourceConfigBuilder类,再在通过调用getGroupDataSourceConfig时,会通过获取configService中的数据源的参数完成GroupDataSourceConfig的创建

public void visitGroupDataSourceConfig(GroupDataSourceConfig groupDsConfig) {String config = configService.getProperty(getGroupDataSourceKeyForApp());//判断是否为空// ...if (StringUtils.isNotBlank(config)) {//首先解析groupConfig,获取singleConfig的名称,读写比例等信息GroupConfig groupConfig = JaxbUtils.fromXml(config, GroupConfig.class);if (groupConfig != null && groupConfig.getSingleConfigs() != null) {for (SingleConfig singleConfig : groupConfig.getSingleConfigs()) {singleConfig.checkConfig();// 查找或创建DataSouceConfig,并设置名字读写比例等基本信息// 再通过visitDataSourceConfig设置其他信息,用户名,密码等// 可读的部分 if (singleConfig.getReadWeight() >= 0) {ReadOrWriteRole role = new ReadOrWriteRole(singleConfig.getName(), true, false);DataSourceConfig dataSource = groupDsConfig.findOrCreateDataSourceConfig(role);dataSource.setCanRead(true);dataSource.setWeight(singleConfig.getReadWeight());visitDataSourceConfig(dataSource);}// 可写的部分if (singleConfig.getWriteWeight() >= 0) //...}}//验证数据源的信息,足够数量的读写库validateConfig(groupDsConfig.getDataSourceConfigs());}// 设置切面groupDsConfig.setFilters(getProperty(String.format("%s.default.filters",Constants.DEFAULT_DATASOURCE_ZEBRA_PRFIX), null));// 初始化路由策略, 默认为CenterAwareRouter
groupDsConfig.setRouterStrategy(getProperty(String.format(Constants.ROUTER_STRATEGY_LION_KEY_PATTERN, jdbcRef),Constants.ROUTER_STRATEGY_CENTER_AWARE_ROUTER));}
设置监听器

DefaultDataSourceConfigManager的父类AbstractConfigManager既是configService服务的订阅者也是GroupSource的消息推送者。

public AbstractConfigManager(ConfigService configService) {this.configService = configService;this.innerPropertyChangeListener = new InnerPropertyChangeListener();this.configService.addPropertyChangeListener(this.innerPropertyChangeListener);}

一旦有配置信息改变,则会去执行onPropertyUpdated函数,它是一个抽象函数,需要子ç类去重写,并且通知所有的订阅者

protected abstract void onPropertyUpdated(PropertyChangeEvent evt);class InnerPropertyChangeListener implements PropertyChangeListener {@Overridepublic void propertyChange(final PropertyChangeEvent evt) {try {onPropertyUpdated(evt);notifyListeners(evt);} catch (Exception e) {logger.error("fail to update property, apply old config!", e);}}}

3.2 初始化

初始化的整个过程如下所示:

  • DataSourceManager进行初始化
  • 对数据源初始化
  • 初始化读写策略
  • 注册配置文件刷新器
protected void initInternal() {SingleDataSourceManagerFactory.getDataSourceManager().init();initDataSources();initReadWriteStrategy();DataSourceConfigRefresh.getInstance().register(this);LOGGER.info(String.format("GroupDataSource(%s) successfully initialized.", jdbcRef));}

3.2.1 初始化数据源管理器

也是一个单例模式,所管理的是单个数据源对象,有如下几个要点:

  • 主要是启动一个后台线程对单一数据源的管理
  • 在关闭数据源的时候将当前数据源加入到队列中
  • 后台线程从阻塞队列中获取要关闭的数据源,否则阻塞在这
public class DefaultSingleDataSourceManager implements SingleDataSourceManager {private Thread dataSourceMonitor;private BlockingQueue<SingleDataSource> toBeClosedDataSource = new LinkedBlockingQueue<SingleDataSource>();@Overridepublic synchronized SingleDataSource createDataSource(DataSourceConfig config, List<JdbcFilter> filters) {return new SingleDataSource(config, filters);}@Overridepublic synchronized void destoryDataSource(SingleDataSource dataSource) {if (dataSource != null) {this.toBeClosedDataSource.offer(dataSource);}}@Overridepublic synchronized void init() {//new thread start}// ... stopclass CloseDataSourceTask implements Runnable {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {SingleDataSource dataSource = null;try {dataSource = toBeClosedDataSource.take();dataSource.close();} catch (ZebraException e) {if (dataSource != null) {try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e1) {}toBeClosedDataSource.offer(dataSource);}} catch (Exception ignore) {}}}}
}

3.2.2 初始化数据源

由于GroupDataSource是由读和写两种数据源组成的,分别由如下对象管理:

  • 读:负载均衡数据源,LoadBalancedDataSource,一般会有多个从数据库分担读的压力
  • 写:故障转移数据源,FailOverDataSource,当主挂掉时,会有从数据库升级为主,实现故障转移
LoadBalancedDataSource

init函数内部,需要对存储在map中的每一个数据源通过dataSourceManager进行初始化,初始化完成后缓存 在内存之中,保存成id->datasource的映射关系

同时对内部的路由器初始化,后文会对路由器进行详细的介绍,这里使用的是backup类型

public void init() {this.dataSourceManager = SingleDataSourceManagerFactory.getDataSourceManager();for (DataSourceConfig config : loadBalancedConfigMap.values()) {checkConfig(config);}for (DataSourceConfig config : loadBalancedConfigMap.values()) {SingleDataSource dataSource = dataSourceManager.createDataSource(config, this.filters);this.dataSources.put(config.getId(), dataSource);}this.router = new BackupDataSourceRouter(loadBalancedConfigMap, configManagerType, configService, routerStrategy);}

#####FailOverDataSource

相较于LoadBalancedDataSource,它并不需要router来进行负载均衡,永远只有一个master,因此只需要对这一个数据源初始化即可

3.2.3 初始化读写策略

读写策略用于控制读请求是走读库还是写库的,使用的是java中的spi机制,从resource/services/中加载实现对应接口的类,并将策略加载到wrapper中,wrapper是一系列的读写策略的集合,同样也实现了ReadWriteStrategy

private void initReadWriteStrategy() {ServiceLoader<ReadWriteStrategy> strategies = ServiceLoader.load(ReadWriteStrategy.class);ReadWriteStrategyWrapper wrapper = new ReadWriteStrategyWrapper();if (strategies != null) {for (ReadWriteStrategy strategy : strategies) {if (strategy != null) {wrapper.addStrategy(strategy);}}}readWriteStrategy = wrapper;refreshReadWriteStrategyConfig();}

3.2.4 注册配置刷新实例

DataSourceConfigRefresh也是一个单例模式,它会启动一个后台进程,每隔30秒遍历一遍dataSourceList中的内容,当内部的数据源的配置刷新间隔大于CHECK_INTERVAL就会调用GroupDataSource中的refresh方法刷新,过程如下:

  • 通过调用buildGroupConfig()获取最新的数据源信息
  • 分别调用故障转移和负载均衡数据源的refresh方法刷新信息
  • 刷新切面
  • 刷新读写策略

两种数据源的刷新方法如下:

FailOverDataSource

  1. 如果配置的config信息没有发生改变,则返回
  2. 新的配置如果为空,则需要销毁原来的master对应的数据源,并且更新配置
  3. 否则,通过新的配置创建数据源,先进行数据源的交换,完成后再销毁原有的数据

LoadBalancedDataSource
和FailOverDataSource类似,首先计算集合的交集来判断是否有数据源的创建,删除和不变化的部分,对对应的数据源操作,最后更新router

3.3 路由

3.3.1 整体架构

  • 可以选择remote或者local两种方式来管理region信息,来配置系统的地理信息
  • RegionAwareRouter内包含多个CenterAwareRouter来管理不同的中心
  • CenterAwareRouter中包括了多个WeightRouter来管理center优先级
    • IdcAware:一个IdcAwareRouter来管理同中心的路由
    • Not IdcAware:WeightRouter来管理一个中心内的所有路由

[外链图片转存中…(img-eTzo8OjD-1587632594771)]

参考Zebra路由设计

3.3.2 路由策略

  1. 按流量权重路由 会根据优先级选择对应中心内所有读库按权重路由,未开启则整个区域内按权重路由

  2. 按机房路由(IdcAware),路由顺序如下:

    1. 优先收集同机房或同中心

      1. 选择同机房的读库,按流量权重路由
      2. 选择同中心其他读库,按流量权重路由
    2. 按优先级选择同区域内其他中心,按流量权重路由
    3. 选择同区域非中心的读库,按流量权重路由
  3. 不按机房路由,路由顺序如下:

    1. 选择同中心的读库,按流量权重路由
    2. 按照优先级选择其他中心的读库,按流量权重路由
    3. 选择同区域非中心的读库,按流量权重路由

3.3.3 region管理器

地理信息的路由管理器的层级结构如下图所示,需要实现ZebraRegionManager接口,AbstractZebraRegionManager的内部实现了绝大多数的功能,它的两个子类只需要重写自己的初始化逻辑即可

region管理器需要实现接口中的以下功能对地域的层级结构进行维护,通过抽取连接数据库的URL和地域的配置进行对比来判断是属于哪个机房或者中心

public interface ZebraRegionManager {String NO_CENTER = "NoCenter";// 初始化void init();// 是否是同一个机房boolean isInSameIdc(String address1, String address2);// 是否在同一个区域boolean isInLocalRegion(String address);// 是否在同一个机房boolean isInLocalIdc(String address);// 通过ip地址获取机房名String getIdc(String address);// 是否在同一个中心boolean isInLocalCenter(String address);// 通过ip地址获取中心名String findCenter(String address);// 通过中心的名字获取对应的优先级int getCenterPriority(String centerName);// 获取所在的中心String getLocalCenter();
}

以下将针对LocalRegionManager进行分析, 在init()中通过输入流对resource/region/center.json和``resource/region/region.json`配置文件进行读取,通过对JSON进行解析

CenterConfig

CenterConfig主要包含region下对应的center信息包括优先级以及center下所包含的机房的信息,例子如下

{"regions" : [{"region" : "region1", "centers" : [{"center":"center1","priority":2, "idcs":[{"idc" : "idc1", "desc" : "机房1"},{"idc" : "idc2", "desc" : "机房2"},{"idc" : "idc3", "desc" : "机房3"}]},{"center":"center2","priority":1,"idcs":[{"idc" : "idc4", "desc" : "机房4"}]}]},{"region" : "region2", "centers" : [{"center":"center3","priority":1,"idcs":[{"idc" : "idc5", "desc" : "机房5"},{"idc" : "idc6", "desc" : "机房6"}]}]}]
}

解析的结果保存在两个map的数据结构中,分别是:

  • priorityMap用于存放从中心的名字(center-name)到优先级(priority)的映射关系
  • centerMap用于存放从机房名字(idc-name)到中心名字(center-name)的映关系

RegionConfig

RegionConfig主要保存区域下的机房名称和机房内部的网段信息以及相关的注释

{"regions" : [{"region" : "region1", "idcs" : [{"idc" : "idc1", "net" : ["192.1", "192.2"], "desc" : "机房1"},{"idc" : "idc2", "net" : ["192.3", "192.4"], "desc" : "机房2"},{"idc" : "idc3", "net" : ["192.5"], "desc" : "机房3"},{"idc" : "idc4", "net" : ["192.6"], "desc" : "机房4"}]},{"region" : "region2", "idcs" : [{"idc" : "idc5", "net" : ["192.10", "192.11"], "desc" : "机房5"},{"idc" : "idc6", "net" : ["192.20", "192.21"], "desc" : "机房6"}]}]
}

需要特别注意的是这里使用自定义实现的字典树实现根据ip段的层级管理,其中每个TrieNode节点,包含了以下信息:

  • key为Integer类型,为ip段号
  • value为String类型,指示当前的region信息
  • attributes保存其他的属性,包括idc机房名称,desc描述信息
  • children为子树,使用有序的map保存
public class TrieNode<K, V> {private K key;private SortedMap<K, TrieNode<K, V>> children;private Comparator<K> keyComparator;private V value;private Map<String, String> attributes;//...
}

下面将按照接口中要求实现的方法,对源码进行分析

isInSameIdc(String address1, String address2)

判断两个地址是否是同一个IDC

  1. 首先判断两个地址是否相同
  2. 通过地址获取对应的的IDC名字进行对比

为了通过地址查找对应的IDC的名字,需要在TrieNode中查找节点,采用深搜的办法一层一层寻找节点,找到后返回从Attribute中获取的idc信息

protected String _getIdc(String address) {TrieNode<Integer, String> node = _getNode(address);return node.getAttribute("idc");}private TrieNode<Integer, String> _getNode(String address) {if (regionTrie == null)return null;String[] dots = address.split("\\.");TrieNode<Integer, String> parentNode = regionTrie;for (String dot : dots) {try {Integer key = Integer.valueOf(dot.trim());TrieNode<Integer, String> node = parentNode.getChild(key);if (node != null) {parentNode = node;} else {break;}} catch (NumberFormatException nfe) {break;}}return parentNode;}

isInLocalRegion(String address)

判断地址是否是相同的区域

通过地址获取当前的region和localRegion进行对比,通过地址获取region的名称,实现的方式也类似,首先查找到地址对应的node,并返回node内的value

protected String _getRegion(String address) {TrieNode<Integer, String> node = _getNode(address);return node.getValue();}

其他

其他的方法实现和以上都类似,基本都是在TrieNode中寻找节点,返回节点对应的信息

3.3.4 路由策略实现

这部分将自上而下和自下而上两种方式对路由策略的实现进行解析,首先介绍顶层的RegionRouter,然后介绍底层的WeightRouter和IdcRouter,最后介绍CenterRouter,将底层的几种路由结合起来。

RegionAwareRouter

首先,将数据源和当前的服务是否在同一个region进行分类,分别存放在两个map中,存放的是id到DataSourceConfig的映射关系。然后根据路由的策略,初始化local和remote的路由

if (Constants.ROUTER_STRATEGY_REGION_AWARE_ROUTER.equals(routerStrategy)) {// 区域内使用权重路由if (localRegionDataSourceConfigs.size() > 0) {this.localRegionRouter = new WeightDataSourceRouter(localRegionDataSourceConfigs);}if (remoteRegionDataSourceConfigs.size() > 0) {this.remoteRegionRouter = new WeightDataSourceRouter(remoteRegionDataSourceConfigs);}} else {// 区域内使用中心路由或机房路由boolean idcAware = false;if (Constants.ROUTER_STRATEGY_IDC_AWARE_ROUTER.equals(routerStrategy)) {idcAware = true;}if (localRegionDataSourceConfigs.size() > 0) {this.localRegionRouter = new CenterAwareRouter(localRegionDataSourceConfigs, configManagerType, configService, idcAware);}if (remoteRegionDataSourceConfigs.size() > 0) {this.remoteRegionRouter = new CenterAwareRouter(remoteRegionDataSourceConfigs, configManagerType, configService, idcAware);}}

WeightDataSourceRouter

通过TreeSet来选择集合中大于给定数据的最小元素来根据权重计算来选择数据源:

  • 计算weight的总和
  • RouterTarget内部保存3个维度的信息,包括id,weight,end,end为sum(weights)-1
public WeightRandom(Map<String, DataSourceConfig> configs) {for (DataSourceConfig config : configs.values()) {int weight = config.getWeight();groupDataSourceTargetSize += weight;RouterTarget groupDataSourceTarget = new RouterTarget(config.getId(), weight, groupDataSourceTargetSize - 1);targets.add(groupDataSourceTarget);}}

select方法中,首先要排除掉排除的目标,重新拷贝一个weights,然后从[0,sum(weights))生成一个随机数,然后通过treeSet直接选择节点

public RouterTarget select(Set<RouterTarget> excludeTargets) {if (!this.targets.isEmpty()) {TreeSet<RouterTarget> weights = this.targets;int tmpGroupDataSourceTargetSize = this.groupDataSourceTargetSize;if (excludeTargets != null && !excludeTargets.isEmpty()) {// 需要排除某些GroupDataSourceTarget的话,就重新copy一个weightsTreeSet<RouterTarget> copyWeights = new TreeSet<RouterTarget>();tmpGroupDataSourceTargetSize = 0;for (RouterTarget routerTarget : weights) {if (excludeTargets.contains(routerTarget)) {continue;}int weight = routerTarget.getWeight();tmpGroupDataSourceTargetSize += weight;copyWeights.add(new RouterTarget(routerTarget.getId(), weight, tmpGroupDataSourceTargetSize - 1));}weights = copyWeights;}if (weights.isEmpty() || tmpGroupDataSourceTargetSize <= 0) {return null;}int randomNum = random.nextInt(tmpGroupDataSourceTargetSize);RouterTarget tempForSearch = new RouterTarget(null, -1, randomNum);return weights.ceiling(tempForSearch);} else {return null;}}

IdcAwareRouter

和RegionAwareRouter类似,根据是否是同一个IDC来分类,两个router又可以分别根据权重进行路由

//...
// 根据是否是同一个IDC分类
if (urlAndPort != null && urlAndPort.length > 0) {if (regionManager.isInLocalIdc(urlAndPort[0])) {localIdcDataSourceConfigs.put(dsId, config);} else {remoteIdcDataSourceConfigs.put(dsId, config);}}
// 分别构建基于权重策略的router
if (localIdcDataSourceConfigs.size() > 0) {this.localIdcWeightedRouter = new WeightDataSourceRouter(localIdcDataSourceConfigs);}
if (remoteIdcDataSourceConfigs.size() > 0) {this.remoteIdcWeightedRouter = new WeightDataSourceRouter(remoteIdcDataSourceConfigs);}

CenterAwareRouter

从数据源获取地址,将数据源分成三类:

  • 是否是同一个机房,放入localIdcSourceConfigs中,保存dsId到数据源配置的映射
  • 是否是同一个中心,放入localCenterSourceConfigs中,保存dsId名字到数据源配置的映射
  • 不是同一个中心的,放入remoteCenterSourceConfigs中,保存中心名,以及dsId和数据源配置的映射

同样,也是有两个router去路由请求,处理流程如下:

  • 如果是机房优先路由,则localRouter为IdcAwareRouter类型,将localIdc与localCenter的数据源合并,IdcAwareRouter内部会再做区分
  • 否则根据权重路由
    • 如果当前服务在中心中,将LocalIdc加入到localCenter中,则localRouter在中心内根据权重去路由
    • 如果当前服务不在中心中,则将LocalIdc和localCenter全部标记为NO_CENTER,加入到remoteCenterSourceConfigs
  • 其他的center根据优先级进行选择,之后在中心内部再根据权重进行路由,NO_CENTER的优先级最低
// 同一个idc或同一个centerif (idcAware) {localCenterSourceConfigs.putAll(localIdcSourceConfigs);if (localCenterSourceConfigs.size() > 0) {this.localCenterRouter = new IdcAwareRouter(localCenterSourceConfigs, configManagerType, configService);}} else {String localCenter = regionManager.getLocalCenter();if ((localCenter == null || ZebraRegionManager.NO_CENTER.equals(localCenter))&& localCenterSourceConfigs.size() <= 0) {// 不在中心内if (localIdcSourceConfigs.size() > 0) {Map<String, DataSourceConfig> centerDataSourceConfigs = remoteCenterSourceConfigs.get(ZebraRegionManager.NO_CENTER);if (centerDataSourceConfigs == null) {centerDataSourceConfigs = new HashMap<String, DataSourceConfig>();remoteCenterSourceConfigs.put(ZebraRegionManager.NO_CENTER, centerDataSourceConfigs);}centerDataSourceConfigs.putAll(localIdcSourceConfigs);}} else {// 在中心内localCenterSourceConfigs.putAll(localIdcSourceConfigs);this.localCenterRouter = new WeightDataSourceRouter(localCenterSourceConfigs);}}// 其他center, 按优先级排列if (remoteCenterSourceConfigs.size() > 0) {List<CenterDsConfigWrapper> centerDsConfigWrappers = sortByCenterPriority(remoteCenterSourceConfigs);for (CenterDsConfigWrapper wrapper : centerDsConfigWrappers) {priorityCenterIdcAwareRouters.add(new WeightDataSourceRouter(wrapper.getCenterSourceConfigs()));}}

4. 总结

GroupDataSource所涉及的东西比SingleDataSource多了许多,包括了配置文件的解析、管理,连接请求的路由以及路由策略和实现等,本文主要讲解读写分离数据中独有的部分,相似的内容则不再赘述。

Zebra源码分析-GroupDataSource相关推荐

  1. zebra源码分析-导读

    zebra源码分析-导读 客户端架构 JDBC 核心部分介绍 代码流程 数据源 Statement 参考 zebra源码分析-导读 zebra是一个基于JDBC API协议上开发出的高可用.高性能的数 ...

  2. Zebra源码分析-SingleDataSource

    Zebra源码分析-SingleDataSource 1. 简介 1.1 层级结构 1.2 内部结构 2. 使用示例 2.1 直接使用JDBC 2.2 结合MyBatis以及Spring 3.源码分析 ...

  3. dynamic-datasource-spring-boot-starter源码分析

    目录 一.简介 二.源码分析 2.1 整体结构 2.2 自动配置怎么实现的 2.3 如何集成众多连接池 2.4 DS注解如何被拦截处理的 2.5 多数据源动态切换及如何管理多数据源 2.6 组数据源的 ...

  4. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  5. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  6. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  7. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  8. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  9. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

最新文章

  1. 一分钟详解OpenCV之相机标定函数calibrateCamera()
  2. python详细安装教程环境配置-python环境安装详细步骤
  3. 在linux下安装matlab2013a的经验
  4. OnInit 和 Page_Init 事件有什么不同
  5. mongo db 分享 ppt
  6. 分辨率_电视分辨率多少合适
  7. 22.1-在散列集上进行集合操作
  8. 菌群代谢物和宿主:如何影响肝脏疾病?
  9. linux tc取消网卡流量限制,Linux高级流量控制tc使用
  10. 上市只是开端,库客音乐用版权打出组合拳
  11. 《近匠》专访启明星辰安全研究中心副总监侯浩俊——物联网安全攻防的“线上幽灵”...
  12. android 隐藏图标_苹果手机竟然还有这么多隐藏小功能(一)
  13. 计算机系统:异常控制流
  14. Android使用Fragment打造万能页面切换框架(三)
  15. 贝叶斯例题(一)先验分布与后验分布
  16. 5福最多多少钱_2018支付宝集5福最块获得五福卡方法 支付宝集五福福卡怎么获得...
  17. 关于left与offsetLeft的区别
  18. 计算机导论期末考试知识点,计算机导论期末复习(知识点).doc
  19. 【无标题】java求等腰梯形面积
  20. 牛仔裤品牌!!大排行

热门文章

  1. 分类信息 - 网址大全
  2. 技术分享 | 常见接口协议解析
  3. 关于各大输入法词库格式之间的转换
  4. 如何计算产生SPWM所需要的占空比
  5. 视频播放ExoPlayer(附小demo)
  6. 11选5经典技巧 收集的
  7. Codeforces 575C 状压+KM
  8. cisco ap 上线不成功
  9. linux 显卡驱动 在哪里,linux下如何看我的显卡驱动是否装好了
  10. [Qt学习笔记]Qt程序加密,实现软件运行次数和硬件信息绑定