如何构建Zdal,这篇文章主要介绍如何在传统的Java Web项目中引入Zdal,来达到分库或者分表的目的,本文是《zdal设计文档》的实战补充。

添加zdal-client依赖

在Zdal项目中有很多模块,这些模块分别有着不同的功能和角色,在zdal-doc中的《zdal设计文档》中是这么给每个模块定义的:

  • zdal-client : 加载本地配置文件,初始化各种数据源,调整数据源的配置信息,路由规则等

  • zdal-datasource : 管理访问数据库连接的组件,控制app对数据库资源的使用,目前支持mysql,oracle,db2等数据库的数据访问

  • zdal-parser : 手工编写的高性能的方便扩展的SQL Parser,支持MySQL、Oracle,DB2等流行关系数据库的SQL Parser

  • zdal-rule : 在分库分表中,根据拆分字段进行选库选表的组件,基于groovy规则引擎

  • zdal-common : Zdal功能所用到一些公共组件类

我们在使用zdal中间件的时候其实只需要在POM中引用zdal-client即可,zdal-client已经依赖了内部其他组件,同时建议剔除掉对Spring的引用,主要是为了防止Spring版本的冲突:

<dependency> 
    <groupId>com.alipay.zdal</groupId> 
    <artifactId>zdal-client</artifactId> 
    <version>0.0.1</version> 
    <exclusions> 
        <exclusion> 
            <groupId>org.springframework</groupId> 
            <artifactId>spring</artifactId> 
        </exclusion> 
    </exclusions> 
</dependency>

DataSource替换成ZdalDataSource

笔者在《分库分表技术概览》一文中总结过分库分表现在主要是两种解决方案:应用层依赖类中间件和中间层代理类中间件。Zdal属于应用层依赖类中间件,主要是通过重写JDBC接口的方式来实现的,应用层使用ORM框架并没有本质的影响,因为ORM框架底层还是用的JDBC技术来访问数据库。

在JDBC规范中主要是通过DataSource来获取数据库连接,而Zdal也提供了这样一种与以往不同的DataSource。传统项目中可能使用c3p0这种有连接池功能的DataSource,类似于这种配置:

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> 
    <property name="driverClass" value="com.mysql.jdbc.Driver"/> 
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/fantasy"/> 
    <property name="user" value="root"/> <property name="password" value=""/> 
</bean>

现在Zdal中间件为了对上层尽可能的透明,尽量少的修改业务代码,把分库分表的逻辑封装在DataSource下面,对上层提供ZdalDataSource的实现,用户替换掉老的c3p0数据源即可:

<bean id="dataSource" class="com.alipay.zdal.client.jdbc.ZdalDataSource" init-method="init" destroy-method="close"> 
    <property name="appName" value="fantasy"/> 
    <property name="appDsName" value="fantasyDs"/> 
    <property name="dbmode" value="dev" /> 
    <property name="configPath" value="classpath:zdal" /> 
</bean>

这里的appName即应用项目的名字,appDsName是真实数据源的名字,dbmode的初衷是为了区分开发和生产环境(其实现在很多maven项目可以通过profile和命令行指定的方式来指定特定环境的文件,这种dbmode的方式其实作用不大),configPath是真实数据库配置和规则配置文件的基本地址。

原来的zdal代码中configPath不支持从classpath中加载,可以在ZdalConfigurationLoader中删除 File configurationFile = new File(configPath, MessageFormat.format(Constants.LOCAL_CONFIG_FILENAME_SUFFIX, appName, dbMode)) 这种检验文件是否存在的代码,因为用 java.io.File 类就要求配置文件必须是在文件系统中真实存在,而实际上配置文件可能在jar包中,并没有对应于文件系统中的文件,而且Spring加载文件的时候自己会检验文件是否存在,修改后的getZdalConfigurationFromLocal方法如下:

private synchronized Map<String, ZdalConfig> getZdalConfigurationFromLocal(String appName, String dbMode, String appDsName, 
    String configPath) { 
     List<String> zdalConfigurationFilePathList = new ArrayList<String>(); 
     zdalConfigurationFilePathList.add(configPath + "/" + MessageFormat.format( 
                Constants.LOCAL_CONFIG_FILENAME_SUFFIX, appName, dbMode)); 
     zdalConfigurationFilePathList.add(configPath + "/" + MessageFormat.format( 
            Constants.LOCAL_RULE_CONFIG_FILENAME_SUFFIX, appName, dbMode)); 
     return loadZdalConfigurationContext(zdalConfigurationFilePathList 
        .toArray(new String[zdalConfigurationFilePathList.size()]), appName, dbMode); 
}

这样修改后loadZdalConfigurationContext中使用FileSystemXmlApplicationContext就可以支持classpath、file前缀来加载不同协议的XML文件。

