目录

1 shark入门

本篇作为本文的开篇,笔者衷心希望能够用精炼的语句将shark的各个技术点尽可能的阐述清楚。同时为了保证你能够尽快上手shark,那么请仔细阅读用户指南的每一个章节。

1.1 dataBase架构演变史

对于刚上线的互联网项目来说,由于前期用户活跃度并不大,并发量相对较小,因此企业一般都会选择将所有数据信息存放在单库中进行读/写操作。随着用户活跃度的不断提升,单库逐渐力不从心,这时DBA就会将数据库设置为读写分离状态(一主一从/一主多从),Master负责写,Slave负责读。按照二八定律,80%的操作更多是读,那么剩下的20%则为写,读写分离后,大大提升了单库无法支撑的负载压力。不过光靠读写分离并不能一劳永逸,随着用户活跃度再次提升,必然会遇见读/写瓶颈,因此到了这个阶段,DBA就需要实现垂直分库。

所谓垂直分库,就是根据业务划分将原本冗余在单库中的业务表拆分,落到不同的业务库中,实现分而治之,并对外形成一个整体去提供读/写服务,以此提升数据库的执行性能。但是单一业务的数据信息仍然落盘在单表中,理论上Mysql单表超过500W行时,读操作就会成为瓶颈,哪怕重建索引,也无法解决数据暴增带来的检索效率低下等问题。这其实也是RDBMS等类型数据库的特点,相对于Nosql数据库而言,由于底层存储架构不同,所以自然无法相提并论。因此到了这个阶段,DBA就需要在垂直分库的基础上实施水平分片,也就是大家常说的sharding操作。

所谓水平分片,就是将原本冗余在单库中的单个业务表拆分为N个“逻辑相连”的子表(比如tab_0000、tab_0001、tab_0002...),不同的子表负责存储不同区间的数据,这种Sharding方式我们称之为“单库多片”模式。尽管数据库做了垂直,但是如果某一业务的读写操作特别频繁,那么数据库的处理能力会下降,因此为了提升数据库的并行处理能力,我们可以将单库也进行水平(比如db_0000、db_0001、db_0002...),业务子表按照特定的规则落在这些业务子库中,这种Sharding方式我们称之为“多库多片”模式。假设分库分表后,子表的数据量又达到阈值时,DBA只需要横向扩容即可。基于分库分表的数据库设计,目前在国内一些大型互联网企业中应用的非常普遍,比如:阿里巴巴、京东、云集微店等。

最后简单总结一下,分库分表主要是为了解决单库性能瓶颈,充分利用分布式+集群的威力提升数据库的读/写性能。关于互联网场景下常见的性能瓶颈:

1.2 sharding与cluster的区别

单纯从技术上来讲,Mysql Cluster仅仅只是一个数据库集群,其优势只是扩展了数据库的并行处理能力,但使用成本、维护成本相当高(目前使用者很少,实施也相对复杂)。而Sharding是一个成熟且实惠的方案,不仅可以解决数据库的并行处理能力,还能够解决单表数据过大的检索瓶颈。简单来说,前者是集群模式,而后者是分布式模式,因此无论从任何一个维度来看,Sharding无疑是当下互联网最好的选择。

1.3 shark简介

分布式mysql分库分表中间件,sharding领域的一站式解决方案。具备丰富、灵活的路由算法支持,能够方便DBA实现库的水平扩容和降低数据迁移成本。shark采用应用集成架构,放弃通用性,只为换取更好的执行性能与降低分布式环境下外围系统的宕机风险。目前shark每天为不同的企业、业务提供超过千万级别的sql读/写服务。

1.4 常见的sharding中间件对比

目前市面上的分库分表中间件并不多,并且大部分都是基于Proxy架构,在对于不看重通用性的应用场景下,早期基于应用集成架构的中间件比较成熟的则只有淘宝的TDDL,但TDDL并非是完美的,其弊端同样明显,比如:社区并不活跃、技术文档资料匮乏,再加上并不开源,因此注定了TDDL无法为欣赏它的非淘宝系用户服务。而Shark孕育而生的目的正是为了填补应用集成集成架构场景下的空白。目前常见的一些Shardig中间件产品对,如下所示:

功能 Cobar Mycat Heisenberg Shark TDDL Sharding-JDBC
是否开源 开源 开源 开源 开源 部分开源 开源
架构模型 Proxy架构 Proxy架构 Proxy架构 应用集成架构 应用集成架构 应用集成架构
数据库支持 MySQL 任意 任意 MySQL 任意 MySQL
外围依赖 Diamond
使用复杂度 一般 一般 一般 简单 复杂 一般
技术文档支持 较少 付费 较少 丰富 一般

基于Proxy架构的Sharding中间件,基本上都衍生自Cobar,并且这类产品对分片算法的支持都有限。具体使用什么样的sharding中间件产品,还需要根据具体的应用场景而定,当然如果你并不看重通用性,使用Shark是非常好的选择。

1.5 从maven中央仓库下载shark的构件

