分库分表在数据量大的系统中比较常用,解决方案有Cobar,TDDL等,这次主要是拿当当网开源的Sharding-JDBC来做个小例子。
它的github地址为:​​​https://github.com/dangdangdotcom/sharding-jdbc​​​ 简介:
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等。
理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle,SQLServer,DB2等数据库的计划。
Sharding-JDBC定位为轻量级java框架,使用客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。SQL解析使用Druid解析器,是目前性能最高的SQL解析器。
具体的介绍可以上它的文档那里看看,简单归纳起来就是,它是一个增强版的JDBC,对使用者透明,逻辑代码什么的都不用动,它来完成分库分表的操作;然后它还支持分布式事务(不完善)。看起来很不错的样子。
下面用个小例子来看一下分库分表的使用。使用的是SpringBoot,JPA(hibernate),druid连接池。

使用Idea新建个Spring Boot项目

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.tianyalei</groupId>
<artifactId>shardingtest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>shardingtest</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>


</project>

目前最新的sharding jdbc是1.4.2,里面druid版本是1.0.12,这个是因为

1 SpringBoot 使用sharding jdbc进行分库分表_spring


虽然没试验,但是还是按他们的要求来吧。

在官方文档里能看到,配置sharding jdbc有三种方式,可以用java代码配置,YAML配置和Spring xml配置http://dangdangdotcom.github.io/sharding-jdbc/02-guide/configuration/

不同的配置需要引入不同的maven依赖。这里我采用java代码配置的方式,最简单。

在application.yml里配置一下jpa

如下:

spring:
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: none

指明jpa的数据库为mysql,hibernate的ddl-auto方式为none。
为毛为none?而不是update之类的,update的话hibernate就能自动帮我们建表了。
是因为,既然是分库分表,表名就是Order1,Order2之类的,hibernate只能建立个Order映射,是建不出来多个分表的,所以表就自己建。

建domain

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
* Created by wuwf on 17/4/19.
*/
@Entity
@Table(name = "t_order")
public class Order {
@Id
private Long orderId;

private Long userId;

public Long getOrderId() {
return orderId;
}

public void setOrderId(Long orderId) {
this.orderId = orderId;
}

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}
}

一个订单表,表名为t_order,里面有个主键orderId和userId,这次userId还没用上,以后用多对一关联时再用。

可以看到,只在orderId上加了@Id2而没有加@GeneratedValue(strategy = GenerationType.AUTO)的主键生成策略,mysql一般用自增。

为什么不加呢?因为不能加,你分表了,主键如果还是自增,就会出现主键重复!!重复了,程序就不能识别数据唯一性了。

1 SpringBoot 使用sharding jdbc进行分库分表_分库分表_02


所以这个主键需要由我们自己来创建生成。

再创建一个最基本的repository

import com.tianyalei.domain.Order;
import org.springframework.data.repository.CrudRepository;

/**
* Created by wuwf on 17/4/19.
*/
public interface OrderRepository extends CrudRepository<Order, Long> {
}

分库分表的配置

