专栏首页Coder的技术之路一款优秀数据库中间件的不完全解析

一款优秀数据库中间件的不完全解析

更多高并发应对文章,请扫码关注
  1. 数据库中间件有啥用
  2. 架构剖析之高屋建瓴 2.1. 整体概述 2.2. 组件图看架构
  3. 细节剖析之一叶知秋 3.1. 配置加载和bean初始化 3.2. 细说读写分离
  4. 总结

Part1数据库中间件有啥用

有一天,你去三亚玩耍,就想玩个冲浪,即时你不差钱,难道还要自己采买快艇、滑板等等装备来满足这为数不多的心血来潮么。租一个就行了嘛。这其实就是连接池的作用。

数据库中间件可以理解为是一种具有连接池功能,但比连接池更高级的、带很多附加功能的辅助组件,不仅可以租冲浪板,还可以提供地点推荐、上保险等等各类服务。

从网上的资料看,zdal应该算是半开源的,好像是之前开源过,但后续没有准备维护,然后就删除了,不过github被fork下来好多,随便一搜就是一片,当前,只是老的版本。目前蚂蚁内部的zdal好像已经更新到zdal5了吧,那咱可就看不到了。

越复杂的系统,数据库中间件的作用越大。就拿zdal来说,它提供分库分表,结果集合并,sql解析,数据库failover动态切换等数据访问层统一解决方案。下面就一起来看下,其内部实现是怎么样的。

Part2架构剖析之高屋建瓴

2.1整体概述

如上图所示,zdal有四个重要的组成部分:

  • 价值体现--客户端Client包。对外暴露基本操作接口,用于业务层简单黑盒的操作数据源;业务只和client交互,动态切换/路由等逻辑只需要进行规则配置,相关逻辑由zdal实现。
  • 核心功能--连接管理datasource包。最核心的能力,提供多种类型数据库的连接管理;不管功能多花哨,最终目的还是为了解决数据库连接的问题。
  • 关键能力--SQL解析parser包。基础SQL解析能力;解析sql类型、字段名称、数据库等等,配合规则进行路由
  • 扩展能力--库表路由rule包。根据parser解析出的字段确定逻辑库表和物理库表。

2.2组件图看架构

组件图对整体架构和各组件及相互联系的理解可以起到很好的帮助。一个简版的组件图画了好久,还有不少错,不过大概是这么个意思,哎,基本功要丢~

对照上图可以比较清晰的看到:

  • Client包对应用层暴露的数据源、负责监听配置动态变更的监听组件、负责加载组织各部分的配置组件、负责加载spring bean 和库表规则的配置组件;
  • Client中加载了规则组件,实现逻辑表和数据库的路由规则。
  • Client中的库表配置调用datasource中的数据源管理服务并构建连接池的连接池;
  • Client中的SqlDispatcher服务调用SQL解析组件实现SQL解析。

Part3细节剖析之一叶知秋

3.1配置加载和bean初始化

大部分情况下,我们使用如mybatis这样的ORM框架来进行数据库操作,其实不管是ORM还是其他方式,应用层都需要对数据源进行配置。

所以,client对外暴露了一个符合JDBC标准的datasource数据源,用来满足应用层ORM等框架配置数据源的要求--ZdalDataSource

如图片被压缩看不清,后台回复<zdal类图>获取

//只提供了一个init方法,这也是spring启动时时,必须要调用的初始化方法,所有功能,都从这里开始
public class ZdalDataSource extends AbstractZdalDataSource implements DataSource{
    public void init() {
        try {
            super.initZdalDataSource();
        } catch (Exception e) {
            CONFIG_LOGGER.error("...");
            throw new ZdalClientException(e);
        }
    }

ZdalDataSource#init() 方法即为配置加载的核心入口,init中负责加载spring配置,根据配置初始化数据源,并创建连接池,同时,将逻辑表和物理库的对应关系都维护起来供后续路由调用。

