关闭

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

标签: 分库分表sharding-jdbc
12861人阅读 评论(3) 收藏 举报
分类:

分库分表在数据量大的系统中比较常用,解决方案有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逻辑表
        //01两个表是真实的表,t_order是个虚拟不存在的表,只是供使用。如查询所有数据就是select * from t_order就能查完01表的
        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……

3
0
查看评论

springboot多数据源读写分离和主库数据源service层事务控制

读写分离如果撇开框架无非就是实现多个数据源,主库用写的数据源,从库用读的数据源。 因为想研究数据库读写分离和分库分表的设计,所以就自己搭建了一套springboot+druid+mybatis+aop 实现一主多从的设计。 第一步:首先需要自定义数据源的配置项,springboot默认解析的是带前...
  • ggj20ss
  • ggj20ss
  • 2016-05-31 10:57
  • 18289

14.玩转Spring Boot 多数据源

玩转Spring Boot 多数据源 在项目中有的时候需要用到多个数据源,有个问题就是单数据源的事务是没有问题的,多数据源是会存在事务问题的。这里不做事务讲解,事务可以用JTA分布式事务,也可以用MQ。具体不做叙述,接下来说如何实现多数据源并且使用AOP来切换。 本例代码使用Mybatis具体...
  • cl_andywin
  • cl_andywin
  • 2016-12-28 11:33
  • 1853

springboot分表sharding-jdbc-core

1,maven配置 com.dangdang sharding-jdbc-core 1.5.3 2,XbDataSource    用Sharding JDBC封装的DataSource @Component public class Xb...
  • liutaochn
  • liutaochn
  • 2017-09-19 15:05
  • 584

Spring boot+MyBatis+Sharding jdbc配置

Maven配置 MyBatis配置相关 sharding-jdbc分表配置为便于在工作中进行单机多服务部署及简化开发配置,将现有系统迁移至Spring boot。Maven配置       Spring boot相关依赖:web、jdbc、aop相关依赖包(由...
  • hpb21
  • hpb21
  • 2016-11-26 16:52
  • 3908

学习sharding-jdbc(二)之spring+mybatis+sharding-jdbc整合

新建Maven项目 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ...
  • linuu
  • linuu
  • 2016-03-19 10:50
  • 11378

采用shardbatis在springBoot中实现表的水平拆分,整合swagger,mybatis,shardbatis,pagehelper

在最近的一个小项目中,由于会用到一点关于分表的操作,数据库是mysql的,在那个mysql数据库中有一个函数,每天会创建一张数据表作为分表,创建出来的表和原始表的结构不变,只是表名有一点改变。 为此,为了解决这个问题,我在网上搜索了一些关于分表的解决办法,我这里用的是mybatis作为持久层框架,在...
  • puhaiyang
  • puhaiyang
  • 2017-01-13 22:35
  • 2753

spring boot + mybatis 多数据源,mysql服务主从读写分离

在后台服务中对mysql采用主从复制,采用读写分离,这样可以大大减轻mysql的压力,当然这种操作需要实时性要求不高,mysql主从服务存在一定的延时一、项目结构图: 二、application.propertiesspring.jpa.database=mysqldatasource.maste...
  • mjlfto
  • mjlfto
  • 2017-07-09 22:11
  • 763

spring+mybatis+druid数据源+sharding-jdbc分库分表

spring+mybatis+druid数据源+sharding-jdbc分库分表
  • aitangyong
  • aitangyong
  • 2016-11-23 09:38
  • 6831

解读分库分表中间件Sharding-JDBC

3月18日-19日,由CSDN重磅打造的互联网应用架构实战峰会、数据库核心技术与实战应用峰会将在上海举行。作为SDCC 2016(中国软件开发者大会)系列技术峰会的一部分,秉承干货实料(案例)的内容原则,这两场峰会将邀请业内顶尖的架构师和技术专家,共同探讨高可用/高并发系统架构设计、新技术应用、移动...
  • u4110122855
  • u4110122855
  • 2016-02-15 22:10
  • 29462

数据库shard中间件对比,以及sharding-jdbc 实现原理分析

【编者按】数据库分库分表从互联网时代开启至今,一直是热门话题。在NoSQL横行的今天,关系型数据库凭借其稳定、查询灵活、兼容等特性,仍被大多数公司作为首选数据库。因此,合理采用分库分表技术应对海量数据和高并发对数据库的冲击,是各大互联网公司不可避免的问题。 虽然很多公司都致力于开发自己的分库分...
  • shukebai
  • shukebai
  • 2017-03-26 17:26
  • 1837
    个人资料
    • 访问:353113次
    • 积分:4412
    • 等级:
    • 排名:第8123名
    • 原创:100篇
    • 转载:39篇
    • 译文:0篇
    • 评论:148条
    博客专栏
    友情链接
    最新评论