追求极致的数据库分区分表方案

序言

一直在作企业应用,目前要作一些互联网应用,固然只是应用是放在互联网的,数据量距离真正的互联网应用仍是有至关大的差距的。可是不可避免的,在数据库出现瓶颈的状况仍是有的,如今作互联网上的应用,固然也要未雨绸缪,要考虑数据量大的时候的解决方案。 java

这个目前开源的商用的也都有很多解决方案,一来,作技术的都有这么个臭毛病,即便是使用别人的方案,本身也要搞清楚内部的一些实现机制,这样才会有真正的体会,不然去评估一个方案的时候,就只能盲人摸象了。 sql

为此,构建一个验证型的分布式数据库框架,来解决数据库的垂直与水平扩展方面的问题,因为是验证性开发,因此,思考不完善的地方确定存在,欢迎批评指正。 数据库

提高数据库处理能力方案

读写分离方案

海量数据的存储及访问,经过对数据库进行读写分离,来提高数据的处理能力。读写分离它的方案特色是数据库产生多个副本,数据库的写操做都集中到一个数据库上,而一些读的操做呢,能够分解到其它数据库上。这样,只要付出数据复制的成本,就可使得数据库的处理压力分解到多个数据库上,从而大大提高数据处理能力。 服务器

  • 优势:因为全部的数据库副本,都有数据的全拷贝,所以全部的数据库特性均可以实现,部分机器当机不影响系统的使用。
  • 缺点:数据的复制同步是一个问题,要么采用数据库自身的复制方案,要么自行实现数据复制方案。须要考虑数据的迟滞性,一致性方面的问题。

数据分区方案

原来全部的数据都是在一个数据库上的,网络IO及文件IO都集中在一个数据库上的,所以CPU、内存、文件IO、网络IO均可能会成为系统瓶颈。而分区的方案就是把某一个或某几张相关的表的数据放在一个独立的数据库上,这样就能够把CPU、内存、文件IO、网络IO分解到多个机器中,从而提高系统处理能力。 网络

  • 优势:不存在数据库副本复制,性能更高。
  • 缺点:分区策略必须通过充分考虑,避免多个分区之间的数据存在关联关系,每一个分区都是单点,若是某个分区宕机,就会影响到系统的使用。

数据分表方案

无论是上面的读写分离方案仍是数据分区方案,当数据量大到必定程度的时候,都会致使处理性能的不足,这个时候就没有办法了,只能进行分表处理。也就是把数据库当中数据根据按照分库原则分到多个数据表当中,这样,就能够把大表变成多个小表,不一样的分表中数据不重复,从而提升处理效率。 app

  • 优势:数据不存在多个副本,没必要进行数据复制,性能更高。
  • 缺点:分表之间的数据不多进行集合运算;分表都是单点,若是某个分表宕机,若是使用的数据不在此分表,不影响使用。

分表也有两种方案: 框架

1. 同库分表:全部的分表都在一个数据库中,因为数据库中表名不能重复,所以须要把数据表名起成不一样的名字。 分布式

  • 优势:因为都在一个数据库中,公共表,没必要进行复制,处理更简单
  • 缺点:因为还在一个数据库中,CPU、内存、文件IO、网络IO等瓶颈仍是没法解决,只能下降单表中的数据记录数。表名不一致,会导后续的处理复杂。

2. 不一样库分表:因为分表在不一样的数据库中,这个时候就可使用一样的表名。 函数

  • 优势:CPU、内存、文件IO、网络IO等瓶颈能够获得有效解决,表名相同,处理起来相对简单
  • 缺点:公共表因为在全部的分表都要使用,所以要进行复制、同步。

混合方案

经过上面的描述,咱们理解了读写分离,数据分区,数据分表三个解决方案,实际上都各有优势,也各有缺 ,所以,实践当中,会把三种方案混合使用。因为数据不是一天长大的,实际上,在刚开始的时候,可能只采用其中一种方案,随着应用的复杂,数据量的增加,会逐步采用多个方案混合的方案。以提高处理能力,避免单点。 性能

实现路线分析

正所谓条条大路通罗马,解决这个问题的方案也有多种,但究其深源,均可以归到两种方案之上,一种是对用户透明的方案,即用户只用像普通的JDBC数据源同样访问便可,由框架解决全部的数据访问问题。另一种是应用层解决,具体通常是在Dao层进行封装。

