以下文章来源于Coder的技术之路 ,作者Coder的技术之路
前支付宝员工,现任某大厂资深研发。努力,让自己从事真正的技术工种,而不是熟练工种。。。
1、数据库中间件有啥用
有一天,你去三亚玩耍,就想玩个冲浪。即使你不差钱,难道还要自己采买快艇、滑板等装备来满足这为数不多的心血来潮吗?租一个就行了嘛。这其实就是连接池的作用。
数据库中间件可以理解为是一种具有连接池功能,但比连接池更高级的、带很多附加功能的辅助组件。不仅可以租冲浪板,还可以提供地点推荐、上保险等等各类服务。
从网上的资料看,zdal 应该算是半开源的。好像是之前开源过,但后续没有准备维护,然后就删除了。不过 GitHub 被 Fork 下来好多,随便一搜就是一片。当前,只是老的版本。目前蚂蚁内部的 zdal 好像已经更新到 zdal5 了吧,那咱可就看不到了。
越复杂的系统,数据库中间件的作用越大。就拿 zdal 来说,它提供分库分表、结果集合并、SQL 解析、数据库 Failover 动态切换等数据访问层统一解决方案。下面就一起来看下,其内部实现是怎么样的。
如上图所示,zdal 有四个重要的组成部分:
组件图对整体架构和各组件及相互联系的理解可以起到很好的帮助。一个简版的组件图画了好久,还有不少错,不过大概是这么个意思,哎,基本功要丢~
对照上图可以比较清晰地看到:
大部分情况下,我们使用如 MyBatis 这样的 ORM 框架来进行数据库操作。其实,不管是 ORM 还是其他方式,应用层都需要对数据源进行配置。
所以,client 对外暴露了一个符合 JDBC 标准的 datasource 数据源 ZdalDataSource,用来满足应用层 ORM 等框架配置数据源的要求。
// 只提供了一个 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 配置加载的启动流程。下面我们就来详细看一下,读写分离和分库分表的规则是怎么被加载,怎么起作用的。
首先,我们需要有数据源的相关配置,如下图:
此 XML 配置会在 init 方法被调用时被初始化,解析成 ZdalConfig 类的属性。ZdalConfig 类的主要成员见下面代码:
public class ZdalConfig {// key=dsName;value=DataSourceParameter// 所有物理数据源的配置项, 比如用户名、密码、库名等private Map < String, DataSourceParameter > dataSourceParameters = new ConcurrentHashMap < String, DataSourceParameter > ();// 逻辑数据源和物理数据源的对应关系:// key=logicDsName,value=physicDsNameprivate 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 对应的 DBSelecterif (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;}
4、总结
本文把阿里数据库中间件相关的组件和加载流程进行了总结,就一个最基本的分组读写分离的流程,对内部实现进行了阐述。说是解析,其实是提供给大家一种阅读的思路,毕竟篇幅有限,如果对中间件感兴趣的同学,可以 Fork 代码,按上述逻辑自己阅读。
看源码时,比如 Dubbo 这些中间件其实是比较容易入手的,因为他们都依托于 Spring 装载 JavaBean。所以,对 Spring 容器暴露的那些 init、load 方法,就是很好的切入点。个人思路,希望对大家有所帮助。
- EOF -
1、掘地三尺搞定 Redis 与 MySQL 数据一致性问题
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
点赞和在看就是最大的支持❤️