shark构件从1.3.8版本之后已经可以从maven中央仓库中进行下载,生产中建议使用shark1.4.0后的版本。

<dependency>
    <groupId>com.sharksharding</groupId>
    <artifactId>shark</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>

1.6 shark依赖的其它构件

shark项目的pom.xml文件中依赖的一些其它构件(比如依赖的Spring版本为3.2.13.RELEASE),如果你觉得版本过低,那么可在项目的pom.xml文件中使用maven的<exclusions/>标签来排除依赖,自行下载所需版本的构件。

1.6.1 版本注意

java version>=1.7.0
spring version>=3.2.13.RELEASE

1.7 下载并编译shark源码

shark的源码地址为:https://github.com/gaoxianglong/shark.git ,当成功下载好shark的源码后,可以使用如下命令进行编译:

mvn compile
mvn test-compile

1.8 shark的架构模型

数据路由任务无非就是根据Sharding算法对持有的多数据源进行动态切换,这是任何分库分表中间件的核心功能。使用Shark后,应用层将会持有N个数据源,Shark通过ShardKey进行运算,然后通过Route技术对数据库和数据表进行读/写操作。由于Shark内部并没有实现自己的DBConnectionPool,这就意味着,开发人员可以随意切换DBConnectionPool产品,如果你觉得C3P0没有BonePC性能高,那么你可以切换为BonePC,或者如果你觉得BonePC不够稳定,你又可以切换为druid。

对于开发人员而言,并不需要关心底层的数据库架构,业务逻辑中任何的CRUD操作,都像是在操作单个数据库、单个业务表一样,并且读写效率还不能够比之前低太多(几毫秒之内完成),而Shark就承担着这样一个任务。Shark所处的领域模型定位,如下所示:

Shark的领域模型介于持久层和JDBC之间,也就是位于数据路由层。Shark站在巨人的肩膀上,这个巨人正是Spring。简单来说,Shark重写了Spring的JdbcTemplate(2.0.0版本后直接支持Spring原生的JdbcTemplate,实现业务的零侵入),并使用AbstractRoutingDataSource作为动态数据源层。因此从另一个侧面反应出了Shark的源码注定是简单、轻量、易阅读、易维护的,因为Shark的核心只做分库分表。我们知道一般的Shading中间件,动不动就上千个类和几十万行代码,其中“猫腻”太多,不仅DBConnectionPool需要自己实现、动态数据源需要自己实现,再加上一些杂七杂八的功能,比如:通用性支持、多种类型的RDBMS或者Nosql支持,那么代码自然臃肿,可读性差。而阅读Shark的代码将会非常轻松和优雅。Shark的3层架构,如下所示:

既然Shark只考虑最核心的Sharding功能,同时也就意味着它的性能恒定指标还需要结合其它第三方产品,比如Shark的动态数据源层所使用的 DBConnectionPool可以为druid,也可以为BonePC。你别指望Shark还能为你处理边边角角的零碎琐事,想要什么效果,自行组合配置,这就是Shark,一个开源、分布式、轻量级的Mysql分库分表中间件。Shark的应用总体架构,如下所示:

1.9 shark的分布式路由算法

1.10 单表查询支持的sql模板

任何一条sql语句,只要携带路由条件,shark都支持,除了多表查询和子表查询外。目前一些大型互联网公司,比如淘宝、京东、云集微店的数据库方案均是单表sql查询,单表查询比多表联合查询性能慢不了多少,为了扩展性,牺牲一点性能(往返连接、资源消耗等)没什么大不了,so what?互联网场景下,利用单表sql换来的优势有3点:

1.11 shark使用过程中的一些注意事项

2 配置读写分离

Shark2.0.0版本后,对业务几乎是零侵入的,开发人员在业务代码中使用Spring提供的JdbcTemplate即可完成读写分离、Sharding等操作(不支持NamedParameterJdbcTemplate和SimpleJdbcTemplate等其他JDBC模板,因为封装层次越低,执行性能越好),但需要注意的是,一旦实施Sharding后,Shark是不支持任何的批量操作方法的。

Shark支持的一主一从模式,读写分离配置,如下所示:

    <aop:aspectj-autoproxy proxy-target-class="true" />
    <context:component-scan
        base-package="com.sharksharding.core">
        <context:include-filter type="annotation"
            expression="org.aspectj.lang.annotation.Aspect" />
    </context:component-scan>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSourceGroup" />
    </bean>
    <!-- 读写分离配置 -->
    <bean id="shardRule" class="com.sharksharding.core.shard.ShardRule"
        init-method="init">
        <property name="isShard" value="false" />
        <property name="wr_index" value="r1w0" />
    </bean>
    <bean id="dataSourceGroup" class="com.sharksharding.core.shard.SharkDatasourceGroup">
        <property name="targetDataSources">
            <map key-type="java.lang.Integer">
                <entry key="0" value-ref="dataSource1" />
                <entry key="1" value-ref="dataSource2" />
            </map>
        </property>
    </bean>
