海量数据进行分库分表及其实践

项目github地址:https://github.com/wushidong125/sharding-db

背景情况

用户表达到了 几千万级别,在做很多操作都比较吃力,.所以,考虑对其进行分表、分库

常用的切分方案

可以分为两种切分模式。一种是按照功能将表划分成不同的表来切分到不同的数据库之上,这种切可以称之为数据的垂直切分;另外一种则是根据将一个表中的数据按照某种条件【id取模、hash】拆分到多台数据库上面,这种切分称之为数 据的水平切分。

优缺点及应用场景:

垂直切分

即业务切分 
下面来分析下垂直切分的优缺点: 
优点: 
 拆分后业务清晰,拆分规则明确。 
 系统之间整合或扩展容易。 
 数据维护简单。 
缺点: 
 部分业务表无法 join,只能通过接口方式解决,提高了系统复杂度。 
 受每种业务不同的限制存在单库性能瓶颈,不易数据扩展跟性能提高。 
 事务处理复杂。 
由于垂直切分是按照业务的分类将表分散到不同的库,所以有些业务表会过于庞大,存在单库读写与存储瓶 
颈,所以就需要水平拆分来做解决。

水平切分

相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中 
包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中 

几种典型的分片规则包括: 
 按照用户 ID 求模,将数据分散到不同的数据库,具有相同数据用户的数据都被分散到一个库中。 
 按照日期,将不同月甚至日的数据分散到不同的库中。 
 按照某个特定的字段求摸,或者根据特定范围段分散到不同的库中

优点: 
 拆分规则抽象好,join 操作基本可以数据库做。 
 不存在单库大数据,高并发的性能瓶颈。 
 应用端改造较少。 
 提高了系统的稳定性跟负载能力。 
缺点: 
 拆分规则难以抽象。 
 分片事务一致性难以解决。 
 数据多次扩展难度跟维护量极大。 
 跨库 join 性能较差。


---------------------------------------------------------------------------------------------------------------------------

实践:

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

参考: http://www.cnblogs.com/yeyinfu/p/7317044.html

          http://blog.csdn.net/mingover/article/details/71108852



  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值