Sharding-JDBC实战

 

1、快速入门

1.1、分库分表数据库架构

以订单中心为例,无论库还是表,都是以卖家ID或者买家ID去分片。假设有两个数据库 order_center_0和order_center_1,每个库有两张表t_order_0,t_order_1以及t_order_item_0和t_order_item_1,于是我们得到下面的数据库架构:

Sharding-JDBC实战

1.2、Sharding-JDBC的配置

1.2.1、maven依赖

<dependency>

<groupId>com.dangdang</groupId>

<artifactId>sharding-jdbc-core</artifactId>

<version>1.4.1</version>

</dependency>

 

<dependency>

<groupId>com.dangdang</groupId>

<artifactId>sharding-jdbc-config-common</artifactId>

<version>1.4.1</version>

</dependency>

 

<dependency>

<groupId>com.dangdang</groupId>

<artifactId>sharding-jdbc-config-spring</artifactId>

<version>1.4.1</version>

</dependency>

 

<dependency>

<groupId>com.dangdang</groupId>

<artifactId>sharding-self-id-generator</artifactId>

<version>1.4.1</version>

</dependency>

 

<dependency>

<groupId>org.codehaus.groovy</groupId>

<artifactId>groovy-all</artifactId>

<version>2.4.7</version>

</dependency>

 

<!-- druid最好用1.0.12,因为当当官网也是基于这个开发的,最新版本的druid对某些sql的解析有问题-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>druid</artifactId>

<version>1.0.12</version>

</dependency>

 

1.2.2、spring配置      

    

<beans xmlns="http://www.springframework.org/schema/beans"

     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance

     xmlns:context="http://www.springframework.org/schema/context"

    xmlns:rdb="http://www.dangdang.com/schema/ddframe/rdb

    xsi:schemaLocation="http://www.springframework.org/schema/beans 

                        http://www.springframework.org/schema/beans/spring-beans.xsd

                        http://www.springframework.org/schema/context 

                        http://www.springframework.org/schema/context/spring-context.xsd 

                        http://www.dangdang.com/schema/ddframe/rdb 

                        http://www.dangdang.com/schema/ddframe/rdb/rdb.xsd ">

  <context:property-placeholder location="classpath:setting-test.properties" ignore-unresolvable="true" />

 <bean id="order_center_0" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 

        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://120.26.123.50:3306/order_center_0"/>

        <property name="username" value="developer"/>

        <property name="password" value="developer_meiyou_01"/>

    </bean>

    <bean id="order_center_1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">

        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>

        <property name="url" value="jdbc:mysql://120.26.123.50:3306/order_center_1"/>

        <property name="username" value="developer"/>

        <property name="password" value="developer_meiyou_01"/>

    </bean> 

    <rdb:strategy id="orderTableStrategy" sharding-columns="order_id" algorithm-expression="t_order_${order_id.longValue() % 2}"/> 

    <rdb:strategy id="orderItemTableStrategy" sharding-columns="order_id" algorithm-expression="t_order_item_${order_id.longValue() % 2}"/> 

    <rdb:strategy id="databaseStrategy" sharding-columns="order_id" algorithm-expression="order_center_${order_id.longValue() % 2}"/>

    <rdb:data-source id="shardingDataSource"> 

        <rdb:sharding-rule data-sources="order_center_0,order_center_1">

            <rdb:table-rules>

                <rdb:table-rule logic-table="t_order" table-stratey="orderTableStrategy"  database-strategy="databaseStrategy"/>

                <rdb:table-rule logic-table="t_order_item" table-stratey="orderItemTableStrategy"  database-strategy="databaseStrategy"/>

            </rdb:table-rules>

        </rdb:sharding-rule>

    </rdb:data-source>

</beans>

1、每个数据库都对应一个数据源,上面两个库分别对应order_center_0、order_center_1这两个数据源

2、shardingDataSource是一个统一的数据源门面,它屏蔽了下层的多个数据源,给上层统一的获取Connection的接口。

3、shardingDataSource存在一个分片规则ShardingRule,它持有了每个数据库的数据源,并且持有了表分片规则TableRules,TableRules是一个列表,里面包含每个逻辑表的规则TableRule。

4、每个逻辑表的规则TableRule会使用到数据库分库策略DatabaseStraegy和分表策略TableStrategy,这些策略会决定具体的库和表,将逻辑表转换成物理表。比如t_order可能会被映射成order_center_1.t_order_0

5、分库策略和分表策略可以使用groovy表达式,比如上面的例子,遇到复杂的策略,可以实现下面这些接口:

5.1、com.dangdang.ddframe.rdb.sharding.api.strategy.table.SingleKeyTableShardingAlgorithm

5.2、com.dangdang.ddframe.rdb.sharding.api.strategy.table.MultipleKeysTableShardingAlgorithm

5.3、com.dangdang.ddframe.rdb.sharding.api.strategy.database.SingleKeyDatabaseShardingAlgorithm

5.4、com.dangdang.ddframe.rdb.sharding.api.strategy.database.MultipleKeysDatabaseShardingAlgorith

 

1.3、Sharding-JDBC基本操作代码示例

1.3.1、插入

@Test

 public void test_测试插入订单()throws Exception{

        IPIdGenerator idgen = new IPIdGenerator();  //使用IPIdGenerator生成全局唯一的orderId

        Connection connection = shardingDataSource.getConnection();

        PreparedStatement ps = connection.prepareStatement("insert into t_order(order_id,seller_id,buyer_id) values(?,?,?)");

        ps.setLong(1, (Long)idgen.generateId());

        ps.setInt(2, 29131);

        ps.setInt(3, 4232);

        ps.execute();

 }

 

1.3.2、更新

@Test

 public void test_测试更新订单()throws Exception{

       Connection connection = shardingDataSource.getConnection();

       PreparedStatement ps = connection.prepareStatement("update t_order set seller_id=7876 where order_id=20156528694374400");

       ps.execute();

 }

 

1.3.3、删除

@Test

 public void test_测试删除订单()throws Exception{

       Connection connection = shardingDataSource.getConnection();

       PreparedStatement ps = connection.prepareStatement("delete from t_order where order_id=20159493618511872");

       ps.execute();

 }

 

1.3.4、分库分表查询

@Test

 public void test_测试分库分表查询() throws Exception{

       Connection connection = shardingDataSource.getConnection();

       PreparedStatement ps = connection.prepareStatement("select order_id,seller_id,buyer_id from t_order where order_id=?");

       ps.setLong(1, 9987L); //走order_center_1库的t_order_1表

       ResultSet rs = ps.executeQuery();

       while(rs.next()){

              Long orderId = rs.getLong("order_id");

              Integer sellerId = rs.getInt("seller_id");

              Integer buyerId = rs.getInt("buyer_id");

              Assert.assertEquals(9987, orderId.longValue());

              Assert.assertEquals(123, sellerId.intValue());

              Assert.assertEquals(456, buyerId.intValue());

       }

 }

 

1.3.5、全表扫描

@Test

 public void test_测试全表扫描() throws Exception{

           Connection connection = shardingDataSource.getConnection();

           PreparedStatement ps = connection.prepareStatement("select * from t_order");

           ResultSet rs = ps.executeQuery();

           while(rs.next()){

                Long orderId = rs.getLong("order_id");

                Integer sellerId = rs.getInt("seller_id");

                Integer buyerId = rs.getInt("buyer_id");

                System.out.println(orderId);

                System.out.println(sellerId);

                System.out.println(buyerId);

            }

 }

 

1.3.6、Hint使用

如果sql里没有传分片字段,则可以使用HintManager手动加入,如果不加入,则走全表扫描

@Test

 public void test_测试使用Hint() throws Exception{

       HintManager hintManager = HintManager.getInstance();

       Connection connection = shardingDataSource.getConnection();

       PreparedStatement ps = connection.prepareStatement("select * from t_order");

       hintManager.addDatabaseShardingValue("t_order", "order_id", "9980");

       hintManager.addTableShardingValue("t_order", "order_id", "9980");

       ResultSet rs = ps.executeQuery();

       while(rs.next()){

             Long orderId = rs.getLong("order_id");

             Integer sellerId = rs.getInt("seller_id");

             Integer buyerId = rs.getInt("buyer_id");

             Assert.assertEquals(9980, orderId.longValue());

             Assert.assertEquals(221, sellerId.intValue());

             Assert.assertEquals(753, buyerId.intValue());

        }

 }

 

1.3.7、查询后排序

本质上是拿到每个数据源的结果集然后在内存排序,所以尽量将每个数据源中返回的结果集最小化

@Test

 public void test_测试排序()throws Exception{

          Connection connection = shardingDataSource.getConnection();

          PreparedStatement ps = connection.prepareStatement("select * from t_order order by order_id");

          ResultSet rs = ps.executeQuery();

          while(rs.next()){

               Long orderId = rs.getLong("order_id");

               Integer sellerId = rs.getInt("seller_id");

               Integer buyerId = rs.getInt("buyer_id");

          }

 }

 

1.3.8、无条件全表统计