<!-- 省略数据源配置 -->
<bean></bean>

上述程序实例中,SharkDatasourceGroup就是一个用于管理多数据源的Group,继承自org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,它充当了shark动态数据源层的角色,由此基础之上实现Route。

在ShardRule中,属性isShard定义了Sharding开关,缺省为false,也就意味着缺省是没有开启分库分表的,那么在不Sharding的情况下,我们依然可以使用shark来完成读写分离操作。wr_index属性定义了读写分离的起始索引,也就是说,有多少个master,就一定需要有等量的slave,比如:master有1个,slave也应该是1个,因此SharkDatasourceGroup中持有的数据源总个数就应该一共是2个,索引从0-1,如果主库的索引为0,那么从库的索引就应该为1,也就是“r1w0”。当配置完成后,一旦shark监测到程序中所执行的sql为写操作时,就会自动切换为master的数据源,反之切换为slave的数据源。

2.0.0版本之前,com.sharksharding.core.shard.SharkJdbcTemplate是Shark提供jdbc模板,继承自org.springframework.jdbc.core.JdbcTemplate。读写分离的配置信息,如下所示:

<!-- 读写分离配置 -->
<bean id="jdbcTemplate" class="com.sharksharding.core.shard.SharkJdbcTemplate"
    init-method="init">
    <property name="isShard" value="false" />
    <property name="dataSource" ref="dataSourceGroup" />
    <property name="wr_index" value="r1w0" />
</bean>

当配置成功后,程序中便可以对SharkJdbcTemplate自动装配,如下所示:

@Resource
private SharkJdbcTemplate jdbcTemplate;

shark的启动日志:

2.1 sql编写注意事项

sql语句的第一个参数必须是shard key。耦合在业务代码中的sql语句,如下所示:

insert into tab(c1,c2) values(?,?),

上述写法shark是不支持的,约定写法,如下所示:

insert into tab(c1,c2) values("+ c1 +",?)

也就是说,第一个参数不允许是占位符,而必须是实际参数。在某些情况下,我们可能不太希望将sql耦合在我们的业务代码中,这种情况下,shark提供有类似于mybatis的做法,将sql定义在配置文件中。详情请见3.1小结。

2.2 sql别名支持

在shark1.4.1版本之前,sql语句中是不允许带别名的,因为别名的应用场景更多是集中在多表查询中,因此shark早期版本并不打算支持别名,但是shark1.4.1及后续版本中,将正式开始支持,如下所示:

#带别名
select * from tab t where t.c1=? and t.c2=?
#不带别名
select * from tab where c1=? and c2=?

3 配置sharding操作

如果你觉得使用shark配置读写分离后并不能够满足场景需要,那么你可以使用本节的sharding配置。

3.1 数据库sharding后带来的影响

3.2 配置sql文件与逻辑代码结构

在某些情况下,我们不太希望将sql语句耦合在业务逻辑代码中,因为这样不利于维护。因此shark提供有类似于mybatis的做法,将sql定义在配置文件中以此达到降低耦合的目的。shark将sql文件定义在properties文件中,采用key-value的方式,key建议定义为持久层的方法名称,value为具体的sql语句。sql写法,如下所示:

addTab=insert into tab(c1,c2) values(?,?)

sql文件的配置方式,如下所示:

<bean class="com.sharksharding.sql.PropertyPlaceholderConfigurer">
    <constructor-arg name="path"
        value="classpath:properties/sql.properties" />
</bean>

除了允许定义加载classpath下的sql配置文件外,shrak还允许加载文件路径下的sql配置文件。使用方式如下所示:

@Resource
private SharkJdbcTemplate jdbcTemplate;
@Resource
private PropertyPlaceholderConfigurer property;

@Override
public void addTab(long shardKey) {
    final String SQL = property.getSql("addTab", shardKey);
    jdbcTemplate.update(SQL);
}

上述程序示例中,PropertyPlaceholderConfigurer的getSql()方法的第一个参数就是定义在properties中的key,而shardKey就是路由条件。

3.3 单库多表模式

shark支持2种分库分表模式,即单库多表和多库多表。单库分表模式的算法为:

tbIndex=“key % tbSize”

配置信息,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true" />
<context:component-scan
    base-package="com.sharksharding.core,com.test.sharksharding.use">
    <context:include-filter type="annotation"
        expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSourceGroup" />
</bean>
<!-- 单库多片模式 -->
<bean id="shardRule" class="com.sharksharding.core.shard.ShardRule"
    init-method="init">
    <property name="isShard" value="true" />
    <property name="wr_index" value="r1w0" />
    <property name="shardMode" value="false" />
    <property name="tbRuleArray" value="#key1|key2# % 1024" />
    <property name="tbSuffix" value="_0000" />
</bean>
<bean id="dataSourceGroup" class="com.sharksharding.core.shard.SharkDatasourceGroup">
    <property name="targetDataSources">
        <map key-type="java.lang.Integer">
            <entry key="0" value-ref="dataSource1" />
            <entry key="1" value-ref="dataSource2" />
        </map>
    </property>