package com.tianyalei.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.dangdang.ddframe.rdb.sharding.api.ShardingDataSourceFactory;
import com.dangdang.ddframe.rdb.sharding.api.rule.DataSourceRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.ShardingRule;
import com.dangdang.ddframe.rdb.sharding.api.rule.TableRule;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.DatabaseShardingStrategy;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.TableShardingStrategy;
import com.mysql.jdbc.Driver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
* Created by wuwf on 17/4/19.
*/
@Configuration
public class DataSourceConfig {
@Bean
public DataSource getDataSource() {
return buildDataSource();
}

private DataSource buildDataSource() {
//设置分库映射
Map<String, DataSource> dataSourceMap = new HashMap<>(2);
//添加两个数据库ds_0,ds_1到map里
dataSourceMap.put("ds_0", createDataSource("ds_0"));
dataSourceMap.put("ds_1", createDataSource("ds_1"));
//设置默认db为ds_0,也就是为那些没有配置分库分表策略的指定的默认库
//如果只有一个库,也就是不需要分库的话,map里只放一个映射就行了,只有一个库时不需要指定默认库,但2个及以上时必须指定默认库,否则那些没有配置策略的表将无法操作数据
DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "ds_0");

//设置分表映射,将t_order_0和t_order_1两个实际的表映射到t_order逻辑表
//0和1两个表是真实的表,t_order是个虚拟不存在的表,只是供使用。如查询所有数据就是select * from t_order就能查完0和1表的
TableRule orderTableRule = TableRule.builder("t_order")
.actualTables(Arrays.asList("t_order_0", "t_order_1"))
.dataSourceRule(dataSourceRule)
.build();

//具体分库分表策略,按什么规则来分
ShardingRule shardingRule = ShardingRule.builder()
.dataSourceRule(dataSourceRule)
.tableRules(Arrays.asList(orderTableRule))
.databaseShardingStrategy(new DatabaseShardingStrategy("user_id", new ModuloDatabaseShardingAlgorithm()))
.tableShardingStrategy(new TableShardingStrategy("order_id", new ModuloTableShardingAlgorithm())).build();

DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);

return dataSource;
}

private static DataSource createDataSource(final String dataSourceName) {
//使用druid连接数据库
DruidDataSource result = new DruidDataSource();
result.setDriverClassName(Driver.class.getName());
result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
result.setUsername("root");
result.setPassword("");
return result;
}
}

我们没有在配置文件里指明数据库的DataSource,就需要在java代码里来配置DataSource。普通情况不分库时,只需要在getDataSource方法直接返回createDataSource方法就行了,里面指定了使用druidDataSource。
现在分库了,我们就要用Sharding JDBC封装的DataSource了,由它来接管数据库连接。
也就是DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
可以看到,Sharding JDBC封装的DataSource主要是需要构造一个shardingRule参数。
这个类也主要就是构造这个规则,注释里面写的比较清晰了。差不多流程就是创建个Map<String, DataSource>赋给DataSourceRule,然后创建1个或多个TableRule,主要是配置多表映射的逻辑表,然后生成ShardingRule,参数有dataSourceRule、1或多个TableRule(在.tableRules(Arrays.asList(orderTableRule, XXXXTableRule))里添加)、然后是databaseShardingStrategy来指明按照什么字段的什么规则来决定使用哪个库、tableShardingStrategy同上。
譬如库的选择的话,我们可以使用user_id来做个算法来分流,使用user_id % 2 = 0的放到库1,其他的放库2。表的话可以用order_id % 2 = 0的来划分到不同表。规则可以随意指定,比较常见是按查询规则,譬如经常查询某个用户的所有订单,那就可以把user_id相等的放到一个表里去。

具体的策略算法

在上面的代码里,分别使用了ModuloDatabaseShardingAlgorithm和ModuloTableShardingAlgorithm来分别指定库和表的分流策略。
现在来看看这两个类。

import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.database.SingleKeyDatabaseShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
* Created by wuwf on 17/4/19.
*/
public class ModuloDatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<Long> {

@Override
public String doEqualSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
for (String each : availableTargetNames) {
if (each.endsWith(shardingValue.getValue() % 2 + "")) {
return each;
}
}
throw new IllegalArgumentException();
}

@Override
public Collection<String> doInSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
for (Long value : shardingValue.getValues()) {
for (String tableName : availableTargetNames) {
if (tableName.endsWith(value % 2 + "")) {
result.add(tableName);
}
}
}
return result;
}

@Override
public Collection<String> doBetweenSharding(Collection<String> availableTargetNames,
ShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
Range<Long> range = shardingValue.getValueRange();
for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
for (String each : availableTargetNames) {
if (each.endsWith(i % 2 + "")) {
result.add(each);
}
}
}
return result;
}

}
import com.dangdang.ddframe.rdb.sharding.api.ShardingValue;
import com.dangdang.ddframe.rdb.sharding.api.strategy.table.SingleKeyTableShardingAlgorithm;
import com.google.common.collect.Range;