@Test

 public void test_测试无条件统计()throws Exception{

             Connection connection = shardingDataSource.getConnection();

             PreparedStatement ps = connection.prepareStatement("select count(*) as c from t_order");

             ResultSet rs = ps.executeQuery();

             while(rs.next()){

                   Long c = rs.getLong("c");

                   System.out.println(c);

             }

 }

 

1.3.9、有条件统计

@Test

 public void test_测试有条件统计()throws Exception{

            Connection connection = shardingDataSource.getConnection();

            PreparedStatement ps = connection.prepareStatement("select count(*) as c from t_order where order_id=9987");

            ResultSet rs = ps.executeQuery();

            while(rs.next()){

                 Long c = rs.getLong("c");

                 System.out.println(c);

             }

 }

 

1.3.10、多表查询

@Test

 public void test_测试多表查询()throws Exception{

             Connection connection = shardingDataSource.getConnection();

             PreparedStatement ps = connection.prepareStatement("select * from t_order,t_order_item  where t_order.order_id = t_order_item.order_id and t_order.seller_id=221");

             ResultSet rs = ps.executeQuery();

             ResultSetMetaData metaData = rs.getMetaData();

             while(rs.next()){

                 for(int i =0;i<metaData.getColumnCount();i++){

                     System.out.println(rs.getObject(i+1));

                 }

              }

 }

 

1.3.11、join操作

@Test

 public void test_测试表联合查询()throws Exception{

              Connection connection = shardingDataSource.getConnection();

              PreparedStatement ps = connection.prepareStatement("select t_order_item.order_item_id as item_id from t_order  inner join t_order_item  on t_order.order_id = t_order_item.order_id where     t_order.order_id=9987");

              ResultSet rs = ps.executeQuery();

              while(rs.next()){

                   System.out.println(rs.getLong("item_id"));

               }

 }

 

1.3.12、分页

@Test

 public void test_测试全表扫描_limit分页()throws Exception{

                 Connection connection = shardingDataSource.getConnection();

                 PreparedStatement ps = connection.prepareStatement("select * from t_order order by order_id limit 2,2");

                 ResultSet rs = ps.executeQuery();

                 while(rs.next()){

                          Long orderId = rs.getLong("order_id");

                          Integer sellerId = rs.getInt("seller_id");

                           Integer buyerId = rs.getInt("buyer_id");

                  }

 }

 

1.4、与Mybatis的兼容性

Mybatis插件自动生成的模板方法

兼容性

countByExample

语法完全兼容,如果对某个库或者某个表进行count

需要设置分片字段,或者使用Hint

insert

语法完全兼容,分片字段必须设置,否则无法正确

插入

insertSelective

语法完全兼容,分片字段必须设置,否则无法正确

插入

selectByExample

语法不支持distinct设置,分片字段不设置则走全表扫描,

分片字段设置则走具体的某张表

selectByPrimaryKey

语法完全兼容,如果PrimaryKey不是分片字段,需要使用

Hint,否则走全表扫描,性能较差

updateByExampleSelective

语法完全兼容,分片字段不设置则走全表扫描,分片字段

设置则走具体的某张表

updateByExample

语法完全兼容,分片字段不设置则走全表扫描,分片字段

设置则走具体的某张表

updateByPrimaryKeySelective

语法完全兼容,如果PrimaryKey不是分片字段,需要使用

Hint,否则走全表扫描,性能较差

updateByPrimaryKey

语法完全兼容,如果PrimaryKey不是分片字段,需要使用

Hint,否则走全表扫描,性能较差

deleteByExample

语法完全兼容,分片字段不设置则走全表扫描,分片字段

设置则走具体的某张表

deleteByPrimaryKey

语法完全兼容,如果PrimaryKey不是分片字段,需要使用

Hint,否则走全表扫描,性能较差

 

 

1.5、已知的限制

1.5.1、JDBC未支持列表

  • Sharding-JDBC暂时未支持不常用的JDBC方法。

1.5.2、DataSource接口

  • 不支持timeout相关操作

1.5.3、Connection接口

  • 不支持存储过程,函数,游标的操作
  • 不支持执行native的SQL
  • 不支持savepoint相关操作
  • 不支持Schema/Catalog的操作
  • 不支持自定义类型映射

1.5.4、Statement和PreparedStatement接口

  • 不支持返回多结果集的语句(即存储过程,非SELECT多条数据)
  • 不支持国际化字符的操作

1.5.5、对于ResultSet接口

  • 不支持对于结果集指针位置判断
  • 不支持通过非next方法改变结果指针位置
  • 不支持修改结果集内容
  • 不支持获取国际化字符
  • 不支持获取Array