</bean>
<!-- 省略数据源配置 -->
<bean></bean>

3.4 多库多表模式

shark支持2种分库分表模式,即单库多表和多库多表。多库分表模式的算法为:

dbIndex=“key % tbSize / dbSize” 
tbIndex=“key % tbSize % dbSize”

配置信息,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true" />
<context:component-scan
    base-package="com.sharksharding.core,com.test.sharksharding.use">
    <context:include-filter type="annotation"
        expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSourceGroup" />
</bean>
<!-- 多库多片模式 -->
<bean id="shardRule" class="com.sharksharding.core.shard.ShardRule"
    init-method="init">
    <property name="isShard" value="true" />
    <property name="wr_index" value="r32w0" />
    <property name="shardMode" value="true" />
    <property name="consistent" value="true" />
    <property name="dbRuleArray" value="#key1|key2# % 1024 / 32" />
    <property name="tbRuleArray" value="#key1|key2# % 1024 % 32" />
    <property name="tbSuffix" value="_0000" />
</bean>
<bean id="dataSourceGroup" class="com.sharksharding.core.shard.SharkDatasourceGroup">
    <property name="targetDataSources">
        <map key-type="java.lang.Integer">
<!-- 省略数据源引用 -->
            <entry key="0" value-ref="dataSource1" />
            <entry key="..." value-ref="dataSource..." />
            <entry key="1023" value-ref="dataSource1024" />
        </map>
    </property>
</bean>
<!-- 省略数据源配置 -->
<bean></bean>

上述程序示例中,主库一共是32个(1024个子表,每个库包含子表数为32个),那么自然从库也就是32个,在SharkDatasourceGroup中一共会持有64个数据源,数据源索引为0-63。

在ShardRule中,属性isShard定义了sharding开关,true为开启。属性wr_index定义了读写分离的起始索引,这也就意味着0-31都是master,而32-63都是slave,一旦shark监测到程序中所执行的sql为写操作时,就会自动切换为master的数据源,反之切换为slave的数据源(当然如果不希望配置slave,那么属性wr_index应该定义为r0w0)。属性shardMode定义了需要使用shark的哪一种分库分表模式,true为多库多表模式,而false则为单库多表模式。属性consistent定义了片名是否连续,true为连续,false为非连续。属性dbRuleArray和tbRuleArray定义了分库和分表规则。tbSuffix属性指明了子库的后缀。

如果sql语句中的第一个参数并不是shardkey时,shark将会抛出下述异常信息:

com.sharksharding.exception.SqlParserException: can not find shardkey

3.5 自动生成数据源文件

为了避免手工配置出现错误的情况,shark提供有自动生成sharding配置文件和数据源配置文件的功能(支持druid和c3p0)。

3.5.1 自动生成sharding配置文件

假设采用片名连续的库内分片模式,32个库1024个表,使用com.sharksharding.util.xml.CreateCoreXml进行生成,如下所示:

public @Test void testCreateCoreXml() {
    CreateCoreXml c_xml = new CreateCoreXml();
    /* 是否控制台输出生成的配置文件 */
    c_xml.setIsShow(true);
    c_xml.setDbSize("64");
    c_xml.setShard("true");
    c_xml.setWr_index("r0w32");
    c_xml.setShardMode("false");
    c_xml.setConsistent("true");
    c_xml.setDbRuleArray("#uid|email_hash# % 1024 / 32");
    c_xml.setTbRuleArray("#uid|email_hash# % 1024 % 32");
    c_xml.setSqlPath("classpath:properties/sqlFile.properties");
    /* 执行配置文件输出 */
    Assert.assertTrue(c_xml.createCoreXml(new File("c:/shark-context.xml")));
}

3.5.2 自动生成c3p0数据源文件

使用com.sharksharding.util.xml.CreateCoreXml进行生成,如下所示:

/**
 * 生成c3p0数据源配置文件
 * 
 * @author gaoxianglong
 */
public @Test void testCreateC3p0Xml() {
    CreateC3p0Xml c_xml = new CreateC3p0Xml();
    /* 是否控制台输出生成的配置文件 */
    c_xml.setIsShow(true);
    c_xml.setTbSuffix("_0000");
    /* 数据源索引起始 */
    c_xml.setDataSourceIndex(1);
    /* 配置分库分片信息 */
    c_xml.setDbSize("16");
    /* 配置数据源信息 */
    c_xml.setJdbcUrl("jdbc:mysql://ip1:3306/db");
    c_xml.setUser("${name}");
    c_xml.setPassword("${password}");
    c_xml.setDriverClass("${driverClass}");
    c_xml.setInitialPoolSize("${initialPoolSize}");
    c_xml.setMinPoolSize("${minPoolSize}");
    c_xml.setMaxPoolSize("${maxPoolSize}");
    c_xml.setMaxStatements("${maxStatements}");
    c_xml.setMaxIdleTime("${maxIdleTime}");
    /* 执行配置文件输出 */
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/dataSource-context.xml")));
}
/**
 * 生成c3p0的master/slave数据源配置文件
 * 
 * @author gaoxianglong
 */
