后台提供高并发系列历史文章阶段总结版,欢迎关注
高并发系列历史文章微信链接文档
垂直性能提升
1.1. 架构优化:集群部署,负载均衡
1.2. 万亿流量下负载均衡的实现
1.3. 架构优化:消息中间件的妙用
1.4. 架构优化:用消息队列实现存储降级
1.5. 存储优化:mysql的索引原理和优化
1.6. 索引优化补充篇:explain索引优化实战
1.7. 存储优化:详解分库分表
1.8. 本篇内容:详解数据库中间件
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 方法,就是很好的切入点。个人思路,希望对大家有所帮助。
评论