1.5.6、JDBC 4.1

  • 不支持JDBC 4.1接口新功能(查询所有未支持方法,请阅读com.dangdang.ddframe.rdb.sharding.jdbc.unsupported包。)

1.5.7、SQL语句限制

  • 不支持DDL语句
  • 不支持子语句
  • 不支持UNION 和 UNION ALL
  • 不支持特殊INSERT(每条INSERT语句只能插入一条数据,不支持VALUES后有多行数据的语句)
  • 不支持DISTINCT聚合
  • 不支持dual虚拟表

 

2、技术原理

2.1、Sharding-JDBC组件

Sharding-JDBC实战

 

Sharding-JDBC目前的版本是1.4.1,这个版本中主要包含四个组件,配置组件sharding-jdbc-config、全局ID生成组件sharding-jdbc-self-id-generator、柔性事务组件sharding-jdbc-transaction、核心组件sharding-jdbc-core。

2.1.1、配置组件sharding-jdbc-config

配置组件的作用是初始化整个Sharding-JDBC框架,它基于两种策略:spring和yaml。

yaml需要自己写配置文件,然后用DataSource dataSource = new YamlShardingDataSource(yamlFile);完成数据源初始化。spring基于命名空间和bena解析器进行初始化,将spring配置文件转换成内存中的ShardingRuleConfig,然后用ShardingRuleBuilder构造数据源返回到spring bean容器中。

 

2.1.2、全局ID生成组件sharding-jdbc-self-id-generator

sharding-jdbc的全局ID生成算法是基于Twitter的分布式自增ID算法snow flake,原理可以Google查阅。sharding-jdbc-self-id-generator有两种策略,第一种是HostNameIdGenerator,这种策略基于机器名字去生成ID,适合于生产环境的机器有特定编号的情况,比如order_cenetr_prod_01,order_cenetr_prod_02。第二种是IPIdGenerator,基于机器IP去生成ID。

 

2.1.3、柔性事务组件sharding-jdbc-transaction

柔性事务其实是一种弱的分布式事务,sharding-jdbc目前只实现了最大努力型事务,本质是用数据库保存失败的事务,然后通过调度任务不断重试。

 

2.1.4、核心组件sharding-jdbc-core

sharding-jdbc-core实现了sharding-jdbc的核心功能,对JDBC原生API进行了包装,对上层业务无感知。通过sql解析,sql路由,结果归并等实现了分库分表下的sql执行。

2.2、基于spring的初始化流程

Sharding-JDBC实战

基于spring的初始化流程很简单,主要是解析XML文件生成ShardingRuleConfig,然后通过ShardingRuleBuilder生成ShardingRule,然后将ShardingRule传入SpringShardingDataSource的构造函数,从而完成数据源的构造。下面主要看一下ShardingRuleConfig的类属性与spring配置文件中属性的对应关系。

2.2.1、ShardingRuleConfig

ShardingRuleConfig

 

ShardingRuleConfig字段

字段类型

字段描述

spring配置文件属性

dataSource

Map<String, DataSource>

真实数据源映射

<rdb:sharding-rule data-sources="dbtbl_0,dbtbl_1">中的data-sources

defaultDataSourceName

String

默认数据源名称

<rdb:sharding-rule default-data-source="dbtbl_0">中的default-data-source

tables

Map<String, TableRuleConfig>

表规则配置映射

<rdb:table-rules>

bindingTables

List<BindingTableRuleConfig>

绑定表

<rdb:binding-table-rules>

defaultDatabaseStrategy

StrategyConfig

默认分库策略

<rdb:default-database-strategy>

defaultTableStrategy

StrategyConfig

默认分表策略

<rdb:default-table-strategy>

idGeneratorClass

String

全局ID生成器类

<rdb:id-generator-class="">

 

spring配置:

 <rdb:sharding-rule data-sources="" default-data-source="">

     <rdb:table-rules>

         <rdb:table-rule />

     </rdb:table-rules>

     <rdb:binding-table-rules>

          <rdb:binding-table-rule />

     </rdb:binding-table-rules>

 <rdb:default-database-strategy />

 <rdb:id-generator-class="">  

</rdb:sharding-rule>

 

TableRuleConfig

TableRuleConfig字段

字段类型

字段描述

spring配置文件属性

dynamic

boolean

是否是动态表

dynamic="true"

actualTables

String

真实表名,多个表以逗号分隔,支持inline表达式,

指定数据源需要加前缀,不加前缀为默认数据源。

不填写表示为只分库不分表或动态表(需要配置dynamic=true)。

