Simple-Sharding : 一款极简的分库分表中间件

Simple-Sharding是一款基于JDBC API开发、简单易用的分库分表中间件,目标是通过较少的代码来揭示分库分表中间件最核心的本质。

背景

目前大多数互联网公司在遇到数据层瓶颈的时候,几乎都会做垂直或水平拆分。垂直拆分即按业务将库表分离,但是当拆分后的单表数据量达到一个新的量级的时候,会接着对这个大表做水平拆分,即将单个大表拆分成多个分表,有时会将其中的一些分表落地到不同的分库,以此来应对快速增长的业务。

从知名的分库分表中间件TDDL和Cobar开始,各个公司也都相继研发甚至开源了自己的分库分表中间件,这些中间件主要分为两类:一类是基于JDBC API实现的中间件,一类是类似于MySQL Proxy的代理中间件。整体的思路都是通过拦截应用层的SQL请求,根据相应规则做路由分发,然后落地到物理节点,最后执行获取结果。

根据笔者研究市面上已有代码的经验,发现成熟的项目往往代码量庞大,历史变更较多,导致学习研究分库分表中间件并不是一件十分容易的事情,很多时候抓不住本质,于是笔者就根据目前已经学习到的知识和经验,自己动手写了一个极简主义的分库分表中间件,我把她命名为Simple-Sharding !

开源地址为https://github.com/yuanwhy/simple-sharding ,欢迎Star,嘿嘿~

Simple-Sharding

下面具体介绍Simple-Sharding的一些细节以及笔者在其中的思考,欢迎批评指正。

实现思路

每一个学习过Java操作数据库的同学,最开始都是从JDBC的知识入手,后来才慢慢在项目中引入像Mybatis这样的ORM框架,建立起更加复杂的DAL(Data Access Layer)。

比如典型的代码如下:

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
Statement stmt = conn.createStatement();
String sql;
sql = "SELECT id, first, last, age FROM Employees";
ResultSet rs = stmt.execute(sql);

JDBC全称Java Database Connectivity, 是Java官方定义的一套访问数据库的接口规范,这些接口主要包括:

  • DataSource
  • Connection
  • Statement
  • PreparedStatement
  • ResultSet

每个数据库厂商都会自己实现这一套接口并提供给应用程序使用,比如MySQL提供的Connector/J,程序包为mysql-connector-java-{version}.jar。再复杂的DAO(Data Access Object),本质上内部还是通过JDBC来操作数据库,所有的ORM框架内部只有通过调用JDBC才能获取数据库连接。所以,Simple-Sharding的设计就是重写这套JDBC API,提供给应用新的DataSource实现类。

在Simple-Sharding中, 实现了前四个关键的接口,即

  • LogicDataSource
  • LogicConnection
  • LogicStatement
  • LogicPreparedStatement

比如,LogicDataSource的主要作用就是创建逻辑意义的数据库连接给上层使用,内部实现如下:

@Override
public Connection getConnection() throws SQLException {

    Connection connection = new LogicConnection(this);

    return connection;
}

应用将Simple-Sharding的DataSource注入到自己的IoC容器中,代替传统的c3p0或dbcp数据源:

<bean id="dataSource" class="com.yuanwhy.simple.sharding.jdbc.LogicDataSource">
    <property name="logicDatabase" value="passport"/>
    <property name="shardingRule" ref="shardingRule"/>
    <property name="physicalDataSourceMap">
        <map>
            <entry key="passport_0" value-ref="physicalDataSource0"/>
            <entry key="passport_1" value-ref="physicalDataSource1"/>
        </map>
    </property>
</bean>

而LogicConnection的主要作用就是获得Statement,而Statement是执行SQL语句的关键:

