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,于是我们得到下面的数据库架构:
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-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"/>
</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目前的版本是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的初始化流程
基于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整体执行流程
2.3.1、JDBC操作的适配
1、获取数据库连接
ShardingDataSource每次获取数据库连接,都会新建一个ShardingConnection,这个ShardingConnection会使用真实的数据源获取真实的数据库连接。ShardingConnection会缓存获取过的数据库连接,一次调用中使用多次ShardingConnection,会从缓存中获取真实连接。由于真实连接也是被缓存的,本质上就是数据库连接引用在内存的转移,意义不大。
2、预编译sql
ShardingConnection预编译sql会创建一个ShardingPreparedStatement,在初始化的时候会用ShardingContext里的SQLRouteEngine生成一个PreparedSQLRouter,后续通过PreparedSQLRouter进行sql的解析、路由。
2.3.2、SQL解析和路由