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

时间:2022-06-17
本文章向大家介绍1 SpringBoot 使用sharding jdbc进行分库分表,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

分库分表在数据量大的系统中比较常用,解决方案有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,这个是因为

虽然没试验,但是还是按他们的要求来吧。 在官方文档里能看到,配置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一般用自增。 为什么不加呢?因为不能加,你分表了,主键如果还是自增,就会出现主键重复!!重复了,程序就不能识别数据唯一性了。

所以这个主键需要由我们自己来创建生成。 再创建一个最基本的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

具体的策略算法

在上面的代码里,分别使用了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个表,数据刚好是按配置的规则均匀分布的。

这样我们就完成了分库分表的操作。这里主键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……