@Override
public Statement createStatement() throws SQLException {

    Statement statement = new  LogicStatement(this);

    return statement;
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {

    LogicPreparedStatement prepareStatement = new LogicPreparedStatement(this, sql);

    return prepareStatement;
}

规则接口

除了提供JDBC API之外,还要提供给应用指明分库分表规则的接口,因为中间件需要根据用户定义的规则对原始的SQL进行路由和重写,即根据分库字段获得分库的真实库名,根据分表字段获得分表的真实表名。

public interface ShardingRule {

    String getFieldNameForDb();

    String getFieldNameForTable();

    String getDbSuffix(Object fieldValueForDb);

    String getTableSuffix(Object fieldValueForTable);

}

接口ShardingRule定义了四个方法,getFieldNameForDb是希望能得知哪一个是分库字段,getFieldNameForTable是希望能得知哪一个是分表字段,然后通过getDbSuffix和getTableSuffix分别从分库字段值和分表字段值中计算出物理库名和物理表名的后缀。

Simple-Sharding提供了一个默认的分库分表规则的实现HashShardingRule,该规则采用取模hash法来获取后缀,当然应用也可以自己实现这个接口,来自定义分库分表规则。

数据模型

在Simple-Sharding的Unit Test中建立了这样一个数据模型:在passport库中有user表,user表分买家(Role为0)和卖家(Role为1),user表有id、name、role等必要的字段。传统的场景是passport库中只有一个user表,现在根据需求做分库分表,一种比较合理的方案是按买家和卖家来分库,每个库再做分表,于是逻辑架构如下图所示:

User

分表规则默认使用HashShardingRule,为了考虑分布的均匀性,一般选择id为分表字段进行取模运算作为表名后缀,比如这里分了两个表,用户id为11的分表为user_1表(11%2 = 1)。用户id应该设置为全局唯一,这时候数据库自增显然不再适用,全局唯一id生成算法又是另外一个话题了,这里不作为讨论的重点。

SQL解析与执行

在调用statement.execute(sql)执行SQL时,statement获得的是原始SQL,中间件需要把原始SQL语句解析成AST(抽象语法树),然后取得分库分表字段值。Simple-Sharding使用了现成的SQL Parser工具 ,即阿里开源的Druid内部的SQL Parser组件:

List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL);

SQLStatement currentSqlStatement = sqlStatements.get(0);
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
currentSqlStatement.accept(visitor);

之后就可以通过currentSqlStatement和visitor直接获得分库分表字段值,当然前提是ShardingRule中配置了分库分表字段的名称。

解析之后,一方面要从配置的所有物理库中获取目标物理分库physicalDataSource0或physicalDataSource1,另一方面要将原始SQL进行替换,将passport替换成passport_0或passport_1、user替换成user_0或user_1。

比如原始SQL为

select * from passport.user where role = 0 and id = 11;

重写后的SQL为

select * from passport_0.user_1 where role = 0 and id = 11;

之后便是通过获取到的物理数据源physicalDataSource0来像传统方式一样执行SQL语句,并将获得的结果集返回给上层应用。在Simple-Sharding的LogicStatement类的doExecute方法中具体展示了在物理数据源上执行真实SQL的过程。

事务支持

Simple-Sharding目前仅支持单库事务,分布式事务太过复杂,目前暂不考虑。单库事务的实现思路其实很简单,只要保证一串事务内的SQL解析后都落地到同一个分库即可,即整个事务阶段LogicConnection只会使用一个物理Connection,事务结束后又开启新的事务的时候,LogicConnection又会开启一个新的可能完全不同的物理Connetion。

关键代码如下:

if(this.logicConnection.getPhysicalConnection() != null) {

    if (physicalDbName.equals(this.logicConnection.getPhysicalDbName())) {

        physicalConnection = this.logicConnection.getPhysicalConnection();

    } else {
        throw new RuntimeException("不支持跨库事务 : " + originalSql);
    }

} else {

    physicalConnection = physicalDataSource.getConnection();

    this.logicConnection.setPhysicalConnection(physicalConnection);
    this.logicConnection.setPhysicalDbName(physicalDbName);

    physicalConnection.setAutoCommit(this.logicConnection.getAutoCommit());

}

那么如何测试事务呢?在Simple-Sharding的Unit Test中给出了测试事务的用例:获取Connection之后,autoCommit设置为false便在逻辑上开启了一个事务(autoCommit=true的时候每一条SQL都默认是一个事务),之后执行增删改查并且没提交的时候,在其他会话中会表现出隔离性(MySQL默认隔离级别):

connection1.setAutoCommit(false);

User user = new User(123, "yuanwhy", 18, User.Role.BUYER.getId());
insertUser(connection1, user);
User foundUserFromConnection1 = selectUser(connection1, user);
Assert.assertTrue(user.equals(foundUserFromConnection1));

User foundUserFromConnection2 = selectUser(connection2, user);
Assert.assertTrue(foundUserFromConnection2 == null); // 事务隔离,connection2一定读不到connection1的数据

这样就基本在Simple-Sharding中实现了单库事务,单库事务对实际应用也是必须的,这一点在很多分库分表中间件中都有实现。

总结

Simple-Sharding在JDBC API的基础上实现了一套新的数据源,内部提供了基本的分库分表支持,同时简单地实现了单库事务,基本上把分库分表中间件最核心的流程走通了一遍。作为研究性质的项目,Simple-Sharding目前还很年轻,不推荐在生产环境中使用,希望对于想学习分库分表中间件的同学有所帮助。

最后,欢迎Star和交流 : https://github.com/yuanwhy/simple-sharding .

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容