1. 程式人生 > >【分庫分表】sharding-jdbc + spring boot對大表進行分庫分表

【分庫分表】sharding-jdbc + spring boot對大表進行分庫分表

一、前言

      最近小編跳槽了,剛好入職了一家移動網際網路公司。非常的幸運。來新公司後的第一個專案就是對通知服務進行優化改進,其中,一個業務就是當用戶登入的時候,就會登入訪問通知表,根據使用者id載入通知資訊。由於通知量已經上億了,在查詢的時候是非常慢的。

      以前的專案中,使用的mycat做資料庫中介軟體,對資料庫進行分庫分表操作的。這個操作也是挺好的。同事提出了另一種方案——使用Sharding-Jdbc進行分庫分表。

二、Sharding-Jdbc介紹

      Sharding-JDBC是噹噹應用框架ddframe中,從關係型資料庫模組dd-rdb中分離出來的資料庫水平分片框架,是繼dubbox、elastic-job之後ddframe開源的第三個專案。

      Sharding-JDBC直接封裝jdbc協議,可理解為增強版的JDBC驅動,舊程式碼遷移成本幾乎為零,定位為輕量級java框架,使用客戶端直連資料庫,以jar包形式提供服務,無proxy層。

##主要包括以下特點:

  • 可適用於任何基於java的ORM框架,如:JPA、Hibernate、Mybatis、Spring JDBC Template,或直接使用JDBC

  • 可基於任何第三方的資料庫連線池,如:DBCP、C3P0、Durid等

  • 理論上可支援任意實現JDBC規範的資料庫。目前僅支援mysql

  • 分片策略靈活,可支援等號、between、in等多維度分片,也可支援多分片鍵。

  • SQL解析功能完善,支援聚合、分組、排序、limit、or等查詢,並支援Binding Table以及笛卡爾積表查詢。

  • 效能高,單庫查詢QPS為原生JDBC的99.8%,雙庫查詢QPS比單庫增加94%。

架構

這裡寫圖片描述

核心概念

LogicTable:資料分片的邏輯表,對於水平拆分的資料庫(表)來說,是同一類表的總稱。如:訂單資料根據主鍵尾數拆分為10張表,分表是t order 0到t order 9,他們的邏輯表名為t_order。

  • ActualTable:分片資料中真實存在的物理表。

  • DataNode:資料分片的最小單元,由資料來源名稱和資料表組成。如:ds 1.t order_0。

  • DynamicTable:邏輯表和物理表不一定需要在配置規則中靜態配置。如,按照日期分片的場景,物理表的名稱隨著時間的推移會產生變化。

  • BindingTable:指在任何場景下分片規則均一致的主表和子表。例:訂單表和訂單項表,均按照訂單ID分片,則此兩張表互為BindingTable關係。BindingTable關係的多表關聯查詢不會出現笛卡爾積關聯,查詢效率將大大提升。

  • ShardingColumn:分片欄位用於將資料庫(表)水平拆分的欄位。

  • ShardingAlgorithm:分片演算法。

  • SQL Hint:對於分片欄位非SQL決定,而由其他外接條件決定的場景,可使用SQL Hint靈活的注入分片欄位。

三、實戰操作

##3.1 建立資料庫和表

      分別建了兩個庫兩張表:

CREATE DATABASE `user_0`;