public @Test void testCreateC3p0MSXml() {
    CreateC3p0Xml c_xml = new CreateC3p0Xml();
    c_xml.setIsShow(true);
    c_xml.setTbSuffix("_0000");
    /* 生成master数据源信息 */
    c_xml.setDataSourceIndex(1);
    c_xml.setDbSize("16");
    c_xml.setJdbcUrl("jdbc:mysql://ip1:3306/db");
    c_xml.setUser("${name}");
    c_xml.setPassword("${password}");
    c_xml.setDriverClass("${driverClass}");
    c_xml.setInitialPoolSize("${initialPoolSize}");
    c_xml.setMinPoolSize("${minPoolSize}");
    c_xml.setMaxPoolSize("${maxPoolSize}");
    c_xml.setMaxStatements("${maxStatements}");
    c_xml.setMaxIdleTime("${maxIdleTime}");
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/masterDataSource-context.xml")));
    /* 生成slave数据源信息 */
    c_xml.setDataSourceIndex(17);
    c_xml.setDbSize("16");
    c_xml.setJdbcUrl("jdbc:mysql://ip2:3306/db");
    c_xml.setUser("${name}");
    c_xml.setPassword("${password}");
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/slaveDataSource-context.xml")));
}

3.5.3 自动生成druid数据源文件

使用com.sharksharding.util.xml.CreateCoreXml进行生成,如下所示:

/**
 * 生成druid数据源配置文件
 * 
 * @author gaoxianglong
 */
public @Test void testCreateDruidXml() {
    CreateDruidXml c_xml = new CreateDruidXml();
    /* 是否控制台输出生成的配置文件 */
    c_xml.setIsShow(true);
    /* 数据源索引起始 */
    c_xml.setDataSourceIndex(1);
    /* 配置分库分片信息 */
    c_xml.setDbSize("16");
    /* false为懒加载模式,反之启动时开始初始化数据源 */
    c_xml.setInit_method(true);
    c_xml.setTbSuffix("_0000");
    /* 生成数据源信息 */
    c_xml.setUsername("${username}");
    c_xml.setPassword("${password}");
    c_xml.setUrl("jdbc:mysql://ip1:3306/db");
    c_xml.setInitialSize("${initialSize}");
    c_xml.setMinIdle("${minIdle}");
    c_xml.setMaxActive("${maxActive}");
    c_xml.setPoolPreparedStatements("${poolPreparedStatements}");
    c_xml.setMaxOpenPreparedStatements("${maxOpenPreparedStatements}");
    c_xml.setTestOnBorrow("${testOnBorrow}");
    c_xml.setTestOnReturn("${testOnReturn}");
    c_xml.setTestWhileIdle("${testWhileIdle}");
    c_xml.setFilters("${filters}");
    c_xml.setConnectionProperties("${connectionProperties}");
    c_xml.setUseGlobalDataSourceStat("${useGlobalDataSourceStat}");
    c_xml.setTimeBetweenLogStatsMillis("${timeBetweenLogStatsMillis}");
    /* 执行配置文件输出 */
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/dataSource-context.xml")));
}
/**
 * 生成druid数据源配置文件
 * 
 * @author gaoxianglong
 */
public @Test void testCreateDruidMSXml() {
    CreateDruidXml c_xml = new CreateDruidXml();
    /* 是否控制台输出生成的配置文件 */
    c_xml.setIsShow(true);
    /* 数据源索引起始 */
    c_xml.setDataSourceIndex(1);
    /* 配置分库分片信息 */
    c_xml.setDbSize("16");
    /* false为懒加载模式,反之启动时开始初始化数据源 */
    c_xml.setInit_method(true);
    c_xml.setTbSuffix("_0000");
    /* 生成master数据源信息 */
    c_xml.setUsername("${username}");
    c_xml.setPassword("${password}");
    c_xml.setUrl("jdbc:mysql://ip1:3306/db");
    c_xml.setInitialSize("${initialSize}");
    c_xml.setMinIdle("${minIdle}");
    c_xml.setMaxActive("${maxActive}");
    c_xml.setPoolPreparedStatements("${poolPreparedStatements}");
    c_xml.setMaxOpenPreparedStatements("${maxOpenPreparedStatements}");
    c_xml.setTestOnBorrow("${testOnBorrow}");
    c_xml.setTestOnReturn("${testOnReturn}");
    c_xml.setTestWhileIdle("${testWhileIdle}");
    c_xml.setFilters("${filters}");
    c_xml.setConnectionProperties("${connectionProperties}");
    c_xml.setUseGlobalDataSourceStat("${useGlobalDataSourceStat}");
    c_xml.setTimeBetweenLogStatsMillis("${timeBetweenLogStatsMillis}");
    /* 执行配置文件输出 */
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/masterDataSource-context.xml")));
    /* 生成slave数据源信息 */
    c_xml.setDataSourceIndex(17);
    c_xml.setDbSize("16");
    c_xml.setUsername("${username}");
    c_xml.setPassword("${password}");
    c_xml.setUrl("jdbc:mysql://ip2:3306/db");
    /* 执行配置文件输出 */
    Assert.assertTrue(c_xml.createDatasourceXml(new File("e:/slaveDataSource-context.xml")));
}