在配置文件中声明分库分表规则

zdal的配置文件主要有两个,并且默认使用了一种规则,从getZdalConfigurationFromLocal的代码中可以看出来,即 appName-dbMode-ds.xml和appName-dbMode-rule.xml ,这里的appName和dbMode要和dataSource的设置以及两个配置文件内部bean中的value保持一致。

比如笔者在Github中的fantasy项目的配置文件为fantasy-dev-ds.xml,主要用来指定物理数据源有哪些,是使用group、shard、shardfailover、shardgroup的哪种模式,内容如下:

<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" 
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	   xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd"> 
	 
	<bean id="fantasy" class="com.alipay.zdal.client.config.bean.ZdalAppBean"> 
		<property name="appName" value="fantasy" /> 
		<property name="dbmode" value="dev" /> 
		<property name="appDataSourceList"> 
			<list>		 
				<ref bean="fantasyDs" /> 
			</list> 
		</property> 
	</bean> 
	 
	<bean id="fantasyDs" class="com.alipay.zdal.client.config.bean.AppDataSourceBean"> 
		<property name="appDataSourceName" value="fantasyDs" /> 
		<property name="dataBaseType" value="MYSQL" /> 
		<property name="configType" value="SHARD" /> 
		<property name="appRule" ref="fantasyRule"/> 
		<property name="physicalDataSourceSet"> 
			<set> 
				<ref bean="physics0"/> 
			</set> 
		</property> 
	</bean> 
 
	<bean id="physics0" class="com.alipay.zdal.client.config.bean.PhysicalDataSourceBean" > 
		<property name="name" value="master_0" /> 
		<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/fantasy" /> <property name="userName" value="root" /> <property name="password" value="" /> <property name="minConn" value="1" /> <property name="maxConn" value="10" /> <property name="blockingTimeoutMillis" value="180" /> <property name="idleTimeoutMinutes" value="180" /> <property name="preparedStatementCacheSize" value="100" /> <property name="queryTimeout" value="180" /> <property name="prefill" value="true"/> <property name="connectionProperties"> 
			<map> 
				<entry key="connectTimeout" value="500" /> 
				<entry key="autoReconnect" value="true" /> 
				<entry key="initialTimeout" value="1" /> 
				<entry key="maxReconnects" value="2" /> 
				<entry key="socketTimeout" value="5000" /> 
				<entry key="failOverReadOnly" value="false" /> 
			</map> 
		 </property> 
	</bean> 
 </beans>

另外一个重要的文件是fantasy-dev-rule.xml,主要用来描述分库或者分表字段是哪些,是SQL语句执行的依据:

<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" 
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	   xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd"> 
 
	<bean id="fantasyRule" class="com.alipay.zdal.rule.config.beans.AppRule"> 
		<property name="masterRule" ref="fantasyRWRule" /> 
		<property name="slaveRule" ref="fantasyRWRule" /> 
	</bean> 
	 
	<bean id="fantasyRWRule" class="com.alipay.zdal.rule.config.beans.ShardRule"> 
		<property name="tableRules"> 
			<map> 
				<entry key="user" value-ref="userTableRule" /> 
			</map> 
		</property> 
	</bean> 
	 
	<bean id="userTableRule" class="com.alipay.zdal.rule.config.beans.TableRule" 
		init-method="init"> 
		<property name="tbSuffix" value="resetForEachDB:[_0-_1]"/> 
		<property name="dbIndexes" value="master_0"/> 
		<property name="tbRuleArray"> 
			<list> 
				<value> 
					return com.yuanwhy.fantasy.rule.ShardRuleParser.parserTbIndex(#id#); 
				</value> 
			</list> 
		</property> 
	</bean> </beans>

这里的示例就是对user表分表,以用户的id为分表字段,解析的方法在 com.yuanwhy.fantasy.rule.ShardRuleParser.parserTbIndex 中,tbSuffix是resetForEachDB方式,即每个分库都有user_0、user_1表,只不过这里恰好只有一个分库;分表规则设计为id % 10的方式获取分表后缀,那么对于id为10的用户则被分配到user_0表(关于更多的分表规则的信息可以参考《zdal设计文档》)。

将fantasy项目运行之前先执行user-schema.sql初始化语句,通过访问 http://localhost:8080?id=10 运行起来后,可以从Debug信息中观察到原始的语句以及根据分表字段解析后获得的真实可执行的语句:

[DEBUG]  
[original sql]:SELECT id, name, age FROM user 
      WHERE id = ? 
[master_0.user_0]:SELECT id, name, age FROM user_0 
      WHERE id = ?

至此,一个传统的Java Web项目就具有了分库分表的能力(这里只演示了分表,分库的道理是一样的)。

发布评论

分享到:

IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

数据库(分库分表)中间件对比详解
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。