为什么要分表分库?
Mysql数据库使我们后端开发在工作中用到最多的数据库,凭借着开源免费灵活小巧等优势,成为一众互联网的宠儿,但是随着业务的不断增长,Mysql单表数据量成为最大的瓶颈,在优化得当的情况下,一张表最多到上千万数据就不适合继续堆积数据了,这个时候存取数据都会变得非常慢,为了解决业务发展和Mysql的瓶颈之间的矛盾,分表分库技术就诞生了。
常见的分表分库我主要接触了2种,Mycat和ShardingJdbc。
Mycat可以理解为mysql的代理,对开发人员是透明的,主要的分表分库逻辑都在mycat服务里实现,对运维的要求较高。
ShardingJdbc可以理解为jdbc的代理,对开发人员要求较高,主要逻辑都在应用中实现,对运维无压力。
何为分库,何为分表?
分库要解决的问题主要是硬件资源问题,毕竟一台服务器的CPU、内存、网络、硬盘资源是有限的,当并发达到一定级别时,分再多表都是在消耗一台服务器的资源,达到资源瓶颈后,就需要分库来横向拓展服务器资源。
分表主要解决是单表数据量问题,因为数据量达到一定级别后,索引的性能会急剧下降,这个时候我们需要将数据量分开,放到不同的表里,解决这个问题。
那么我们在开发的过程中到底是只用分表还是要分库分表,其实取决于你对性能的要求,这个其实需要视情况而定,如果一台服务器很强悍,分了众多表资源也没有到红线,那不分库也是可以的。如果服务器一般,分了几张表就扛不住了,那就需要横向加机器去分摊硬件压力。
今天主要是大家介绍ShardingJdbc在我们工作中的应用。
简介
如何使用
主要介绍与SpringBoot的集成,我使用的方法主要是用配置来实现,会自动扫描配置分表分库的相关配置,也可以通过在java代码里配置,这个本文没有阐述。
1、添加依赖
<!-- 分表分库中间件 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
2、配置文件
a、通用配置
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.shardingsphere.enabled=true
spring.shardingsphere.props.sql.show=true
b、配置数据源
spring.shardingsphere.datasource.names=m1,m2
spring.shardingsphere.sharding.default-data-source-name=m1
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://139.196.31.219:3306/pay_merchant_account_00?useSSL=false&characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.m1.username=root32
spring.shardingsphere.datasource.m1.password=Root_1234563232
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://121.36.145.42:3306/pay_merchant_account_01?useSSL=false&characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.m2.username=root212
spring.shardingsphere.datasource.m2.password=Root_1234562232
配置了两个主节点
同时配置了default-data-source-name,当不是分表分库的表都使用这个默认数据库
c、如果是有主从读写分离的库配置如下:
# 数据库配置
spring.shardingsphere.datasource.names=m1,m2,s1,s2
spring.shardingsphere.datasource.m1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m1.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.url=jdbc:mysql://139.196.31.219:3306/order_db_1?characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.m1.username=mysql
spring.shardingsphere.datasource.m1.password=mysql
spring.shardingsphere.datasource.m2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m2.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m2.url=jdbc:mysql://139.196.31.229:3306/order_db_2?characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.m2.username=mysql
spring.shardingsphere.datasource.m2.password=mysql
spring.shardingsphere.datasource.s1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s1.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s1.url=jdbc:mysql://139.196.31.239:3306/order_db_1?characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.s1.username=mysql
spring.shardingsphere.datasource.s1.password=mysql
spring.shardingsphere.datasource.s2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.s2.drive-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s2.url=jdbc:mysql://139.196.31.249:3306/order_db_2?characterEncoding=utf8&useSSL=false
spring.shardingsphere.datasource.s2.username=mysql
spring.shardingsphere.datasource.s2.password=mysql
# 读写分离配置
spring.shardingsphere.sharding.master-slave-rules.ds1.name=ds1
spring.shardingsphere.sharding.master-slave-rules.ds1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.ds1.slave-data-source-names=s1
spring.shardingsphere.sharding.master-slave-rules.ds2.name=ds2
spring.shardingsphere.sharding.master-slave-rules.ds2.master-data-source-name=m2
spring.shardingsphere.sharding.master-slave-rules.ds2.slave-data-source-names=s2
配置了两个主节点两个从节点,并按照一主一从的方式配置(注意这里的Mysql主从同步需要自己配置)
d、配置分表分库策略
# 数据库片配置
spring.shardingsphere.sharding.tables.t_order.logic-table=t_order
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{1..2}.t_order_$->{1..2}
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds$->{user_id%2}
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id%2}
这里我们可以配置每个逻辑库具体的分表分库策略,包含如下配置:
表名
实际物理节点
分库字段及分库策略算法(这里用的是行表达式分片策略)
分表字段及分表策略算法(这里用的是行表达式分片策略)
因为我在程序里自己使用了雪花算法来做主键生成,这里并没有配置key-generator的配置。
e、配置绑定表
# 配置绑定表(不配置会产生笛卡尔积)
spring.shardingsphere.sharding.binding-tables[0]=t_order,t_order_desc
我在开发中一般不怎么用join,所以绑定表我并没有用。
f、配置全局表
# 公共表配置
spring.shardingsphere.sharding.broadcast-tables=t_dict
默认会在所有物理节点的数据库都创建这个表,然后每次更新的时候都会更新所有节点的表。
代码版本配置
数据源配置
package chen.huai.jie.sharing.jdbc.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 数据源配置
*
* @author chenhuaijie
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.m1")
public DataSource m1DataSource() {
return genDatasource();
}
@Bean
@ConfigurationProperties(prefix = "spring.m2")
public DataSource m2DataSource() {
return genDatasource();
}
@Bean
@ConfigurationProperties(prefix = "spring.s1")
public DataSource s1DataSource() {
return genDatasource();
}
@Bean
@ConfigurationProperties(prefix = "spring.s2")
public DataSource s2DataSource() {
return genDatasource();
}
private DruidDataSource genDatasource() {
DruidDataSource dataSource = new DruidDataSource();
return dataSource;
}
}
分表分库配置
package chen.huai.jie.sharing.jdbc.config;
import chen.huai.jie.sharing.jdbc.sharding.DefaultDatabasePreciseShardingAlgorithm;
import chen.huai.jie.sharing.jdbc.sharding.DefaultTablePreciseShardingAlgorithm;
import com.google.common.collect.Lists;
import org.apache.shardingsphere.api.config.masterslave.MasterSlaveRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.StandardShardingStrategyConfiguration;
import org.apache.shardingsphere.core.constant.properties.ShardingPropertiesConstant;
import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.*;
/**
* 分库分表配置
*
* @author chenhuaijie
*/
@Configuration
public class ShardingJdbcConfig {
@Autowired
public DataSource m1DataSource;
@Autowired
public DataSource m2DataSource;
@Autowired
public DataSource s1DataSource;
@Autowired
public DataSource s2DataSource;
/**
* 分片数据源
*
* @return
* @throws SQLException
*/
@Bean
public DataSource dataSource() throws SQLException {
//1、 数据源配置
Map<String, DataSource> dataSourceMap = new HashMap<>(4);
dataSourceMap.put("m1", m1DataSource);
dataSourceMap.put("m2", m2DataSource);
dataSourceMap.put("s1", s1DataSource);
dataSourceMap.put("s2", s2DataSource);
//2、分片配置
ShardingRuleConfiguration shardingRuleConfiguration = new ShardingRuleConfiguration();
shardingRuleConfiguration.getMasterSlaveRuleConfigs().addAll(masterSlaveRuleConfigurations());
shardingRuleConfiguration.getTableRuleConfigs().addAll(tableRuleConfigurations());
// 广播表
shardingRuleConfiguration.getBroadcastTables().add("t_dict");
// 绑定表
shardingRuleConfiguration.getBindingTableGroups().add("t_order,t_order_desc");
//3、 属性配置
Properties properties = new Properties();
properties.setProperty(ShardingPropertiesConstant.EXECUTOR_SIZE.getKey(), "10");
properties.setProperty(ShardingPropertiesConstant.MAX_CONNECTIONS_SIZE_PER_QUERY.getKey(), "1024");
properties.setProperty(ShardingPropertiesConstant.SQL_SHOW.getKey(), "true");
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfiguration, properties);
}
/**
* 分表路由配置
*
* @return
*/
private List<TableRuleConfiguration> tableRuleConfigurations() {
List<TableRuleConfiguration> tableRuleConfigurations = new ArrayList<>(1);
TableRuleConfiguration tableRuleConfiguration = new TableRuleConfiguration("t_order", "ds$->{1..2}.t_order_$->{1..2}");
tableRuleConfiguration.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("user_id", new DefaultDatabasePreciseShardingAlgorithm()));
tableRuleConfiguration.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("order_id", new DefaultTablePreciseShardingAlgorithm()));
tableRuleConfigurations.add(tableRuleConfiguration);
return tableRuleConfigurations;
}
/**
* 主从配置
*
* @return
*/
private List<MasterSlaveRuleConfiguration> masterSlaveRuleConfigurations() {
List<MasterSlaveRuleConfiguration> masterSlaveRuleConfigurations = new ArrayList<>(2);
masterSlaveRuleConfigurations.add(new MasterSlaveRuleConfiguration("ds1", "m1", Lists.newArrayList("s1")));
masterSlaveRuleConfigurations.add(new MasterSlaveRuleConfiguration("ds2", "m2", Lists.newArrayList("s2")));
return masterSlaveRuleConfigurations;
}
}
开发自己的分表分库策略
上面的例子我们用的是行表达式分片策略,在实际开发过程中,我们可以通过自定义的策略来替换
替换的配置如下:
spring.shardingsphere.sharding.tables.pay_merchant_account.table-strategy.standard.sharding-column=merchant_no
spring.shardingsphere.sharding.tables.pay_merchant_account.table-strategy.standard.precise-algorithm-class-name=com.merchant.account.sharding.account.PayMerchantAccountTableShardingAlgorithm
几种分片策略:
1、精准分库算法,实现PreciseShardingAlgorithm接口,并重写doSharding()方法,
public class PayMerchantAccountDatabaseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
String merchantNo = shardingValue.getValue();
String columnName = shardingValue.getColumnName();
String logicTableName = shardingValue.getLogicTableName();
String lastNumberString = merchantNo.substring(merchantNo.length() - 1);
Integer lastNumber = NumberUtils.parseNumber(lastNumberString, Integer.class);
if (lastNumber <=5) {
return availableTargetNames.stream().filter(x -> x.endsWith("1")).findAny().get();
} else {
return availableTargetNames.stream().filter(x -> x.endsWith("2")).findAny().get();
}
}
}
这里的availableTargetNames是可用的目标分片,这里其实并不区分分库策略还是分表策略,配置在分库策略那里这里的availableTargetNames就是分库的信息,配置在分表策略那里就是分表的信息。shardingValue是你操作的记录的信息,包含逻辑表名,字段名,字段值,然后通过这个方法从availableTargetNames获取一个可用的值,就是分库或者分表要使用的分片。其实还是比较简单的。因为分库分表算法都是实现同一个接口,所以我们在代码里最好明确区分一下分库还是分表策略。
2、范围分片算法
public class DefaultRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
Set<String> result = new LinkedHashSet<>();
// between and 的起始值
Long lower = shardingValue.getValueRange().lowerEndpoint();
Long upper = shardingValue.getValueRange().upperEndpoint();
// 循环范围计算分库逻辑
for (Long i = lower; i <= upper; i++) {
for (String databaseName : availableTargetNames) {
if (databaseName.endsWith(i % availableTargetNames.size() + "")) {
result.add(databaseName);
}
}
}
return result;
}
}
与精准分片不同的是,这里是通过一个范围来计算返回一个集合,因为范围里的数据可能来一个集合的库或者表。
3、复合分片策略
public class DefaultComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Integer> shardingValue) {
// 得到每个分片健对应的值
Collection<Integer> orderIdValues = this.getShardingValue(shardingValue, "order_id");
Collection<Integer> userIdValues = this.getShardingValue(shardingValue, "user_id");
List<String> shardingSuffix = new ArrayList<>();
// 对两个分片健同时取模的方式分库
for (Integer userId : userIdValues) {
for (Integer orderId : orderIdValues) {
String suffix = userId % 2 + "_" + orderId % 2;
for (String databaseName : availableTargetNames) {
if (databaseName.endsWith(suffix)) {
shardingSuffix.add(databaseName);
}
}
}
}
return shardingSuffix;
}
private Collection<Integer> getShardingValue(ComplexKeysShardingValue<Integer> shardingValues, final String key) {
Collection<Integer> valueSet = new ArrayList<>();
Map<String, Collection<Integer>> columnNameAndShardingValuesMap = shardingValues.getColumnNameAndShardingValuesMap();
if (columnNameAndShardingValuesMap.containsKey(key)) {
valueSet.addAll(columnNameAndShardingValuesMap.get(key));
}
return valueSet;
}
}
支持多分片键
4、行表达式策略
行表达式分片策略(InlineShardingStrategy
),在配置中使用 Groovy
表达式,提供对 SQL语句中的 =
和 IN
的分片操作支持,它只支持单分片健。
行表达式分片策略适用于做简单的分片算法,无需自定义分片算法,省去了繁琐的代码开发,是几种分片策略中最为简单的。
它的配置相当简洁,这种分片策略利用inline.algorithm-expression
书写表达式。
比如:ds-$->{order_id % 2}
表示对 order_id
做取模计算,$
是个通配符用来承接取模结果,最终计算出分库ds-0
··· ds-n
,整体来说比较简单。
事务保证
亲测,通过@Transactional(rollbackFor = Exception.class) 做到了同库和跨库的事务处理。
测试方法,通过两个保存操作,根据分表分库逻辑会保存到两个不同实例的数据库中,异常情况会回滚,正常情况下会写到数据库。