一直在作企业应用,目前要作一些互联网应用,固然只是应用是放在互联网的,数据量距离真正的互联网应用仍是有至关大的差距的。可是不可避免的,在数据库出现瓶颈的状况仍是有的,如今作互联网上的应用,固然也要未雨绸缪,要考虑数据量大的时候的解决方案。 java
这个目前开源的商用的也都有很多解决方案,一来,作技术的都有这么个臭毛病,即便是使用别人的方案,本身也要搞清楚内部的一些实现机制,这样才会有真正的体会,不然去评估一个方案的时候,就只能盲人摸象了。 sql
为此,构建一个验证型的分布式数据库框架,来解决数据库的垂直与水平扩展方面的问题,因为是验证性开发,因此,思考不完善的地方确定存在,欢迎批评指正。 数据库
海量数据的存储及访问,经过对数据库进行读写分离,来提高数据的处理能力。读写分离它的方案特色是数据库产生多个副本,数据库的写操做都集中到一个数据库上,而一些读的操做呢,能够分解到其它数据库上。这样,只要付出数据复制的成本,就可使得数据库的处理压力分解到多个数据库上,从而大大提高数据处理能力。 服务器
原来全部的数据都是在一个数据库上的,网络IO及文件IO都集中在一个数据库上的,所以CPU、内存、文件IO、网络IO均可能会成为系统瓶颈。而分区的方案就是把某一个或某几张相关的表的数据放在一个独立的数据库上,这样就能够把CPU、内存、文件IO、网络IO分解到多个机器中,从而提高系统处理能力。 网络
无论是上面的读写分离方案仍是数据分区方案,当数据量大到必定程度的时候,都会致使处理性能的不足,这个时候就没有办法了,只能进行分表处理。也就是把数据库当中数据根据按照分库原则分到多个数据表当中,这样,就能够把大表变成多个小表,不一样的分表中数据不重复,从而提升处理效率。 app
分表也有两种方案: 框架
1. 同库分表:全部的分表都在一个数据库中,因为数据库中表名不能重复,所以须要把数据表名起成不一样的名字。 分布式
2. 不一样库分表:因为分表在不一样的数据库中,这个时候就可使用一样的表名。 函数
经过上面的描述,咱们理解了读写分离,数据分区,数据分表三个解决方案,实际上都各有优势,也各有缺 ,所以,实践当中,会把三种方案混合使用。因为数据不是一天长大的,实际上,在刚开始的时候,可能只采用其中一种方案,随着应用的复杂,数据量的增加,会逐步采用多个方案混合的方案。以提高处理能力,避免单点。 性能
正所谓条条大路通罗马,解决这个问题的方案也有多种,但究其深源,均可以归到两种方案之上,一种是对用户透明的方案,即用户只用像普通的JDBC数据源同样访问便可,由框架解决全部的数据访问问题。另一种是应用层解决,具体通常是在Dao层进行封装。
一样是JDBC方案,也有两种解决方案,一种是有代理模式,一种是无代理模式。
有代理模式,有一台专门的代理服务器,来接收用户请求,而后发送请求给数据库集群中的数据,并对数据进行聚集后再提交给请求方。
无代理模式,就是说没有代理服务器,集群框架直接部署在应用访问端。
有代理模式,可以提供的功能更强大,甚至可买提供中间库进行数据处理,无代理模式处理性能较强有代理模式少一次网络访问,相对来讲性能更好,可是功能性不若有代理模式。
因为须要对SQL脚本进行判断,而后进行路由,所以DAO层优化方案通常都是选用iBatis或Spring Jdbc Template等方案进行封装,而对于Hibernate等高度封装的OR映射方案,实现起来就很是困难了。
需求决定了后续的解决方案及问题领域:
明确不支持的内容或限定条件:
上面的红色部分特性是最新添加的功能特性,这里简单解释一下:
支持数据库自增加主键:好比:Mysql,SqlServer等数据库支持 auto increase主键,原来不支持,如今能够完美支持了
支持数据库分页指令:好比,有些数据库支持start limit或相似的分页指令,原来不支持,如今能够完美支持了
支持DDL语句:原来对数据库结构方面的操纵,原来不支持,如今能够完美的支持了,好比:能够一次修改全部的分片的表结构
支持Grovy脚本定义分区分片规则:原来是只能采用实现接口的方式进行分区和分片规则的实现,如今能够用Grovyy脚原本定义了
支持JDK1.5~1.8:原来是是分红两个版本,一个jdbc3,一个jdbc4的,框架开发和维护很是麻烦,如今合并成一个工程,两个能够同步支持了
支持链接延迟获取:原来是整个分区分片中须要的链接一次申请到,如今优化为须要时才申请,这样会大大下降对数据库链接资源的须要,同时因为事务变小,也能够显著提高处理效率。
框架采用三层设计:最上层是Cluster,一个Cluster至关于咱们常规的一个数据库;一个Cluster当中能够包含一到多个Partition,也就是分区;而一个Partition中能够包含一到多个Shard,也就是分片。
因此一个就造成了一个树状结构,经过Cluster->Partion->Shard就构成了整个数据库集群。可是对于开发人员来讲,实际上并不知道这个内部结构,他只是链接上了一个JDBC数据源,而后作它应该作的事情就能够了。
以完整的形态对外提供服务,它封装了Cluster当中全部Partition及其Shard的访问。把它打开是一个数据库集群,对于使用者来讲是一个完整的数据库。
属性名 |
类型 |
说明 |
id |
String |
集群标识 |
userName |
String |
链接集群时的用户名 |
Password |
String |
链接集群时的密码 |
dataSources |
List<DataSourceConfig> |
集群中须要访问的数据源列表 |
partitions |
List<Partition>; |
集群中包含的分区列表 |
分区,分区有两种模式,一种是主从模式,用于作读写分离;另一种模式是分片模式,也就是说把一个表中的数据分解到多个表中。一个分区只能是其中的一种模式。可是一个Cluster能够包含多个分区,不一样的分区能够是不一样的模式。
属性名 |
类型 |
说明 |
id |
String |
分区标识 |
mode |
int |
分区类型,能够是主从,也能够是分表 |
Password |
String |
链接集群时的密码 |
shards |
List<Shard> |
分区中包含的分片列表 |
partitionRules |
List<PartitionRule> |
分区规则,当进行处理的时候,路由到哪一个分区执行 |
Shard与一个物理的数据源相关联。
属性名 |
类型 |
说明 |
id |
String |
分区标识 |
dataSourceId |
String |
实际访问的数据库配置ID |
readWeight |
int |
读权重,仅用于主从读写分离模式 |
writeWeight |
int |
写权重,仅用于主从读写分离模式 |
shardRules |
List<ShardRule> |
分片规则,当进行处理的时候,路由到哪一个分片执行,仅用于分模式 |
tableMappings |
List<TableMapping>; |
表名映射列表,仅用于同库不一样表名分表模式 |
/** * 分布式Key获取器 * * @param <T> */ public interface ClusterKeyGenerator<T> { T getKey(String tableName); }
主键接口能够用来生成各类主键类型,如:字符串、整型、长整型,入口参数必须是表名,框架已经实现了字符串、整型、长整型的分布式高效主键生成器,固然,也能够自行实现。
public interface ClusterManager { /** * 返回是不是分片语句 * * @param partition * @param sql * @return */ boolean isShardSql(Partition partition, String sql); /** * 添加语句处理器 * * @param statementProcessor */ void addStatementProcessor(StatementProcessor statementProcessor); /** * 返回语句处理器列表 * * @return */ List<StatementProcessor> getStatementProcessorList(); /** * 给某个集群的数据表产生主键 * * @param cluster * @param tableName * @param <T> * @return */ <T> T getPrimaryKey(Cluster cluster, String tableName); /** * 返回SQL对应的Statement * * @param sql * @return */ Statement getSqlStatement(String sql); /** * 添加集群 * * @param cluster */ void addCluster(Cluster cluster); /** * 获取集群 * * @param clusterId * @return */ Cluster getCluster(String clusterId); /** * 返回某个分区与sql是否匹配 * * @param partition * @param sql * @return */ boolean isMatch(Partition partition, String sql); /** * 返回某个分片是否匹配 * * @param shard * @param sql * @return */ boolean isMatch(Partition partition, Shard shard, String sql); /** * 返回分片执行语句 * * @param partition * @param shard * @param sql * @return */ String getSql(Partition partition, Shard shard, String sql); /** * 获取匹配的分区<br> * * @param clusterId * @param sql * @return */ Collection<Partition> getPartitions(String clusterId, String sql); /** * 获取匹配的首个分区 * * @param clusterId * @param sql * @return */ Partition getPartition(String clusterId, String sql); /** * 获取匹配的首个分区 * * @param cluster * @param sql * @return */ Partition getPartition(Cluster cluster, String sql); /** * 获取匹配的分区 * * @param cluster * @param sql * @return */ List<Partition> getPartitions(Cluster cluster, String sql); /** * 获取匹配的分片 * * @param partition * @param sql * @return */ List<Shard> getShards(Partition partition, String sql); /** * 返回分片均衡器 * * @return */ ShardBalance getShardBalance(); /** * 设置分片均衡器 * * @param balance */ void setShardBalance(ShardBalance balance); }
/** * 分区规则接口<br> * 规则参数在实现类中定义 * */ public interface PartitionRule { /** * 返回是否命中,若是有多个命中,则只用第一个进行处理 * * @param sql * @return */ boolean isMatch(String sql); }
/** * 分片规则 * */ public interface ShardRule { /** * 返回是否属于当前分片处理 * * @param sql * @return */ boolean isMatch(Partition partition, String sql); }
/** * 用于对SQL进行特殊处理并进行结果合并等<br> * <p/> * 好比sql语句是select count(*) from abc<br> * 则会到全部的shard执行,并对结果相加后返回 * */ public interface StatementProcessor { /** * 返回是否由此SQL处理器进行处理 * * @param sql * @return */ boolean isMatch(String sql); /** * 返回处理器转换过以后的SQL * * @param sql * @return */ String getSql(String sql); /** * 对结果进行合并 * * @param results * @return * @throws SQLException */ ResultSet combineResult(List<ResultSet> results) throws SQLException; }
好比:用户输入的SQL语句是:Select count(*) from aaa
这个时候就会用分片指令到各个分片去查找并返回结果,默认的处理结果是简单合并结果集的方式。这个时候,若是有5个分片,会返回5条记录给最终用户,这固然不是他想要的结果。这个时候就是语句处理器大显身手的时候了,他能够偷梁换柱,也能够改头换面,经过它的处理,就能够返回正确的结果了。
固然,要想外部程序用JDBC的方式进行访问,就得作JDBC层的实现。这个部分作了大量的处理,使得即高效又与用户指望的方式相匹配。
能够说上面全部的准备都是为了这一层作准备的,毕竟最终要落到真正的数据库访问上。因为接口就是标准的JDBC接口,所以就再也不详述。
在分区或分表模式中,因为写操做会被分解到不一样的物理数据库上去,这就会致使出现事务问题。所以框架内部集成了JTA,使得事务保持一致。
没什么好说的,噼里啪啦,噼里啪啦,一阵乱响,代码就绪了,下面看看测试场景。
Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password"); Statement stmt = conn.createStatement(); stmt.execute(“select * from aaa”);
上面彻底是按照JDBC的方式访问数据库的,url必须以“jdbc:dbcluster://”开始,后面跟着的是集群的ID名称,上面示例中就是“cluster1”;用户名、密码必须与集群中配置的相一致。接下来就与普通的jdbc数据源没有任何区别了。
在同一个数据库中建立一样结构的表,好比:
CREATE TABLE `aaa0` ( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `aaa1` ( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `aaa2` ( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) );
测试代码:
public static void main(String[] args) throws Throwable { Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password"); Statement stmt = conn.createStatement(); String sql; //插入100条数据 for (int i = 0; i < 100; i++) { sql = "insert into aaa(id,aaa) values (" + clusterManager.getPrimaryKey(cluster, "aaa") + ",'ppp')"; boolean result = stmt.execute(sql); } }
运行结果:
Using shard:shard1 to execute sql:insert into aaa(id,aaa) values (1,'ppp') Using shard:shard2 to execute sql:insert into aaa(id,aaa) values (2,'ppp') Using shard:shard0 to execute sql:insert into aaa(id,aaa) values (3,'ppp') Using shard:shard1 to execute sql:insert into aaa(id,aaa) values (4,'ppp') Using shard:shard2 to execute sql:insert into aaa(id,aaa) values (5,'ppp') Using shard:shard0 to execute sql:insert into aaa(id,aaa) values (6,'ppp') …….
能够看出,插入的数据确实分到了三个分片中。
再用Select语句查找插入的数据:
public static void main(String[] args) throws Throwable { Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password"); Statement stmt = conn.createStatement(); String sql = "select * from aaa order by id"; ResultSet resultSet = stmt.executeQuery(sql); while (resultSet.next()) { System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1), resultSet.getString(2)); } }
运行结果以下:
Using shard:shard0 to execute sql:select * from aaa order by id Using shard:shard1 to execute sql:select * from aaa order by id Using shard:shard2 to execute sql:select * from aaa order by id id: 1, aaa: ppp id: 2, aaa: ppp id: 3, aaa: ppp id: 4, aaa: ppp id: 5, aaa: ppp id: 6, aaa: ppp ……
从上面的结果能够看到,明显已经合并告终果而且是按顺序显示的
接下来,把测试的数据删除掉:
public static void main(String[] args) throws Throwable { Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", " username ", "password"); Statement stmt = conn.createStatement(); String sql = "delete from aaa"; stmt.execute(sql); }
运行结果以下:
Using shard:shard0 to execute sql:delete from aaa Using shard:shard1 to execute sql:delete from aaa Using shard:shard2 to execute sql:delete from aaa
再去数据库中查看,数据确实已经被删除。
在不一样的数据库中建立一样结构的表,好比:
CREATE TABLE test0. aaa ( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE test1. aaa( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ); CREATE TABLE test2. aaa( `id` int(11) NOT NULL, `aaa` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) );
测试用例同同库分表,结果测试一样OK。
插入与删除等比较简单,就再也不展现了,下面看看读指令的执行过程。
public static void main(String[] args) throws Throwable { Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password"); Statement stmt = conn.createStatement(); for (int i = 1; i <= 100; i++) { boolean result = stmt.execute(“select * from aaa”); } }
运行结果:
Using shard:shard3 to execute sql:select * from aaa Using shard:shard2 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard2 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard1 to execute sql:select * from aaa Using shard:shard3 to execute sql:select * from aaa
能够看到,读的SQL已经由三个分片进行了均衡执行。
对于ResultSet的遍历,也有良好的支持,对于各类移动光标的方法都有支持,并且支持排序的移动,同时对于性能也有良好支持,性能接近于单表操做。
下面展现一下绝对定位:
Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver"); Connection conn = DriverManager.getConnection( "jdbc:dbcluster://cluster1", "luog", "123456"); Statement stmt = conn.createStatement(); String sql = "select * from aaa order by id"; ResultSet resultSet = stmt.executeQuery(sql); resultSet.absolute(10); System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1), resultSet.getString(2)); while (resultSet.next()) { System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1), resultSet.getString(2)); }
运行结果:
Using shard:shard0 to execute sql:select * from aaa order by id Using shard:shard1 to execute sql:select * from aaa order by id Using shard:shard2 to execute sql:select * from aaa order by id id: 10, aaa: ppp id: 11, aaa: ppp id: 12, aaa: ppp id: 13, aaa: ppp id: 14, aaa: ppp id: 15, aaa: ppp id: 16, aaa: ppp id: 17, aaa: ppp id: 18, aaa: ppp id: 19, aaa: ppp …….
能够看到确实是从第10条开始显示。
分区分片通用解决方案,确实有至关的通用性,支持各类数据库,提供了很是大的灵活性,支持多种集群单独或混合使用的场景,同时还能够保持数据访问的事务一致性,为用户的访问提供与JDBC同样的用户接口,这也会大大下降开发人员的开发难度。基本上(违反需求中指定的限制条件的除外)能够作到原有业务代码透明访问,下降了系统的迁移成本。同时它在性能方面也是很是杰出的,与原生的JDBC驱动程序相比,性能没有显著下降。固然它的配置也是很是简单的,学习成本很是低。因为作在JDBC层,所以能够对Hibernate,iBatis等各类框架有良好支持。