前面有过一篇文章介绍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使用限制:
总结
TinyDBRouter确实是非常优秀的分区分表方案,当然它也有缺点,那就是测试还不够充分,没有得到充分的验证。