TinyDbRouter开源喽~~~

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介:

前面有过一篇文章介绍TinyDbRouter,但是当时没有开出来,主要原因是:1偶的粉丝数太少,期望到100的时候,纪念性的发布这个重量级框架,另外一个原因是当时有个编译问题没有完美的解决,偶担心同学们使用的时候不方便--其实偶也不方便,尤其是发布和测试的时候。

现在粉够100了,那个编译问题也顺利的解决了,OK,没有什么理由不快些把它开放给大家。

前面偶起的名字是TinyDBCluster,后来由于有同学们反应说这个与数据库集群歧义,因此还是改成TinyDBRouter了,如果看到两个名字,请把它们当成一样的,后面就专门用TinyDBRouter

其实在开发TinyDbRouter之前,偶主要是想找一个比较合适的数据库分区、分表方案,为此也学习了各种实现方案,比如就了解过routing4db,偶也专门做了对比,当然由于对routing4db的了解毕竟有不足,因此可能有许多不准确的地方;另外也对淘宝系的tddl做了相关研究,但是最后偶还是决定自己尝试写一下,当然写完之后感觉还是不错的,因此才有现在开源的TinyDbRouter。

好的,上面是一些背景情况,现在言归正传,我们正式说框架。

关于Tiny DBRouter的原理性文章,请移步查阅,这里主要讲使用。

要想使用Tiny DBRouter,很简单,首先搞清楚是jdbc3(JDK1.5)还是jdbc4(JDK1.6及以上)的规范。

然后选择对应的Maven坐标:

?
1
2
3
4
5
< dependency >
     < groupId >org.tinygroup</ groupId >
     < artifactId >org.tinygroup.dbrouterjdbc3</ artifactId >
     < version >0.1.0-SNAPSHOT</ version >
</ dependency >

或者

?
1
2
3
4
5
< 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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
< 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就是一个集群喽。


?
1
2
< router id = "aggregate" user-name = "luog" password = "123456"
          key-generator-bean = "routerKeyGeneratorLong" >

id非常重要,在通过jdbc访问数据库集群的时候,在url中要用到id,用户名和密码就是在通过jdbc连接时的用户名密码,呵呵,现在密码是明码,后续版本密码部分,会改为加密存储。

采用逻辑主键时,经常需要生成一个主键,由于集群环境中,主键的生成是一个细致活,原来采用数据库的自动生成序列、自增长啥的都不好用了,因此,一定需要一个集群模式的主键生成器。不过不用担心,框架已经提供了整型、长整型、UUID三种分布式主键生成器,大多数的情况下都够用了,如果再不够,请给我们提需求或者自已动手丰衣足食,自行进行扩展。

?
1
< key-generator-config increment = "1" step = "100" data-source-id = "ds0" />

这里定义了数据主键生成的一些参数配置,首先,需要一个数据源的名称,因为有一些数据需要在数据库中存储。increment表示每次主键增长幅度,step表示每申请一次缓冲多个主键。当然,这两个参数都可以忽略,这时就采用系统默认值了--多数情况下都够了。

?
1
2
3
4
5
< 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的同学一看就知道什么意思就不讲了,为什么这里统一一个区域定义数据源呢??因为如果是同库分表的话,数据源实际上就是一个,这个时候只用定义一个就够了。

接下来就是定义分区了:

一个集群可以包含多个分区,一个分区可以包含多个分片。

?
1
< partition id = "abc" mode = "2" >
mode这里用于声明分区的模式,分区有两种方式,为1的时候表示读写分离模式,为2的时候表示分表模式。 
?
1
2
< partition-rule class = "org.tinygroup.dbrouter.impl.partionrule.PartionRuleByTableName" table-name = "score" />
</ partition-rules >

一个分区可以包含多个分区规则,分区规则主要用于确定哪些表跑到一个分区。这里很简单,配置的是只要表名是score,就跑到本分区来执行。

一个区分又可以有多个分片,每个分片可以有一到多个分片规则,以决定是否到当前分片执行。

?
1
2
3
4
5
6
7
< 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或类名的,都是可以进行扩展滴。