CREATE TABLE `user_info_1` (
  `user_id` bigint(19) NOT NULL,
  `user_name` varchar(45) DEFAULT NULL,
  `account` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `user_info_0` (
  `user_id` bigint(19) NOT NULL,
  `user_name` varchar(45) DEFAULT NULL,
  `account` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE DATABASE `user_1`;

CREATE TABLE `user_info_1` (
  `user_id` bigint(19) NOT NULL,
  `user_name` varchar(45) DEFAULT NULL,
  `account` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `user_info_0` (
  `user_id` bigint(19) NOT NULL,
  `user_name` varchar(45) DEFAULT NULL,
  `account` varchar(45) NOT NULL,
  `password` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.2 新增sharding-jdbc依賴

<dependency>
        <groupId>io.shardingjdbc</groupId>
        <artifactId>sharding-jdbc-core</artifactId>
        <version>2.0.3</version>
</dependency>

      新增後,完整的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.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--sharding-jdbc -->
        <!--<dependency>-->
            <!--<groupId>com.dangdang</groupId>-->
            <!--<artifactId>sharding-jdbc-core</artifactId>-->
            <!--<version>1.3.3</version>-->
        <!--</dependency>-->
        <dependency>
            <groupId>io.shardingjdbc</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>2.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.3</version>
        </dependency>
        <dependency>
            <groupId>commons-dbcp</groupId>
            <artifactId>commons-dbcp</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.44</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3.3 整合sharding-jdbc

      當你mybatis調整好了的時候,這個時候就該加sharding jdbc的配置了,接下如果出問題,應該先朝sharding jdbc的方向去考慮.

      從目錄結構中我們可以看到,我有一個config包,我把我的配置都寫在這裡面的,首先,我們先實現我們的分庫分表的策略。

      分庫策略的類,DemoDatabaseShardingAlgorithm:

package com.example.demo.config;

import com.google.common.collect.Range;
import io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;

import java.util.Collection;
import java.util.LinkedHashSet;
public class DemoDatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        for (String each : collection) {
            if (each.endsWith(Long.parseLong(preciseShardingValue.getValue().toString()) % 2+"")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }
}

      使用io.shardingjdbc,就應該實現PreciseShardingAlgorithm介面,然後實現doSharding方法,對應SQL中的=, IN,還有RangeShardingAlgorithm介面中,對應SQL中的BETWEEN AND,因為我只需要=,in操作,所以只實現了PreciseShardingAlgorithm介面,你如果都需要,你可以都實現(千萬不要忽略了一個類可以實現多個介面)。
如果你使用的當當網的sharding jdbc,那麼你需要實現SingleKeyDatabaseShardingAlgorithm這個介面,實現其中的三個方法,我註釋到的部分就是原來我用噹噹網的sharding jdbc的實現。

      分表策略的類,DemoTableShardingAlgorithm:

package com.example.demo.config;

import com.google.common.collect.Range;
import io.shardingjdbc.core.api.algorithm.sharding.PreciseShardingValue;
import io.shardingjdbc.core.api.algorithm.sharding.standard.PreciseShardingAlgorithm;

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

//public class DemoTableShardingAlgorithm implements SingleKeyTableShardingAlgorithm<Long> {
public class DemoTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        for (String each : collection) {
            if (each.endsWith(Long.parseLong(preciseShardingValue.getValue().toString()) % 2+"")) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }
}

      與分庫的步驟一致,也是需要實現PreciseShardingAlgorithm和RangeShardingAlgorithm兩個介面的類。剩下的就是最重要的部分,sharding jdbc的配置:

      DataSourceConfig:

package com.example.demo.config;


import io.shardingjdbc.core.api.config.ShardingRuleConfiguration;
import io.shardingjdbc.core.api.config.TableRuleConfiguration;
import io.shardingjdbc.core.api.config.strategy.StandardShardingStrategyConfiguration;
import io.shardingjdbc.core.jdbc.core.datasource.ShardingDataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;

@Configuration
@MapperScan(basePackages = "com.example.demo.mapper", sqlSessionTemplateRef = "testSqlSessionTemplate")
public class DataSourceConfig {

    /**
     * 配置分庫分表策略
     * 
     * @return
     * @throws SQLException
     */
    @Bean(name = "shardingDataSource")
    DataSource getShardingDataSource() throws SQLException {
        ShardingRuleConfiguration shardingRuleConfig;
        shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTableRuleConfigs().add(getUserTableRuleConfiguration());
        shardingRuleConfig.getBindingTableGroups().add("user_info");
        shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", DemoDatabaseShardingAlgorithm.class.getName()));
        shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", DemoTableShardingAlgorithm.class.getName()));
        return new ShardingDataSource(shardingRuleConfig.build(createDataSourceMap()));
    }

  
    /**
     * 設定表的node
     * @return
     */
    @Bean
    TableRuleConfiguration getUserTableRuleConfiguration() {
        TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration();
        orderTableRuleConfig.setLogicTable("user_info");
        orderTableRuleConfig.setActualDataNodes("user_${0..1}.user_info_${0..1}");
        orderTableRuleConfig.setKeyGeneratorColumnName("user_id");
        return orderTableRuleConfig;
    }
    

    /**
     * 需要手動配置事務管理器
     *
     * @param shardingDataSource
     * @return
     */
    @Bean
    public DataSourceTransactionManager transactitonManager(DataSource shardingDataSource) {
        return new DataSourceTransactionManager(shardingDataSource);
    }

    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(DataSource shardingDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(shardingDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return bean.getObject();
    }

    @Bean
    @Primary
    public SqlSessionTemplate testSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    private Map<String, DataSource> createDataSourceMap() {
        Map<String, DataSource> result = new HashMap<>();
        result.put("user_0", createDataSource("user"));
        result.put("user_1", createDataSource("user_1"));
        return result;
    }

    private DataSource createDataSource(final String dataSourceName) {
        BasicDataSource result = new BasicDataSource();
        result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
        result.setUrl(String.format("jdbc:mysql://localhost:3306/%s", dataSourceName));
        result.setUsername("root");
        result.setPassword("123456");
        return result;
    }
}

      當你遇到一個問題:意思差不多是,需要一個數據源,但是發現好幾個,你可以在getShardingDataSource()這個方法上添加註解:@Primary,設定預設資料來源,還有一個重中之重的部分,在Applicatian這個啟動類中:加上註解,主要是為了防止程式碼的自動配置。

@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@EnableTransactionManagement(proxyTargetClass = true)
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@EnableTransactionManagement(proxyTargetClass = true)
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

四、小結

      說實話,sharding-jdbc的體驗和mycat是一樣的, 都感覺不到分了很多表。不同的是,sharding-jdbc的分庫分表更加的簡單,不是資料庫中介軟體。操作更加的方便。