import java.util.Collection;
import java.util.LinkedHashSet;

/**
* Created by wuwf on 17/4/19.
*/
public final class ModuloTableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {

/**
* select * from t_order from t_order where order_id = 11
* └── SELECT * FROM t_order_1 WHERE order_id = 11
* select * from t_order from t_order where order_id = 44
* └── SELECT * FROM t_order_0 WHERE order_id = 44
*/
public String doEqualSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
for (String each : tableNames) {
if (each.endsWith(shardingValue.getValue() % 2 + "")) {
return each;
}
}
throw new IllegalArgumentException();
}

/**
* select * from t_order from t_order where order_id in (11,44)
* ├── SELECT * FROM t_order_0 WHERE order_id IN (11,44)
* └── SELECT * FROM t_order_1 WHERE order_id IN (11,44)
* select * from t_order from t_order where order_id in (11,13,15)
* └── SELECT * FROM t_order_1 WHERE order_id IN (11,13,15)
* select * from t_order from t_order where order_id in (22,24,26)
* └──SELECT * FROM t_order_0 WHERE order_id IN (22,24,26)
*/
public Collection<String> doInSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>(tableNames.size());
for (Long value : shardingValue.getValues()) {
for (String tableName : tableNames) {
if (tableName.endsWith(value % 2 + "")) {
result.add(tableName);
}
}
}
return result;
}

/**
* select * from t_order from t_order where order_id between 10 and 20
* ├── SELECT * FROM t_order_0 WHERE order_id BETWEEN 10 AND 20
* └── SELECT * FROM t_order_1 WHERE order_id BETWEEN 10 AND 20
*/
public Collection<String> doBetweenSharding(final Collection<String> tableNames, final ShardingValue<Long> shardingValue) {
Collection<String> result = new LinkedHashSet<>(tableNames.size());
Range<Long> range = shardingValue.getValueRange();
for (Long i = range.lowerEndpoint(); i <= range.upperEndpoint(); i++) {
for (String each : tableNames) {
if (each.endsWith(i % 2 + "")) {
result.add(each);
}
}
}
return result;
}
}

主要看doEqualSharding方法(譬如select * from t_order where user_id = 11就是equal),availableTargetNames就是所有的库名(ds_0,ds_1),shardingValue就是在DataSourceConfig里指定的user_id,代码就是如果user_id是偶数就算到ds_0数据库,其他的就放ds_1数据库。而另外的两个方法,doIn和doBetween是用在如where user_id in (1,23,7)和where user_id between(1, 6)。
table的策略和db的策略是一样的,算法可以自己定。
上面两个都是实现的SingleKeyShardingAlgorithm,也就是单列策略,也可以使用多列策略,譬如user_id 和 order_id同时符合某个条件的,分到哪个表。
new TableShardingStrategy(Arrays.asList(“order_id”, “order_type”, “order_date”), new MultiKeyShardingAlgorithm()))

上面都是根据一列或多列来决定分库分表策略,官方还提供了不根据列的路由策略,参考 强制路由。​​http://dangdangdotcom.github.io/sharding-jdbc/02-guide/hint-sharding-value/​

创建DB和表

