MySQL分库分表实战
为什么要分库分表
在大型网站中,当用户量以及用户产生的业务数据量达到单库单表性能极限时,为了支撑业务可持续发展,对于重要的核心业务必然要进行分库分表来存储业务数据。
对于非核心业务产生的大量数据,例如爬虫爬取的信息,论坛产生的数据等,可以考虑把数据保存在像MongoDB这样的NoSQL存储里面,这些存储会自动分片存储业务数据,不需要业务逻辑进行干预。
分库分表的缺点
分库分表之后实际会产生一系列问题,对于每种问题我们需要针对性进行解决,保障系统稳定运行。
下面就分库分表会产生的问题进行列举:
- 数据的获取需要通过改写路由去多个分片获取数据,然后进行聚合。对于某些SQL需要全库表路由来获取数据,有的SQL甚至需要把SQL语句改写成笛卡尔积去获取分片数据,这中间会产生大量数据库连接造成数据库资源浪费。
- 分库分表之后需要分布式事务来保证数据一致性。对于不同业务可以采用不同的分布式事务实现方式来对数据一致性提供不同的保障级别,这其实是一种业务的权衡。
- 对于分页数据的获取,最好是采用异构数据到ElasticSearch或者其他分布式存储中,既能提供高性能的检索,也能避免由于分库分表导致分页数据获取困难的问题。当然是用汇总表对业务数据汇总,提供一个兜底方案也是很有必要的。
什么是分库分表
分库分表就是把用户数据进行拆分,存储到不同的数据库的不同数据表中。
分库分表主要是用来承载用户的写负载,用户数据在单库单表存储量太大,会导致用户读取阻塞,进而导致用户无法写入。
对于读负载可以通过读写分离,异构到ElasticSearch等方案进行很好的解决。
对于写负载只能通过分库分表的方案进行解决。
怎么进行分库分表
分库分库的第一个要点是如何拆分数据。
大体有以下几种拆分方案,每种方案都有优点也有缺点:
- 按时间段进行拆分
- 取模法
- 查表法
一般业务中使用的都是取模法,因为通过取模法,业务数据可以均匀分布在数据库中,用户请求也可以均匀的请求不同的数据库,可以避免热点表,但是对于不同用户的数据的交互或者联合查询就需要进行多次查询。
当然在某些业务场景下使用其他拆分方案可能会更好,例如历史日志,可以采用日志数据按照时间拆分,进行归档的方式来进行分库分表。
分库分表的第二个要点就是拆分之后SQL如何路由,如何执行。
对于这一点一般采用现有的框架来进行实现,如MyCAT,Sharding-Jdbc等,他们基于不同的模式为我们封装好了拆分之后SQL路由和执行的细节。
其中MyCAT是使用代理服务器的方式实现,Sharding-Jdbc是使用嵌入应用系统中直接调用数据库的方式实现的。
实践中可以根据不同项目组的情况使用不同的方案。
分库分表实践
下面介绍通过Sharding-Jdbc来实现分库分表。通过Sharding-Jdbc可以很方便地实现数据库的拆分,SQL自动改写,路由,请求结果归并等具体的数据处理细节。
下面介绍一个简单的分库分表的实现流程,更多实现细节和使用方案可以参考官方文档。
- 引入Jar包
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>4.1.1</version>
</dependency>
- 配置数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
*// 配置第 1 个数据源*
DruidDataSource dataSource1 = new DruidDataSource();
dataSource1.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/continuous?useUnicode=true&characterEncoding=utf-8&useSSL=false");
dataSource1.setUsername("root");
dataSource1.setPassword("12345678");
dataSourceMap.put("ds0", dataSource1);
*// 配置第 2 个数据源*
DruidDataSource dataSource2 = new DruidDataSource();
dataSource2.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource2.setUrl("jdbc:mysql://localhost:3306/continuous2?useUnicode=true&characterEncoding=utf-8&useSSL=false");
dataSource2.setUsername("root");
dataSource2.setPassword("12345678");
dataSourceMap.put("ds1", dataSource2);
- 配置分库分表的方案
*// 配置 t_order 表规则*
TableRuleConfiguration orderTableRuleConfig =
new TableRuleConfiguration("t_order", "ds0.order0, ds1.order1");
*// 配置分库策略*
orderTableRuleConfig.setDatabaseShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id", databaseShardingAlgorithm));
*// 配置分表策略*
orderTableRuleConfig.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id", tableShardingAlgorithm));
*// 配置业务主键ID生成方案*
orderTableRuleConfig.setKeyGeneratorConfig(
new KeyGeneratorConfiguration("SNOWFLAKE", "user_id"));
*// 配置分片规则*
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);
*// 设置属性*
Properties properties = new Properties();
properties.put("sql.show", true);
*// 创建 ShardingSphereDataSource*
return ShardingDataSourceFactory.*createDataSource*(dataSourceMap, shardingRuleConfig, properties);
- 基于业务进行不同的定制
对于不同业务的查询,有时需要使用Hint的方式进行分库分表。
最佳实践和好的资料
文章中说明什么情况下分库,什么情况下分表。
数据量大,就分表;并发高,就分库。
还详细比较了三种数据库拆分方案的细节。
- Hit分片方式的使用,有几篇文章写很不错
- 在这篇文章有使用查表法对于非分片key查询数据的支持,很值得借鉴。