3.6 多机sequenceid解决方案

一旦数据库sharding后,原本单库单表中使用的序列自增Id将无法再继续使用,那么我们只能够使用自定义的解决方案,当然必须围绕下述2个方面来进行,如下所示:

shark的解决方案是为每一个应用都集成一个id生成器,然后通过一个全局的存储介质去保证生成id的唯一性和连续性。

3.6.1 基于mysql生成全局唯一sequenceid

每一个应用都集成有一个id生成器,统一指定一个数据库作为id的存储介质,数据库中主要负责存储当前id序列的最大值,每次每个id生成器从数据库中拿去id时,都需要锁表。或许你会有一个疑问,是否存在性能问题?如果是每一个id都从数据库去拿必然存在性能问题,因此shark的id生成器一次会从数据库中取出一段id,然后缓存在本地,这样就不用每次都去数据库中取了,但是如果id生成器取了一段id后,突然宕机了,那么这一组id将废弃。其次既然依赖数据库作为存储介质,那么其架构必须是HA,确保高可用。多机sequenceid数据库表结构,如下所示:

#sequenceid的sql
CREATE TABLE shark_sequenceid(
    s_id INT NOT NULL AUTO_INCREMENT COMMENT '主键',
    s_type INT NOT NULL COMMENT '类型',
    s_useData BIGINT NOT NULL COMMENT '申请占位数量',
    PRIMARY KEY (s_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE utf8mb4_bin;

使用SequenceIDManger生成sequenceid,如下所示:

SequenceIDManger.init(NAME, PWD, URL, DRIVER);
long sequenceid = SequenceIDManger.getSequenceId(100, 10, 5000);
logger.info("sequenceid-->" + sequenceid);

基于mysql生成全局唯一sequenceid的数字长度为19位,其最大值不可超过二进制位数64位long类型的最大值9223372036854775807。在getSequenceId()方法中,第一个参数为IDC机房编码, 用于区分不同的IDC机房,3位数字长度。第二个参数为业务类别,2位数字长度,最后一个参数为内存占位数。

3.6.2 基于zookeeper生成全局唯一sequenceid

和基于mysql生成全局唯一sequenceid类似,shark还提供有另外一种形式,那就是基于zookeeper。使用SequenceIDManger生成sequenceid,如下所示:

SequenceIDManger.init(ADDRESS, TIMEOUT);
long sequenceid = SequenceIDManger.getSequenceId(PATH, 100, 100000);
logger.info("sequenceid-->" + sequenceid);

id的唯一性由zookeeper保证,每次对znode下的数据进行更新时都会产品一个版本号,因此基于zookeeper生成全局唯一sequenceid的数字长度为19位,其最大值不可超过二进制位数64位long类型的最大值9223372036854775807。在getSequenceId()方法中,第一个参数为znode根目录,第二个参数为IDC机房编码, 用于区分不同的IDC机房,3位数字长度。第二个参数为业务类别,6位数字长度。

同3.6.1小节一样,既然依赖zookeeper作为存储介质,那么其架构必须是HA,确保高可用。

3.7 事物功能矩阵

4 使用配置中心

如果项目采用分布式架构进行部署后,配置文件始终是个大问题。开发人员往往希望当配置发生改变后,可以不重新启动程序便可动态加载。目前shark支持基于zookeeper、redis3.x cluster作为集中式资源配置中心,一旦相关配置发生改变后,shark所持有的信息确保将是最新的,相对于本地配置文件,集中式资源配置中心将会大大降低维护成本和部署成本。

4.1 基于zookeeper的资源配置中心

一旦使用资源配置中心,那么程序中将不再需要将sharding信息和数据源信息配置在本地配置文件中,而是配置在资源配置中心。配置信息,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true" />
<context:component-scan
    base-package="com.sharksharding">
    <context:include-filter type="annotation"
        expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>
<bean class="com.sharksharding.resources.register.bean.RegisterDataSource"/>
<bean class="com.sharksharding.resources.conn.ZookeeperConnectionManager"
    init-method="init">
    <constructor-arg index="0" value="${address}" />
    <constructor-arg index="1" value="${session.timeout}" />
    <constructor-arg index="2" value="${nodepath}" />
</bean>

上述程序示例中,RegisterDataSource会动态向spring的ioc容器中注册bean实例。而ZookeeperConnectionManager中,我们需要配置一系列zookeeper相关的信息,其中包括资源文件所在的nodepath。

如果使用配置中心,那么建议在客户端的log4j.xml配置文件中关闭如下日志输出:

<!-- 避免druid抛出javax.management.InstanceAlreadyExistsException异常 -->
<logger name="com.alibaba.druid.stat.DruidDataSourceStatManager">
    <level value="off" />
</logger>

4.2 基于redis3 cluster的资源配置中心

shark除了支持基于zookeeper作为资源配置中心外,同时也支持基于redis3 cluster作为资源配置中心。配置信息,如下所示:

<task:annotation-driven />
<aop:aspectj-autoproxy proxy-target-class="true" />
<context:component-scan base-package="com.sharksharding">
    <context:include-filter type="annotation"
        expression="org.aspectj.lang.annotation.Aspect" />
</context:component-scan>
<bean class="com.sharksharding.resources.register.bean.RegisterDataSource" />
<bean id="redisWatcher" class="com.sharksharding.resources.watcher.RedisWatcher" />
<bean class="com.sharksharding.resources.conn.RedisConnectionManager"
    init-method="init">
    <constructor-arg index="0" value="${redis.key}" />
    <constructor-arg index="1" ref="jedisCluster" />
    <!-- 0使用版本号比对,1使用MD5校验,缺省为0 -->
    <constructor-arg index="2" value="1" />
    <property name="redisWatcher" ref="redisWatcher" />
</bean>
<!-- 省略redis配置信息 -->
</bean>

上述程序示例中,RegisterDataSource会动态向spring的ioc容器中注册bean实例。而RedisConnectionManager中,我们需要配置一系列redis相关的信息。

4.2.1 使用版本号比对资源差异

如果RedisConnectionManager中使用版本号比对资源差异,那么RedisWatcher每隔10s会自动向配置中心获取一次资源数据。redis的数据结构为key-value(version%@%resource),当配置中心发生改变时,需要修改当前version的值,shark的配置中心客户端会根据版本差异判断是否是更新了配置中心的数据。

4.2.2 使用md5码比对资源差异

如果RedisConnectionManager中使用md5码比对资源差异,那么RedisWatcher每隔10s会自动向配置中心获取一次资源数据。如果配置中心的配置信息的md5码与shark的配置中心客户端持有的md5码不一致,则认为配置中心发生了改变。

4.3 配置中心使用注意

使用配置中心后,不能继续使用Spring自动装配后的JdbcTemplate,因为当配置中心发生改变后,应用持有的仍然是之前引用,因此必须通过GetJdbcTemplate.getJdbcTemplate()方法获取JdbcTemplate实例,如下所示:

/* 获取JdbcTemplate实例 */
JdbcTemplate jdbcTemlate = GetJdbcTemplate.getJdbcTemplate();

5 运维监控

在进行sharding后,shark提供有一个sql验证页面,能够方便开发、测试、运维人员对落盘后的数据信息进行验证。

5.1 配置shark内置验证页面

内置验证页面(QueryViewServlet)是一个标准的javax.servlet.http.HttpServlet,需要配置在你web应用中的WEB-INF/web.xml中,如下所示:

<servlet>
    <servlet-name>queryViewServlet</servlet-name>
    <servlet-class>com.sharksharding.util.web.http.QueryViewServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>queryViewServlet</servlet-name>
    <url-pattern>/shark/*</url-pattern>
</servlet-mapping>

根据web.xml配置中的url-pattern来访问内置验证页面,如果是上面的配置,内置验证页面的首页是/shark/index.html。内置验证页面如下所示:

考虑到测试环境、生产环境的安全性,内置验证页面缺省是只支持query操作,而其他写入操作将不支持。

6 HA方案

shark缺省并没有提供HA机制,但是可以在生产或测试环境中,使用Keepalived、MHA、配置中心等方式来实现高可用。如果业务场景不允许在主从切换过程中出现少量数据的丢失,那么建议使用MHA等方式。

6.1 基于资源配置中心实现HA

当master的实例挂了以后,立即修改配置中心master的ip即可。

6.2 基于keepalive实现HA

如果使用Keepalived实现HA,那么在shark的配置中需要将master绑定VIP(VirtualIP,虚拟IP)地址,而slave绑定真实的物理IP地址即可。当master的实例挂了以后,Keepalived可以实现自动将ip飘到Slave上。

6.3 基于MHA实现HA

基于MHA实现HA,理论上是不会丢失数据信息的,而6.1和6.2小节的HA方案都会丢失数据,因此如果不希望生产丢失数据,建议使用MHA实现HA。

7 增强Spring Jdbc

shark之所以会在springjdbc的现有功能上进行增强,主要是为了提升开发人员的编码效率,给予最大程度上得实惠,增强功能在shark1.4.1及后续版本开始提供支持。

7.1 mapper自动映射

相信大家使用springjdbc时都曾遇见过一个烦恼,业务在进行查询操作的时候,便会接触到org.springframework.jdbc.core.RowMapper,RowMapper会将数据库检索出的数据信息自动映射到对应的entitybean的字段中,但是如果检索的字段较多时,重写mapRow()方法的时候将会是一件体力活,我们需要将ResultSet中的数据逐个set一遍,因此使用shark的mapper自动映射后,将不再需要手动set。

shark的com.sharksharding.util.mapper包下提供有3个annotation,如下所示:

@Mapper
@Column
@AutoColumn

注解@Mapper作用于类型,只有标记了该注解的类型才能实现mapper自动映射。而@Column作用于字段和方法,用于标记需要自动映射的目标对象的字段。那么在entitybean上加上@Mapper@Column注解后,重写mapRow()方法时将不再需要将ResultSet中的数据逐个set一遍。注解使用,如下所示:

@Mapper
public class UserInfo {
    @Column
    private long uid;
    @Column
    private String userName;
    /* 此处省略set和get方法 */
}

或许大家会有一个疑问,如果被标记的字段名称与数据库字段名称不一致时怎么办?这其实是一个非常常见的场景,往往entitybean的字段命名规则都是与业务相关的,因此自然不会与数据库的表字段名一致,因此使用@Column注解标记entitybean的字段时,可以使用@Column注解的那么属性,给目标字段定义备用名称,如下所示:

@Mapper
public class UserInfo {
    @Column(name="uid")
    private long id;
    @Column(name="userName")
    private String name;
    /* 此处省略set和get方法 */
}

当在entitybean上标记好@Mapper@Column注解后,我们还需要在重写mapRow()方法的时候显式指定使用shark提供的mapper自动映射功能,如下所示:

public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
    UserInfo user = new UserInfo();
    /* 使用shark的mapper自动映射 */
    BeanMapper.mapper(user, rs);
    return user;
}