JDBC层方案

  • 优势:开发人员使用很是方便,开发工做量比较小;能够实现数据库无关。
  • 缺点:框架实现难度比较大,性能不必定能作到最优。

一样是JDBC方案,也有两种解决方案,一种是有代理模式,一种是无代理模式。

有代理模式,有一台专门的代理服务器,来接收用户请求,而后发送请求给数据库集群中的数据,并对数据进行聚集后再提交给请求方。

无代理模式,就是说没有代理服务器,集群框架直接部署在应用访问端。

有代理模式,可以提供的功能更强大,甚至可买提供中间库进行数据处理,无代理模式处理性能较强有代理模式少一次网络访问,相对来讲性能更好,可是功能性不若有代理模式。

DAO层方案

  • 优势:开发人员自由度很是大,性能调优更精准。
  • 缺点:开发人员在必定程度上受影响,与具体的Dao技术实现相关,较难作到数据库无关。

因为须要对SQL脚本进行判断,而后进行路由,所以DAO层优化方案通常都是选用iBatis或Spring Jdbc Template等方案进行封装,而对于Hibernate等高度封装的OR映射方案,实现起来就很是困难了。

需求

需求决定了后续的解决方案及问题领域:

  • 采用JDBC层解决方案:对于最终用户来讲,要彻底透明
  • 采用无代理解决方案:数据库集群框架代码直接放在应用层
  • 支持读写分离、分区、分表三种方式及其混合使用方式:三种方式能够混用能够提供极大的灵活性及对将来的扩展性
  • 须要提供灵活的分区及分表规则支持
  • 对于读写分离的方案,须要提供灵活的路由规则,好比:平均路由规则、加权路由规则,能够提供写库的备用服务器,即主写入服务器当机以后,便可写入备用服务器当中。
  • 支持高性能分布式主键生成器
  • 有良好的集群事务功能
  • 能够经过扩展点来对框架进行扩展,以便于处理分区、分表相关的操做。
  • 支持各类类型支持JDBC驱动的数据库
  • 支持异构数据库集群
  • 支持count、sum、avg、min、max等统计函数
  • 支持排序
  • 支持光标移动
  • 支持结果集合并
  • 支持数据库自增加主键
  • 支持数据库分页指令
  • 支持DDL语句处理
  • 支持Grovy脚本定义规则
  • 支持JDK1.5~1.8
  • 支持链接延迟获取--只到使用时才申请数据库链接,提高性能下降资源打败

明确不支持的内容或限定条件:

  • 不支持分区之间的联合查询

特性说明

上面的红色部分特性是最新添加的功能特性,这里简单解释一下:

支持数据库自增加主键:好比: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

以完整的形态对外提供服务,它封装了Cluster当中全部Partition及其Shard的访问。把它打开是一个数据库集群,对于使用者来讲是一个完整的数据库。

属性名

类型

说明

id

String

集群标识

userName

String

链接集群时的用户名

Password

String

链接集群时的密码

dataSources

List<DataSourceConfig>

集群中须要访问的数据源列表

partitions

List<Partition>;

集群中包含的分区列表

Partition

分区,分区有两种模式,一种是主从模式,用于作读写分离;另一种模式是分片模式,也就是说把一个表中的数据分解到多个表中。一个分区只能是其中的一种模式。可是一个Cluster能够包含多个分区,不一样的分区能够是不一样的模式。

属性名

类型

说明

id

String

分区标识

mode

int

分区类型,能够是主从,也能够是分表

Password

String

链接集群时的密码

shards

List<Shard>

分区中包含的分片列表

partitionRules

List<PartitionRule>

分区规则,当进行处理的时候,路由到哪一个分区执行

Shard

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层的实现。这个部分作了大量的处理,使得即高效又与用户指望的方式相匹配。

能够说上面全部的准备都是为了这一层作准备的,毕竟最终要落到真正的数据库访问上。因为接口就是标准的JDBC接口,所以就再也不详述。

事务问题

在分区或分表模式中,因为写操做会被分解到不一样的物理数据库上去,这就会致使出现事务问题。所以框架内部集成了JTA,使得事务保持一致。

代码实现

没什么好说的,噼里啪啦,噼里啪啦,一阵乱响,代码就绪了,下面看看测试场景。

测试用例

JDBC方式访问集群

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等各类框架有良好支持。

相关文章
相关标签/搜索