    /*父类的init方法*/
protected void initZdalDataSource() {
    /*用FileSystemXmlApplicationContext方式加载配置文件中的数据源和规则,转化成zdalConfig对象*/
    this.zdalConfig = ZdalConfigurationLoader.getInstance().getZdalConfiguration(appName,dbmode, appDsName, configPath);
    this.dbConfigType = zdalConfig.getDataSourceConfigType();
   this.dbType = zdalConfig.getDbType();
   //初始化数据源
   this.initDataSources(zdalConfig);
   this.inited.set(true);
    }
}

从上面的类图和这里的两个入口方法大概了解到zdal配置加载的启动流程。下面我们就来详细看一下,读写分离和分库分表的规则是怎么被加载,怎么起作用的。

3.2细说读写分离

读写分离配置的加载

首先,我们需要有数据源的相关配置,如下图:

此XML配置会在init方法被调用时,被初始化,解析成ZdalConfig类的属性,ZdalConfig类的主要成员见下面代码:

public class ZdalConfig {
    /** key=dsName;value=DataSourceParameter 所有物理数据源的配置项,比如用户名,密码,库名等 */
    private Map<String, DataSourceParameter> dataSourceParameters = new ConcurrentHashMap<String, DataSourceParameter>();
    /** 逻辑数据源和物理数据源的对应关系:key=logicDsName,value=physicDsName */
    private Map<String, String>              logicPhysicsDsNames  = new ConcurrentHashMap<String, String>();
    /** 数据源的读写规则,比如只读,或读写等配置*/
    private Map<String, String>              groupRules           = new ConcurrentHashMap<String, String>();
    /** 异常转移的数据源规则*/
    private Map<String, String>              failoverRules        = new ConcurrentHashMap<String, String>();
    //一份完整的读写分离和分库分表规则配置
    private AppRule                          appRootRule;

可以看到,xml中的规则,被解析到xxxRules里。这里以groupRules为例,failover同理。

下一步则是通过解析得到的zdalConfig 来初始化数据源:

protected final void initDataSources(ZdalConfig zdalConfig) {
    //DataSourceParameter中存的是数据源参数,如用户名密码,最大最小连接数等
    for (Entry<String, DataSourceParameter> entry : zdalConfig.getDataSourceParameters().entrySet()) {
        try {
           //初始化连接池
           ZDataSource zDataSource = new ZDataSource(/*设置最大最小连接数*/createDataSourceDO(entry.getValue(),zdalConfig.getDbType(), appDsName + "." + entry.getKey()));
           this.dataSourcesMap.put(entry.getKey(), zDataSource);
        } catch (Exception e) {
            //...
        }
   }
  //其他分支略,只看最简单的分组模式
  if (dbConfigType.isGroup()) {
       //读写配置赋值
       this.rwDataSourcePoolConfig = zdalConfig.getGroupRules();
       //初始化多份读库下的负载均衡
       this.initForLoadBalance(zdalConfig.getDbType());
  }
  //注册监听:为了满足动态切换
  this.initConfigListener();
}

initForLoadBalance的方法如下:

private void initForLoadBalance(DBType dbType) {
    Map<String, DBSelector> dsSelectors = this.buildRwDbSelectors(this.rwDataSourcePoolConfig);
    this.runtimeConfigHolder.set(new ZdalRuntime(dsSelectors));
    this.setDbTypeForDBSelector(dbType);
}

可以看到,首先构建出了DB选择器,然后赋值给了runtimeConfigHolder供运行时获取。而构建DB选择器的时候,其实是按读写两个维度,把所有数据源都构建了一遍,即group_r和group_w下都包含5个数据源,只不过各自的权重不一样:

//比如按上面的配置写库只有一个,但是也会包含全数据源

group_0_w_0 :< bean:read0DataSource , writeWeight:0>
group_0_w_1 :< bean:writeDataSource , writeWeight:10>
group_0_w_2 :< bean:read1DataSource , writeWeight:0>
group_0_w_3 :< bean:read2DataSource , writeWeight:0>
group_0_w_4 :< bean:read3DataSource , writeWeight:0>

//上述就是写相关的DBSelecter的内容。

读写分离怎么起作用

以delete为例,更新删除是要操作写库的