@AutoColumn和@Column注解类似,不过@AutoColumn作用于类型并非字段和方法,当在entitybean上标记@AutoColumn后,entitybean中所有的字段缺省都会自动映射,除非显式设置@AutoColumn的value属性为false。如下所示:

@Mapper
@AutoColumn
public class UserInfo {
    private long uid;
    private String userName;
    /* 此处省略set和get方法 */
}

7.1.1 @AutoColumn和@Column的执行优先级

这里存在一个和之前一样的应用场景,当使用@AutoColumn后,如果被标记的字段名称与数据库字段名称不一致时怎么办?其实@AutoColumn和@Column这2个注解是并存的,意味着可以同时使用,只不过@Column的执行优先级会高于@AutoColumn,也就是说,如果entitybean的字段上标记有@Column,将不执行被@AutoColumn标记的字段。如下所示:

@Mapper
@AutoColumn
public class UserInfo {
    @Column(name="uid")
    private long id;
    private String userName;
    /* 此处省略set和get方法 */
}

7.2 sql语句文件独立配置

shark支持sql语句硬编码在业务代码中,同时也支持将sql语句独立配置在sql.xml或者sql.properties文件中。由于spring并没有提供类似mybaties那样强大的动态sql拼接,所以在使用springjdbc时难免存在一些不便的情况,因此shark弥补了这块空白。

