前面有过一篇文章介绍TinyDbRouter,但是当时没有开出来,主要原因是:1偶的粉丝数太少,期望到100的时候,纪念性的发布这个重量级框架,另外一个原因是当时有个编译问题没有完美的解决,偶担心同学们使用的时候不方便--其实偶也不方便,尤其是发布和测试的时候。
现在粉够100了,那个编译问题也顺利的解决了,OK,没有什么理由不快些把它开放给大家。
前面偶起的名字是TinyDBCluster,后来由于有同学们反应说这个与数据库集群歧义,因此还是改成TinyDBRouter了,如果看到两个名字,请把它们当成一样的,后面就专门用TinyDBRouter。
其实在开发TinyDbRouter之前,偶主要是想找一个比较合适的数据库分区、分表方案,为此也学习了各种实现方案,比如就了解过routing4db,偶也专门做了对比,当然由于对routing4db的了解毕竟有不足,因此可能有许多不准确的地方;另外也对淘宝系的tddl做了相关研究,但是最后偶还是决定自己尝试写一下,当然写完之后感觉还是不错的,因此才有现在开源的TinyDbRouter。
好的,上面是一些背景情况,现在言归正传,我们正式说框架。
关于Tiny DBRouter的原理性文章,请移步查阅,这里主要讲使用。
要想使用Tiny DBRouter,很简单,首先搞清楚是jdbc3(JDK1.5)还是jdbc4(JDK1.6及以上)的规范。
然后选择对应的Maven坐标:
<dependency>
<groupId>org.tinygroup</groupId>
<artifactId>org.tinygroup.dbrouterjdbc3</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
或者
<dependency>
<groupId>org.tinygroup</groupId>
<artifactId>org.tinygroup.dbrouterjdbc4</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
之所以是SNAPSHOT版本,是因为Tiny框架的升级是阶段性升级的,过一段时间就会变成0.0.13正式版本。
当把相关jar包下载到本地之后,接下来就是配置分区分表数据源了。
我们拿一个例子来说明:
differentSchemaAggregate.xml
<routers>
<router id="aggregate" user-name="luog" password="123456"
key-generator-class="org.tinygroup.dbrouter.impl.keygenerator.RouterKeyGeneratorLong">
<key-generator-config increment="1" step="100" data-source-id="ds0"/>
<data-source-configs>
<data-source-config id="ds0" driver="com.mysql.jdbc.Driver"
user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test0"
test-sql=""/>
<data-source-config id="ds1" driver="com.mysql.jdbc.Driver"
user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test1"
test-sql=""/>
<data-source-config id="ds2" driver="com.mysql.jdbc.Driver"
user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test2"
test-sql=""/>
</data-source-configs>
<partitions>
<partition id="abc" mode="2">
<partition-rules>
<partition-rule
class="org.tinygroup.dbrouter.impl.partionrule.PartionRuleByTableName"
table-name="score"/>
</partition-rules>
<shards>
<shard id="shard0" data-source-id="ds0">
<shard-rules>
<shard-rule
class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"
table-name="score" primary-key-field-name="id" remainder="0"/>
</shard-rules>
</shard>
<shard id="shard1" data-source-id="ds1">
<shard-rules>
<shard-rule
class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"
table-name="score" primary-key-field-name="id" remainder="1"/>
</shard-rules>
</shard>
<shard id="shard2" data-source-id="ds2">
<shard-rules>
<shard-rule
class="org.tinygroup.dbrouer.impl.shardrule.ShardRuleByIdDifferentSchema"
table-name="score" primary-key-field-name="id" remainder="2"/>
</shard-rules>
</shard>
</shards>
</partition>
</partitions>
</router>
</routers>
内容虽然比较长,但是其实很简单的,听偶娓娓道来:
一个配置文件可以配置多个数据库集群,因此根节点叫routers,接下来一段router就是一个集群喽。
<router id="aggregate" user-name="luog" password="123456"
key-generator-bean="routerKeyGeneratorLong">
id非常重要,在通过jdbc访问数据库集群的时候,在url中要用到id,用户名和密码就是在通过jdbc连接时的用户名密码,呵呵,现在密码是明码,后续版本密码部分,会改为加密存储。
采用逻辑主键时,经常需要生成一个主键,由于集群环境中,主键的生成是一个细致活,原来采用数据库的自动生成序列、自增长啥的都不好用了,因此,一定需要一个集群模式的主键生成器。不过不用担心,框架已经提供了整型、长整型、UUID三种分布式主键生成器,大多数的情况下都够用了,如果再不够,请给我们提需求或者自已动手丰衣足食,自行进行扩展。
<key-generator-config increment="1" step="100" data-source-id="ds0"/>
这里定义了数据主键生成的一些参数配置,首先,需要一个数据源的名称,因为有一些数据需要在数据库中存储。increment表示每次主键增长幅度,step表示每申请一次缓冲多个主键。当然,这两个参数都可以忽略,这时就采用系统默认值了--多数情况下都够了。
<data-source-configs>
<data-source-config id="ds0" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test0" test-sql=""/>
<data-source-config id="ds1" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test1" test-sql=""/>
<data-source-config id="ds2" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test2" test-sql=""/>
</data-source-configs>
这里定义的就是集群中要用到的数据源的列表,熟悉jdbc的同学一看就知道什么意思就不讲了,为什么这里统一一个区域定义数据源呢??因为如果是同库分表的话,数据源实际上就是一个,这个时候只用定义一个就够了。
接下来就是定义分区了:
一个集群可以包含多个分区,一个分区可以包含多个分片。
<partition id="abc" mode="2">
mode这里用于声明分区的模式,分区有两种方式,为1的时候表示读写分离模式,为2的时候表示分表模式。
<partition-rule class="org.tinygroup.dbrouter.impl.partionrule.PartionRuleByTableName" table-name="score"/>
</partition-rules>
一个分区可以包含多个分区规则,分区规则主要用于确定哪些表跑到一个分区。这里很简单,配置的是只要表名是score,就跑到本分区来执行。
一个区分又可以有多个分片,每个分片可以有一到多个分片规则,以决定是否到当前分片执行。
<shard id="shard0" data-source-id="ds0">
<shard-rules>
<shard-rule
class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"
table-name="score" primary-key-field-name="id" remainder="0"/>
</shard-rules>
</shard>
上面的规则是指根据score表的id值对shard数进行取余,余数为0的命中。 另外的两个就是说余数为1和为2的时候执行。
很明显分片规则和分区规则都是可以自行扩展的---凡是可以指定bean或类名的,都是可以进行扩展滴。
用白话总结一下,上面的配置:
定义了一个标识为“aggregate”的集群,其用户名密码为“luog”和"123456",定义的主键产生器是每次增加1,每次取100个,用完之后,再去取100个,以此类推。
定义了三个数据源,备用。
定义了一个分区abc,把所有score表的处理都交给此分区进行处理,它的分区模式是分表模式。也就是说score表中的数据会被分解到多个表当中去。
接下来给分区abc定义了三个分片,这三个分片分别指向上面的三个数据源中的一个,第一个负责处理socre表中的id对3取余余数为0的数据;第二个负责处理score表中的id对3取余余数为1的数据;第三个负责处理score表中的id对3取余余数为2的数据;
OK,上面的定义就算完成了,下面上大菜,看测试用例:
public class AggregateTest extends TestCase {
Statement stmt;
private static boolean hasInit;
@Override
protected void setUp() throws Exception {
super.setUp();
RouterManager routerManager = RouterManagerBeanFactory.getManager();
routerManager.addRouters("/differentSchemaAggregate.xml");
Class.forName("org.tinygroup.dbrouterjdbc3.jdbc.TinyDriver");
Connection conn = DriverManager.getConnection("jdbc:dbrouter://aggregate", "luog", "123456");
stmt = conn.createStatement();
prepareRecord();
}
private void prepareRecord() throws SQLException {
//删除数据
if (!hasInit) {
stmt.execute("delete from score");
stmt.executeUpdate("insert into score(id,name,score,course) values(1,'xiaohuihui',99,'shuxue')");
stmt.executeUpdate("insert into score(id,name,score,course) values(2,'xiaohuihui',97,'yuwen')");
stmt.executeUpdate("insert into score(id,name,score,course) values(3,'xiaom',95,'shuxue')");
stmt.executeUpdate("insert into score(id,name,score,course) values(4,'xiaof',97,'yingyu')");
stmt.executeUpdate("insert into score(id,name,score,course) values(5,'xiaom',100,'yuwen')");
stmt.executeUpdate("insert into score(id,name,score,course) values(6,'xiaof',95,'yuwen')");
stmt.executeUpdate("insert into score(id,name,score,course) values(7,'xiaohuihui',95,'yingyu')");
stmt.executeUpdate("insert into score(id,name,score,course) values(8,'xiaom',96,'yingyu')");
stmt.executeUpdate("insert into score(id,name,score,course) values(9,'xiaof',96,'shuxue')");
hasInit = true;
}
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
public void testCount() throws SQLException {
String sql = "select count(*),name from score group by name";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String name = resultSet.getString(2);
if (name.equals("xiaohuihui")) {
assertEquals(3, resultSet.getInt(1));
} else if (name.equals("xiaom")) {
assertEquals(3, resultSet.getInt(1));
} else if (name.equals("xiaof")) {
assertEquals(3, resultSet.getInt(1));
}
}
}
public void testMax() throws SQLException {
String sql = "select max(score) score,course from score group by course";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String course = resultSet.getString(2);
if (course.equals("shuxue")) {
assertEquals(99, resultSet.getInt(1));
} else if (course.equals("yingyu")) {
assertEquals(97, resultSet.getInt(1));
} else if (course.equals("yuwen")) {
assertEquals(100, resultSet.getInt(1));
}
}
}
public void testMaxSingle() throws SQLException {
String sql = "select max(score) score from score";
ResultSet resultSet = stmt.executeQuery(sql);
resultSet.next();
assertEquals(100, resultSet.getInt(1));
}
public void testSum() throws SQLException {
String sql = "select sum(score) score,name from score group by name";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String name = resultSet.getString(2);
if (name.equals("xiaohuihui")) {
assertEquals(291, resultSet.getInt(1));
} else if (name.equals("xiaom")) {
assertEquals(291, resultSet.getInt(1));
} else if (name.equals("xiaof")) {
assertEquals(288, resultSet.getInt(1));
}
}
}
public void testMin() throws SQLException {
String sql = "select min(score) score,name from score group by name";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String name = resultSet.getString(2);
if (name.equals("xiaohuihui")) {
assertEquals(95, resultSet.getInt(1));
} else if (name.equals("xiaom")) {
assertEquals(95, resultSet.getInt(1));
} else if (name.equals("xiaof")) {
assertEquals(95, resultSet.getInt(1));
}
}
}
public void testMinSingle() throws SQLException {
String sql = "select min(score) score from score";
ResultSet resultSet = stmt.executeQuery(sql);
resultSet.next();
assertEquals(95, resultSet.getInt(1));
}
public void testAvg() throws SQLException {
String sql = "select avg(score) score,name from score group by name";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String name = resultSet.getString(2);
if (name.equals("xiaohuihui")) {
assertEquals(97.0, resultSet.getDouble(1));
} else if (name.equals("xiaom")) {
assertEquals(97.0, resultSet.getDouble(1));
} else if (name.equals("xiaof")) {
assertEquals(96.0, resultSet.getDouble(1));
}
}
}
public void testMultiWithOrderby() throws SQLException {
String sql = "select min(score) minscore,max(score) maxscore,sum(score) sumscore,avg(score) avgscore, name from score group by name order by name";
ResultSet resultSet = stmt.executeQuery(sql);
while (resultSet.next()) {
String name = resultSet.getString("name");
if (name.equals("xiaohuihui")) {
assertEquals(95.0, resultSet.getDouble(1));
assertEquals(99.0, resultSet.getDouble(2));
assertEquals(291.0, resultSet.getDouble(3));
assertEquals(97.0, resultSet.getDouble(4));
} else if (name.equals("xiaom")) {
assertEquals(95.0, resultSet.getDouble(1));
assertEquals(100.0, resultSet.getDouble(2));
assertEquals(291.0, resultSet.getDouble(3));
assertEquals(97.0, resultSet.getDouble(4));
} else if (name.equals("xiaof")) {
assertEquals(95.0, resultSet.getDouble(1));
assertEquals(97.0, resultSet.getDouble(2));
assertEquals(288.0, resultSet.getDouble(3));
assertEquals(96.0, resultSet.getDouble(4));
}
}
}
public void testMultiSingle() throws SQLException {
String sql = "select min(score) minscore,max(score) maxscore,sum(score) sumscore,avg(score) avgscore from score";
ResultSet resultSet = stmt.executeQuery(sql);
resultSet.next();
assertEquals(95.0, resultSet.getDouble(1));
assertEquals(100.0, resultSet.getDouble(2));
assertEquals(870.0, resultSet.getDouble(3));
assertEquals(97.0, Math.ceil(resultSet.getDouble(4)));
}
public void testMaxWithFirstAndLast() throws SQLException {
String sql = "select max(score) score,name,course from score group by name order by score";
ResultSet resultSet = stmt.executeQuery(sql);
resultSet.absolute(1);
assertEquals(97, resultSet.getInt(1));
assertEquals("xiaof", resultSet.getString(2));
resultSet.first();
assertTrue(resultSet.isFirst());
assertEquals(97, resultSet.getInt(1));
assertEquals("xiaof", resultSet.getString(2));
resultSet.last();
assertTrue(resultSet.isLast());
assertEquals(100, resultSet.getInt(1));
assertEquals("xiaom", resultSet.getString(2));
}
public void testMaxWithOrderBy() throws SQLException {
String sql = "select max(score) score,course from score group by course order by score";
ResultSet resultSet = stmt.executeQuery(sql);
resultSet.next();
assertEquals("yingyu", resultSet.getString(2));
assertEquals(97, resultSet.getInt(1));
resultSet.next();
assertEquals("shuxue", resultSet.getString(2));
assertEquals(99, resultSet.getInt(1));
resultSet.next();
assertEquals("yuwen", resultSet.getString(2));
assertEquals(100, resultSet.getInt(1));
}
}
上面首先在setUp中做了一点初始化工作,主要就是下面两句:用于加载一个集群配置,实际使用有两种方法:
编程方式,约定方式。下面用的就是编译方式,如果用编写方式就简单了,只要按约定放在合适的位置,框架会自动加载配置文件,就可以不写下面的两行了。
RouterManager routerManager = RouterManagerBeanFactory.getManager();
routerManager.addRouters("/differentSchemaAggregate.xml");
其它的工作就与普通的JDBC没有任何不同了。
我们看看初始化之后, 数据的情况:
从上面可以看到,数据确实已经插入到三个数据表中。
后面的几个测试用例主要测试的是聚合统计方面的处理,实际上,所有的SQL语句都可以正常的执行,对于上层应用来说,它根本就不知道分表了。
急性子的同学们可能要问:
那如果我输入select * from score where id=3,结果会正确出来么?当然
那如果我输入select * from score order by id,结果会正确出来么?当然
我要说的,还远不止如此:
实际上TinyDBRouter已经竭尽全力,来支持数据库的特性:
比如:自增长
还是上面的score类子,如果在插入的时候不指定id值,如下:
insert into score(name,score,course) values('xiaohuihui',97,'yuwen')
TinyDBRouter会同样进行正常的插入,完全透明的处理好分布式主键的问题。这个与类似的框架比就先进许多了。类似的框架都是需要必须输入id,并且自己保证或必须调用其分库分表方案中提供的API来获取主键。这实际上就是有侵入性,也就是人编程人员可以感知到分库分表的存在,且必须按照相应规范进行使用。而使用TinyDBRouter,开发人员可以完全不知道有这么一层存在。
比如统计处理:假设在一个表中有9条数据,我们执行下面的语句:
select avg(score) score,name from score group by name
我们都知道实际处理是名字相同的score值加起来,然后除以记录数,得到平均值。
但是现在数据都分成3个表了,如果在3个表上执行同样的处理:
select avg(score) score,name from score group by name
数据库支持的普适性:
TinyDBRouter理论上支持各种数据库,各种ORMapping框架,而一般的框架是针对某种ORMapping框架做的,比如:专门针对iBatis,Hibernate的;有的只针对MySql或Oracle等。
SQL支持的普适性:TinyDBRouter理论上支持所有不违反TinyDBRouter适应规则的SQL。而许多同类框架则有诸多限制。
TinyDBRouter使用限制:
- 不支持跨分区关联查询
- 分表模式中只支持光标分页,不支持SQL分页
- 不支持savePoint
- 暂时不支持存储过程
总结
TinyDBRouter确实是非常优秀的分区分表方案,当然它也有缺点,那就是测试还不够充分,没有得到充分的验证。