 public void delete(ZdalDataSource dataSource) {
     String deleteSql = "delete from test";
     Connection conn = null;
     PreparedStatement pst = null;
     try {
        conn = dataSource.getConnection();
        pst = conn.prepareStatement(deleteSql);
        pst.execute();
     } catch (Exception e) {
            //...
     } finally {
           //资源关闭
     }
 }

getConnection会从上文中提到的runtimeConfigHolder中获取DBSelecter,然后执行execute方法

 public boolean execute() throws SQLException {
    SqlType sqlType = getSqlType(sql);
    // SELECT相关的就选择group_r对应的DBSelecter
   if (sqlType == SqlType.SELECT || sqlType == SqlType.SELECT_FOR_UPDATE|| sqlType == SqlType.SELECT_FROM_DUAL) {
     //略
    return true;
    //update/delete相关的就选择group_w对应的DBSelecter
  } else if (sqlType == SqlType.INSERT || sqlType == SqlType.UPDATE|| sqlType == SqlType.DELETE) {
       if (super.dbConfigType == DataSourceConfigType.GROUP) {
           executeUpdate0();
       } else {
           executeUpdate();
      }
      return false;
  } 
}

如果是读取相关的,那就选_r的DBSelecter,如果是写相关的,那就选_W的DBSelecter。那么executeUpdate0中是怎么执行区分读写数据源的呢,其实就是把这一组的数据源根据权重筛选一遍。

// WeightRandom#select(int[], java.lang.String[])
private String select(int[] areaEnds, String[] keys) {
   //这里的areaEnds数组,是一个累加范围值数据
   //比如三个库权重    10   9   8
   //那么areaEnds就是  10  19  27 是对每个权重的累加,最后一个值是总和
   int sum = areaEnds[areaEnds.length - 1];
   //这样随机出来的数,是符合权重分布的
   int rand = random.nextInt(sum);
   for (int i = 0; i < areaEnds.length; i++) {
       if (rand < areaEnds[i]) {
           return keys[i];
   }
   return null;
}

Part4总结

本篇文章,把阿里数据库中间件相关的组件和加载流程进行了总结,就一个最基本的分组读写分离的流程,对内部实现进行了阐述。说是解析,其实是提供给大家一种阅读的思路,毕竟篇幅有限,如果对中间件感兴趣的同学,可以fork下代码,按上述逻辑自己阅读下。

看源码时,比如dubbo这些中间件其实是比较容易入手的,因为他们都依托于Spring进行JavaBean的装载,所有,对Spring容器暴露的那些init、load方法,就是很好的切入点。个人思路,希望对大家有所帮助。

高并发系列历史文章微信链接文档

  1. 垂直性能提升 1.1. 架构优化:集群部署,负载均衡 1.2. 万亿流量下负载均衡的实现 1.3. 架构优化:消息中间件的妙用 1.4. 架构优化:用消息队列实现存储降级 1.5. 存储优化:mysql的索引原理和优化 1.6. 索引优化补充篇:explain索引优化实战 1.7. 存储优化:详解分库分表 1.8. 本篇内容:详解数据库中间件

本文分享自微信公众号 - Coder的技术之路(gh_1b3189982966),作者:Coder的技术之路

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-05-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(一)之语法解析

    1. 概述 2. Lexer 词法解析器 3. Token 词法标记 3.2.1 Literals.IDENTIFIER 词法关键词 3.2.2 Litera...

    芋道源码
  • 数据库中间件 MyCAT 源码解析 —— 分片结果合并(一)

    1. 概述 相信很多同学看过 MySQL 各种优化的文章,里面 99% 会提到:单表数据量大了,需要进行分片(水平拆分 or 垂直拆分)。分片之后,业务上必然面...

    芋道源码
  • GitHub 上100个优质前端项目整理,非常全面!

    作 者:小明小明长大了 来 源:https://www.jianshu.com/p/72ca8192f7b8

    开发者技术前线
  • 收藏夹吃灰了:GitHub 上值得收藏的100个精选前端项目

    codrops 一系列具有相当具有创意且有趣的前端效果的集合,是非常棒的学习资料,可以欣赏和下载使用。并且有些项目,也托管到了github仓库中

    王小婷
  • GO笔记之为什么要学习GO

    之类问题经常出现在眼前。以前学语言时倒没怎么关心过这类问题。今年公司由于新业务需要开始全面从PHP转型到Golang。所以我学习它也就是为了工资。额?不能这么俗...

    波罗学
  • 一款 0 门槛轻松易上手的数据可视化工具

    ? 在职场中有一项共识是:数据驱动业务价值。业务在产品、运营、开发、技术支持、销售等环节都有着大量的数据需求, 市面上也出现了很多 BI 可视化工具,但如果能...

    腾讯技术工程官方号
  • 适合 Python 入门的 8 款强大工具!

    Python是一种开源的编程语言,可用于Web编程、数据科学、人工智能以及许多科学应用。学习Python可以让程序员专注于解决问题,而不是语法。由于Python...

    龙哥
  • 8 款强大工具适合 Python 入门的你

    Python是一种开源的编程语言,可用于Web编程、数据科学、人工智能以及许多科学应用。学习Python可以让程序员专注于解决问题,而不是语法。由于Python...

    小小科
  • 适合 Python 入门的 8 款强大工具!

    Python是一种开源的编程语言,可用于Web编程、数据科学、人工智能以及许多科学应用。学习Python可以让程序员专注于解决问题,而不是语法。由于Python...

    Python知识大全
  • 数据可视化产品选型指南

    大数据文摘
  • 只会Excel怎么够?这49款数据可视化神器推荐收藏

    新媒体管家 ? 大数据时代,你还在拿Excel做的图表提交给Boss看吗?有没有想过用其他更炫酷的工具让Boss眼前一亮呢?为了让大家了解如何选择适合的数据可视...

    钱塘数据
  • 90天「高效学习」之后,分享下我的数据分析学习经验

    许多刚刚接触数据分析的人或者转行想从事业务类数据分析的人来说如何学习才能学以致用,是一个尤为重要的问题,结合我的亲身经历讲一讲我的一些总结及看法: 对于想从事业...

    CDA数据分析师
  • 【干货】数据可视化分析工具大集合

    Excel Excel作为一个入门级工具,是快速分析数据的理想工具,也能创建供内部使用的数据图,但是Excel在颜色、线条和样式上课选择的范围有限,这也意味着...

    钱塘数据
  • 大数据与云计算技术周报(第150期)

    影响Hive效率的几乎从不是数据量过大,而是数据倾斜、数据冗余、job或I/O过多、MapReduce分配不合理等等。对Hive的调优既包含对HiveQL语句本...

    大数据和云计算技术
  • 推荐一些优秀的甲方安全开源项目

    这是一份甲方安全开源项目清单,收集了一些比较优秀的安全开源项目,以帮助甲方安全从业人员构建企业安全能力。这些开源项目,每一个都在致力于解决一些安全问题。

    Bypass
  • 数据可视化分析工具大集合

    俗话说“巧妇难为无米之炊”。数据时代,没有一款好的数据可视化分析工具,光有团队怎么行? 商场如战场,数据是把枪。亚马逊运用大数据为客户推荐商品信息,阿里用大数据...

    小莹莹
  • 如何做 Nginx 安全日志分析可视化?

    ModSecurity 这款优秀的开源 WAF,它是一个入侵检测与阻止的引擎,原本是Apache的一个模块,现在可作为单独模块编译添加到 Nginx 服务中

    用户1685462
  • 如何做 Nginx 安全日志分析可视化,看这一篇就够了

    之前介绍过 ModSecurity 这款优秀的开源 WAF,它是一个入侵检测与阻止的引擎,原本是Apache的一个模块,现在可作为单独模块编译添加到 Nginx...

    PHP开发工程师
  • 如何做Nginx安全日志分析可视化

    之前介绍过ModSecurity这款优秀的开源WAF,它是一个入侵检测与阻止的引擎,原本是Apache的一个模块,现在可作为单独模块编译添加到Nginx服务中

    李俊鹏

扫码关注云+社区

领取腾讯云代金券