【分庫分表】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的分庫分表更加的簡單,不是資料庫中介軟體。操作更加的方便。