配置SQLTemplate,如下所示:

<bean id="sqlTemplate" class="com.sharksharding.sql.SQLTemplate">
    <constructor-arg name="path" value="classpath:properties/sql.xml" />
</bean>

sql.xml配置文件可以在classpath下,shark同时也支持文件路径。使用SQLTemplate,如下所示:

@Resource
private SQLTemplate sqlTemplate;
@Override
public List<TabInfo> getTab(Map<String, Object> params) throws Exception {
    final String sql = sqlTemplate.getSql("getTab", params);
    return jdbcTemplate.query(sql, tabMapper);
}

通过SQLTemplate.getSql()方法即可成功从sql.xml文件中获取指定的sql语句。其中第一个参数为就是定义在sql.xml文件中的sql key,而params就是结果集。

7.2.1 动态sql拼接

先来了解下如何配置sql.xml,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<sqls>
    <sql name="setTab">
        <![CDATA[
            INSERT INTO tab(c1,c2) VALUES(${c1},'${c2}')
        ]]>
    </sql>
    <sql name="getTab">
        <![CDATA[
            SELECT * FROM tab WHERE c1=${c1} AND c2='${c2}'
        ]]>
    </sql>
</sqls>

根标签为<sqls/>,每一个sql语句都包含有一个独立的<sql>标签,其中属性name定义了sql key,不允许出现重复。由于shark是基于velocity模板引擎渲染内容,因此自然也支持velocity语法实现sql的动态拼接,如下所示:

<sql name="getTab">
    <![CDATA[
        SELECT * FROM Tab 
        #if(${c1})
            WHERE c1 = ${c1} 
        #end
        #if(${c2})
            AND c2 = '${c2}'
        #end
    ]]>
</sql>

上述是一个典型的根据字段条件实现sql语句的动态拼接示例。由于velocity的语法还支持foreach、逻辑运算等功能,但并不会在shark用户手册里进行讲解,感兴趣的同学可以自行查阅。