actual-tables="t_order_${0..3}"

dataSourceNames

String

数据源名称,多个数据源用逗号分隔,支持inline表达式。

不填写表示使用全部数据源

data-source-names="order_db_${0..3}"

databaseStrategy

StrategyConfig

分库策略

database-strategy="xxx" 引用策略bean

tableStrategy

StrategyConfig

分表策略

table-strategy="xxx" 引用策略bean

 spring配置:

<rdb:table-rule logic-table="" actual-tables="" database-strategy="" table-strategy="" data-source-names="" dynamic="">

 

 

BindingTableRuleConfig

BindingTableRuleConfig字段

字段类型

字段描述

spring配置文件属性

tableNames

String

逻辑表名列表,多个<logic_table_name>以逗号分隔

logic-tables="t_order, t_order_item"

spring配置:

<rdb:binding-table-rule logic-tables="t_order, t_order_item"/>

 

StrategyConfig

StrategyConfig字段

字段类型

字段描述

spring配置文件属性

shardingColumns

String

分片列名,多个列以逗号分隔

sharding-columns=""

algorithmClassName

String

分片算法类

algorithm-class=""

algorithmExpression

String

分片算法表达式,与algorithmClassName出现一个即可

algorithm-expression=""

spring配置:

<rdb:strategy id="databaseStrategy" sharding-columns="user_id" algorithm-class="xxx"/>

<rdb:strategy id="tableStrategy" sharding-columns="order_id" algorithm-expression="t_order_{order_id.longValue() % 2}"/>

 

2.2.2、构建流程

a、ShardingJdbcNamespaceHandler

public void init() {

registerBeanDefinitionParser("strategy", new ShardingJdbcStrategyBeanDefinitionParser()); registerBeanDefinitionParser("data-source", new ShardingJdbcDataSourceBeanDefinitionParser()); registerBeanDefinitionParser("master-slave-data-source", new MasterSlaveDataSourceBeanDefinitionParser());

}

spring通过ShardingJdbcNamespaceHandler注册三个解析器,分别解析三者根元素:strategy、data-source以及master-slave-data-source。其中strategy是全局的分片策略,可以是分库策略,也可以是分表策略,data-source是sharding-jdbc对外提供的数据源,master-slave-data-source是主从数据源。这里主要分析data-source的解析。

b、ShardingJdbcDataSourceBeanDefinitionParser

@Override

protected AbstractBeanDefinition parseInternal(final Element element, final ParserContext parserContext) {

BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(SpringShardingDataSource.class); factory.addConstructorArgValue(parseShardingRuleConfig(element, parserContext)); factory.addConstructorArgValue(parseProperties(element, parserContext)); return factory.getBeanDefinition();

}

ShardingJdbcDataSourceBeanDefinitionParser会解析出ShardingRuleConfig和Properties,设置为根元素的构造参数,然后返回。这里的根元素其实是一个SpringShardingDataSource的BeanDefinition,当spring初始化SpringShardingDataSource的时候,会调用相应的构造参数:

public class SpringShardingDataSource extends ShardingDataSource {

public SpringShardingDataSource(final ShardingRuleConfig shardingRuleConfig, final Properties props) {

super(new ShardingRuleBuilder(shardingRuleConfig).build(), props);

}

}

我们可以看到,SpringShardingDataSource的构造参数使用ShardingRuleBuilder配合之前解析的ShardingRuleConfig构建出ShardingRule,然后调用父类的构造方法,构建出一个ShardingDataSource。

 

2.3、Sharding-JDBC整体执行流程

Sharding-JDBC实战

 

2.3.1、JDBC操作的适配

1、获取数据库连接

Sharding-JDBC实战

 

 

ShardingDataSource每次获取数据库连接,都会新建一个ShardingConnection,这个ShardingConnection会使用真实的数据源获取真实的数据库连接。ShardingConnection会缓存获取过的数据库连接,一次调用中使用多次ShardingConnection,会从缓存中获取真实连接。由于真实连接也是被缓存的,本质上就是数据库连接引用在内存的转移,意义不大。

2、预编译sql

Sharding-JDBC实战

ShardingConnection预编译sql会创建一个ShardingPreparedStatement,在初始化的时候会用ShardingContext里的SQLRouteEngine生成一个PreparedSQLRouter,后续通过PreparedSQLRouter进行sql的解析、路由。

 

2.3.2、SQL解析和路由

Sharding-JDBC实战

 

2.3.3、SQL执行

Sharding-JDBC实战

2.3.4、结果集归并

Sharding-JDBC实战

更多文章:http://www.geekmuseo.com