用白话总结一下,上面的配置:

?
1
2
3
4
5
6
7
定义了一个标识为“aggregate”的集群,其用户名密码为“luog”和 "123456" ,定义的主键产生器是每次增加1,每次取100个,用完之后,再去取100个,以此类推。
 
定义了三个数据源,备用。
 
定义了一个分区abc,把所有score表的处理都交给此分区进行处理,它的分区模式是分表模式。也就是说score表中的数据会被分解到多个表当中去。
 
接下来给分区abc定义了三个分片,这三个分片分别指向上面的三个数据源中的一个,第一个负责处理socre表中的id对3取余余数为0的数据;第二个负责处理score表中的id对3取余余数为1的数据;第三个负责处理score表中的id对3取余余数为2的数据;

OK,上面的定义就算完成了,下面上大菜,看测试用例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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 ));
    }
 
 
 
 
}
 
  
  
  
  
  <div>
 
 
  
  
  
  
  </div>

上面首先在setUp中做了一点初始化工作,主要就是下面两句:用于加载一个集群配置,实际使用有两种方法:

编程方式,约定方式。下面用的就是编译方式,如果用编写方式就简单了,只要按约定放在合适的位置,框架会自动加载配置文件,就可以不写下面的两行了。

?
1
2
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值,如下:

?
1
insert into score( name ,score,course) values ( 'xiaohuihui' ,97, 'yuwen' )

TinyDBRouter会同样进行正常的插入,完全透明的处理好分布式主键的问题。这个与类似的框架比就先进许多了。类似的框架都是需要必须输入id,并且自己保证或必须调用其分库分表方案中提供的API来获取主键。这实际上就是有侵入性,也就是人编程人员可以感知到分库分表的存在,且必须按照相应规范进行使用。而使用TinyDBRouter,开发人员可以完全不知道有这么一层存在。

比如统计处理:

假设在一个表中有9条数据,我们执行下面的语句:

?
1
select avg (score) score, name from score group by name

我们都知道实际处理是名字相同的score值加起来,然后除以记录数,得到平均值。

但是现在数据都分成3个表了,如果在3个表上执行同样的处理:

?
1
select avg (score) score, name from score group by name
然后再进行平均值计算,可不可以呢???答案是否定的。卖个关子这里不做解释,有不理解的同学,下面回帖发问。但是TinyDBRouter框架却可以保证结果的正确性。

数据库支持的普适性:

TinyDBRouter理论上支持各种数据库,各种ORMapping框架,而一般的框架是针对某种ORMapping框架做的,比如:专门针对iBatis,Hibernate的;有的只针对MySql或Oracle等。

SQL支持的普适性:

TinyDBRouter理论上支持所有不违反TinyDBRouter适应规则的SQL。而许多同类框架则有诸多限制。

TinyDBRouter使用限制:

  • 不支持跨分区关联查询
  • 分表模式中只支持光标分页,不支持SQL分页
  • 不支持savePoint
  • 暂时不支持存储过程

总结

TinyDBRouter确实是非常优秀的分区分表方案,当然它也有缺点,那就是测试还不够充分,没有得到充分的验证。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
JavaScript Linux 编译器
c++开源协程库libgo介绍及使用
c++开源协程库libgo介绍及使用
|
数据处理 开发工具
开源开源啦
开源开源啦
96 0
|
人工智能 自动驾驶
MiniGPT4,开源了!
MiniGPT4,开源了!
361 0
|
弹性计算 Dubbo Java
开源领域服务
开源领域服务
136 0
|
程序员
把自己开源了
用文字或视频记录自己的生活和想法,让我突然有种感觉,像是把自己给“开源”了。
182 0
|
Web App开发 测试技术 C#
C#开源汇总
原文:C#开源汇总 商业协作和项目管理平台-TeamLab 网络视频会议软件-VMukti 驰骋工作流程引擎-ccflow 【免费】正则表达式测试工具-Regex-Tester Windows-Phone-7-SDK Excel-读写组件-ExcelLibrary .
5438 0
|
大数据 云计算 Cloud Native