目标:
db0
├── t_order_0 user_id为偶数 order_id为偶数
├── t_order_1 user_id为偶数 order_id为奇数
db1
├── t_order_0 user_id为奇数 order_id为偶数
├── t_order_1 user_id为奇数 order_id为奇数
先来创建两个db,ds_0和ds_1。然后分别在每个库里建表t_order_0和t_order_1。
建标语句:DROP TABLE IF EXISTS ​​​t_order_0​​​;
CREATE TABLE ​​​t_order_0​​​ (
​​​order_id​​​ bigint(20) NOT NULL,
​​​user_id​​​ bigint(20) NOT NULL,
PRIMARY KEY (​​​order_id​​​)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
再改个表名为1,然后分别在两个库里都执行一下。切记不能勾选auto_increment.
下面写个controller来试试添加和查询数据

import com.tianyalei.domain.Order;
import com.tianyalei.repository.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Created by wuwf on 17/4/19.
*/
@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderRepository orderRepository;

@RequestMapping("/add")
public Object add() {
for (int i = 0; i < 10; i++) {
Order order = new Order();
order.setUserId((long) i);
order.setOrderId((long) i);
orderRepository.save(order);
}
for (int i = 10; i < 20; i++) {
Order order = new Order();
order.setUserId((long) i + 1);
order.setOrderId((long) i);
orderRepository.save(order);
}
return "success";
}

@RequestMapping("query")
private Object queryAll() {
return orderRepository.findAll();
}
}

启动Application,然后在浏览器访问http://localhost:8080/order/add

然后查看数据库,可以看到两个库共4个表,数据刚好是按配置的规则均匀分布的。

1 SpringBoot 使用sharding jdbc进行分库分表_bc_03


这样我们就完成了分库分表的操作。这里主键order_id我们是手工指定的,不能用mysql自增的那个。而在实际应用中,主键Id的生成唯一性是个比较麻烦的问题。

分布式主键

先看官方的说法,​​http://dangdangdotcom.github.io/sharding-jdbc/02-guide/id-generator/​​​ 传统数据库软件开发中,主键自动生成技术是基本需求。而各大数据库对于该需求也提供了相应的支持,比如MySQL的自增键。 对于MySQL而言,分库分表之后,不同表生成全局唯一的Id是非常棘手的问题。因为同一个逻辑表内的不同实际表之间的自增键是无法互相感知的, 这样会造成重复Id的生成。我们当然可以通过约束表生成键的规则来达到数据的不重复,但是这需要引入额外的运维力量来解决重复性问题,并使框架缺乏扩展性。
目前有许多第三方解决方案可以完美解决这个问题,比如UUID等依靠特定算法自生成不重复键,或者通过引入Id生成服务等。 但也正因为这种多样性导致了Sharding-JDBC如果强依赖于任何一种方案就会限制其自身的发展。
基于以上的原因,最终采用了以JDBC接口来实现对于生成Id的访问,而将底层具体的Id生成实现分离出来。
其实最终要解决的问题就是各库各表中的数据,主键不能重复。官方提供的statement什么的没看懂,我就直接用它提供的通用主键生成器来生成主键了。其实就是提供了一个类,这个类能生成一个保证不重复的Long型数字,我们就用它做主键。
添加pom依赖:

<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-self-id-generator</artifactId>
<version>${sharding-jdbc.version}</version>
</dependency>

该生成器生成的数据为64bit的long型数据。 在数据库中应该用大于等于64bit的数字类型的字段来保存该值,比如在MySQL中应该使用BIGINT。

唯一主键使用

在DataSourceConfig里加个bean

@Bean
public IdGenerator getIdGenerator() {
return new CommonSelfIdGenerator();
}

采用CommonSelfIdGenerator来生成IdGenerator。然后在controller里使用它生成orderId即可。

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderRepository orderRepository;
@Autowired
private IdGenerator idGenerator;

@RequestMapping("/add")
public Object add() {
// for (int i = 0; i < 10; i++) {
// Order order = new Order();
// order.setUserId((long) i);
// order.setOrderId((long) i);
// orderRepository.save(order);
// }
// for (int i = 10; i < 20; i++) {
// Order order = new Order();
// order.setUserId((long) i + 1);
// order.setOrderId((long) i);
// orderRepository.save(order);
// }
Order order = new Order();
order.setUserId(1L);
order.setOrderId(idGenerator.generateId().longValue());
orderRepository.save(order);
return "success";
}

@RequestMapping("query")
private Object queryAll() {
return orderRepository.findAll();
}
}

再访问add,看看生成的orderId。以后就可以靠这个类生成不重复ID了。

本节Over……