# 数据库拆分
**Repository Path**: cllyl/database-split
## Basic Information
- **Project Name**: 数据库拆分
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2020-11-21
- **Last Updated**: 2020-12-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# MySQL分库分表实战
## 背景分析
#### 问题分析
- 用户请求并发量较大(对数据库的读写请求过多)
- 服务器TPS(Transaction Per Second)、QPS(Query Per Second)上限
- 服务器内存上限
- 服务器IO上限
- 单库数据量过大(表太多、单表数据量太大)导致占用空间大
- 单库对应的服务器的磁盘空间上限
- 单库的处理能力上限
- 单表的处理能力上限
- 单表数据量过大(记录数对、字段多)导致的单表占用空间大
- DML(Data Manipulation Language)数据操作(增、删、改)变慢
- DQL(Data Query Language)数据查询变慢
- DDL(Data Definition Language)添加索引、添加字段变慢
- 数据迁移大问题
#### 主从架构下的读写分离
- 每一个数据库中的数据都一致,每个数据库中的数据都是全部的数据
- 因为分离了读、写操作,将读写操作分离到不同的服务器(数据库)上,一定程度上提高了程序的写能力
- 主要解决了查询性能问题
#### 分库分表之数据分片
**数据节点**:分片之后的表的基本单元
**完整性**:字段完整性、表完整性
**记录数**:表中的记录的数量
分库分表的分片,可以分为水平分片,垂直分片。不论哪种分片方式,都会造成数据库的不完整。
**分库分表之后可能会出现的问题**
1. 分布式事务问题:跨多个库的事务
2. 跨库、跨表关联问题
3. 数据管理、运维问题
- 水平分库
- 将同一个库中的数据,分散到多个数据库中,所有数据库数据总和才是全部的数据
- 垂直分库
- 根据业务对数据库进行拆分,相应的对应用程序根据业务进行分模块,保证每个模块只使用一个数据库
- 水平分表
- 由于一张表中数据量过大,将一张表的数据拆分到多张表中,这些表都在同一个数据库中。可以进行表的union合并。
- 垂直分表
- 由于一张表中的字段过多,或者表中包含不常用的大字段,对表进行垂直拆分,拆分之后,表字段总和才是总字段
## 场景描述
当前项目为学习在项目中进行MySQL数据库水平分库分表。
分片规则(strategy) = 分片键(table_column) + 分片算法(algorithm)
#### 常见分片算法
###### 范围分片(RANGE)
- 根据时间进行分片:年、月、周、天等
- 地域:国家、区域(华南、华北)、省、市、县等
- 大小:1---1000万、1000万---2000万等
###### 等值分片(HASH)
- 根据ID取模:id % 8、id % 10等
- 根据某一个批次:batch = 201、batch=202等
###### 复杂业务场景分析
在单个分片键不能满足业务常见的情况,一般会有一些两种解决方案
- 复合分片键
- 进行数据分片的时候,根据多个分片键进行拆分写入**数据节点**
- 数据冗余
- 全部数据冗余
按照业务场景,对数据记录进行单个分片键分片,对不同的分片键都进行分片存储
- 关联关系冗余
按照业务场景,对多个分片键之间的关联关系进行冗余存储,业务上如果需要用到辅助分片键进行查询的时候,先根据分片键之间的关联关系查询出主分片键,然后根据主分片键进行查询。
#### 水平分库分表结果表现如下
用户端c_order分库分表
- 根据用户ID进行分库
- 根据用户ID对2进行取模,
- 根据订单ID进行分表
- 根据订单ID对2进行取模
- 查询说明
- 只有用户ID:不能定位到唯一数据节点,会出现同一个库内数据查询,多个表之间查询结果集合并
- 只有订单ID:不能定位到唯一数据节点,会出现不同库之间查询,多个库之间的查询结果集合并
- 用户ID+订单ID:可以定位到唯一的数据节点,实现单表查询
## 开发过程
#### sharding-jdbc-exampl模块开发
###### 1. 数据库服务器说明
| 服务器 | 描述信息 |
| ------------------ | ----------------------- |
| 192.168.0.120:3306 | YL-MASTER:分库1的主库 |
| 192.168.0.121:3306 | YL-SLAVE1:分库1的从库1 |
| 192.168.0.122:3306 | YL-SLAVE2:分库1的主库2 |
| 192.168.0.110:3306 | CLL-MASTER:分库2的主库 |
| 192.168.0.111:3306 | CLL-SLAVE1:分库2的从库1 |
| 192.168.0.112:3306 | CLL-SLAVE2:分库2的主库2 |
###### 2. 创建项目
开发项目的时候,通常情况下通用引入。这里是根据使用情况,尽可能少的引入第三方JAR。
> 第三方依赖以及版本说明
```xml
UTF-8
UTF-8
1.8
1.8
2.2.5.RELEASE
1.18.16
8.0.22
1.2.3
4.1.1
```
> pom.xml配置文件
```xml
database-split
com.cll.prototype
1.0-SNAPSHOT
4.0.0
sharding-jdbc-example
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-data-jpa
slf4j-api
org.slf4j
mysql
mysql-connector-java
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
slf4j-api
org.slf4j
snakeyaml
org.yaml
org.springframework.boot
spring-boot-starter-logging
slf4j-api
org.slf4j
org.springframework.boot
spring-boot-starter-test
test
jakarta.activation-api
jakarta.activation
byte-buddy
net.bytebuddy
junit-jupiter-api
org.junit.jupiter
org.projectlombok
lombok
provided
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
1.8
1.8
```
> 启动类
```java
package com.cll.prototype.sharding.jdbc;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 描述信息:
*
* @author CLL
* @version 1.0
* @date 2020/11/21 22:44
*/
@SpringBootApplication
public class ShardingJdbcApplication {
public static void main(String[] args) {
}
}
```
> 核心配置文件application.properties
```properties
# 指定当前使用的辅助配置文件后缀名称
spring.profiles.active=sharding-database-master-slave
# 显示SQL
spring.shardingsphere.props.sql.show=true
```
> 分库分表+读写分离配置文件
```properties
# 分库示例
# 数据源配置
# 指定两个逻辑数据库
spring.shardingsphere.datasource.names=m0,s0,s1,m1,s2,s3
# 分库的主库配置
spring.shardingsphere.datasource.m0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m0.jdbc-url=jdbc:mysql://192.168.0.120:3306/db_split2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=root
spring.shardingsphere.datasource.s0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s0.jdbc-url=jdbc:mysql://192.168.0.121:3306/db_split2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.s0.username=root
spring.shardingsphere.datasource.s0.password=root
spring.shardingsphere.datasource.s1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s1.jdbc-url=jdbc:mysql://192.168.0.122:3306/db_split2?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.s1.username=root
spring.shardingsphere.datasource.s1.password=root
# 分库的主库配置
spring.shardingsphere.datasource.m1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.m1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m1.jdbc-url=jdbc:mysql://192.168.0.110:3306/db_split1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.m1.username=root
spring.shardingsphere.datasource.m1.password=root
spring.shardingsphere.datasource.s2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s2.jdbc-url=jdbc:mysql://192.168.0.111:3306/db_split1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.s2.username=root
spring.shardingsphere.datasource.s2.password=root
spring.shardingsphere.datasource.s3.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.s3.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.s3.jdbc-url=jdbc:mysql://192.168.0.112:3306/db_split1?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong
spring.shardingsphere.datasource.s3.username=root
spring.shardingsphere.datasource.s3.password=root
# 对用户版订单表订单表(c_order)进行分库之后分表配置
# 1. 先进行分库配置,基于用户ID进行分库配置,保证一个用户的订单只会出现在一个库里面
spring.shardingsphere.sharding.tables.c_order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.c_order.database-strategy.inline.algorithm-expression=m$->{user_id % 2}
# 2. 再进行分表配置,基于订单ID进行分表配置,不能保证一个用户的订单只出现在一个表里面
spring.shardingsphere.sharding.tables.c_order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.c_order.table-strategy.inline.algorithm-expression=c_order$->{id % 2}
# 3. 配置真实的数据节点
spring.shardingsphere.sharding.tables.c_order.actual-data-nodes=m${0..1}.c_order${0..1}
# 4. 配置订单表的主键生成策略
spring.shardingsphere.sharding.tables.c_order.key-generator.column=id
spring.shardingsphere.sharding.tables.c_order.key-generator.type=SNOWFLAKE
# 5. 配置分库分表下的读写分离
# 指定当前分库数据源的的名称
#spring.shardingsphere.sharding.master-slave-rules.m0.name=db0
# 指定分库数据源的主节点
spring.shardingsphere.sharding.master-slave-rules.m0.master-data-source-name=m0
# 指定分库数据源的从节点
spring.shardingsphere.sharding.master-slave-rules.m0.slave-data-source-names=s0,s1
spring.shardingsphere.sharding.master-slave-rules.m0.load-balance-algorithm-type=ROUND_ROBIN
spring.shardingsphere.sharding.master-slave-rules.m1.master-data-source-name=m1
spring.shardingsphere.sharding.master-slave-rules.m1.slave-data-source-names=s2,s3
spring.shardingsphere.sharding.master-slave-rules.m1.load-balance-algorithm-type=ROUND_ROBIN
```
> 实体类
```java
package com.cll.prototype.sharding.jdbc.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
/**
* 描述信息:
* 基于用户的订单表
* @author CLL
* @version 1.0
* @date 2020/11/22 15:03
*/
@Data
@Entity
@Table(name = "c_order")
public class COrder {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "is_del")
private Boolean isDel;
@Column(name = "user_id")
private Integer userId;
@Column(name = "company_id")
private Integer companyId;
@Column(name = "publish_user_id")
private Integer publishUserId;
@Column(name = "position_id")
private Long positionId;
@Column(name = "resume_type")
private Integer resumeType;
@Column(name = "status")
private String status;
@Column(name = "create_time")
private Date createTime;
@Column(name = "update_time")
private Date updateTime;
}
```
> 持久层
```java
package com.cll.prototype.sharding.jdbc.repository;
import com.cll.prototype.sharding.jdbc.entity.COrder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface COrderRepository extends JpaRepository {
}
```
> 单元测试类
```java
package com.cll.prototype.sharding.jdbc.repository;
import com.cll.prototype.sharding.jdbc.entity.COrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Example;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.Date;
import java.util.Optional;
import java.util.Random;
/**
* 描述信息:
*
* @author CLL
* @version 1.0
* @date 2020/11/22 14:56
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class COrderRepositoryTest {
private static final Logger logger = LoggerFactory.getLogger(COrderRepositoryTest.class);
@Autowired
private COrderRepository cOrderRepository;
@Test
@Repeat(100)
public void testAddCOrder() {
Random random = new Random();
int userId = random.nextInt(10);
COrder order = new COrder();
order.setIsDel(false);
order.setCompanyId(33333);
order.setPositionId(3242342L);
order.setUserId(userId);
order.setPublishUserId(1111);
order.setResumeType(1);
order.setStatus("AUTO");
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
COrder save = cOrderRepository.save(order);
logger.info("===>>> save c_order result = {}", save);
}
@Test
public void getCOrderByUserId(){
// 分库分表之后,只有用户ID+订单ID才能唯一确定一个表进行查询,才不会出现结果集合并
COrder queryParam = new COrder();
queryParam.setUserId(1);
Example example = Example.of(queryParam);
Optional cOrder = cOrderRepository.findOne(example);
logger.info("===>>> get result = {}", cOrder);
}
@Test
public void getCOrderById(){
// 分库分表之后,只有用户ID+订单ID才能唯一确定一个表进行查询,才不会出现结果集合并
COrder queryParam = new COrder();
queryParam.setId(537303916795133952L);
Example example = Example.of(queryParam);
Optional cOrder = cOrderRepository.findOne(example);
logger.info("===>>> get result = {}", cOrder);
}
@Test
public void getCOrderByIdAndUserId(){
// 分库分表之后,只有用户ID+订单ID才能唯一确定一个表进行查询,才不会出现结果集合并
COrder queryParam = new COrder();
queryParam.setUserId(1);
queryParam.setId(537303916795133952L);
Example example = Example.of(queryParam);
Optional cOrder = cOrderRepository.findOne(example);
logger.info("===>>> get result = {}", cOrder);
}
}
```
> 单元测试日志信息
> 新增数据
```sql
2020-11-22 16:45:04.103 INFO 16228 --- [ main] ShardingSphere-SQL : Logic SQL: insert into c_order (company_id, create_time, is_del, position_id, publish_user_id, resume_type, status, update_time, user_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
2020-11-22 16:45:04.104 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into c_order0 (company_id, create_time, is_del, position_id, publish_user_id, resume_type, status, update_time, user_id, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [33333, 2020-11-22 16:45:04.048, false, 3242342, 1111, 1, AUTO, 2020-11-22 16:45:04.048, 5, 537311750526074880]
2020-11-22 16:45:04.144 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: m0 ::: insert into c_order1 (company_id, create_time, is_del, position_id, publish_user_id, resume_type, status, update_time, user_id, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [33333, 2020-11-22 16:45:04.142, false, 3242342, 1111, 1, AUTO, 2020-11-22 16:45:04.142, 0, 537311750727401473]
2020-11-22 16:45:04.163 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: m1 ::: insert into c_order1 (company_id, create_time, is_del, position_id, publish_user_id, resume_type, status, update_time, user_id, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [33333, 2020-11-22 16:45:04.161, false, 3242342, 1111, 1, AUTO, 2020-11-22 16:45:04.161, 5, 537311750807093249]
2020-11-22 16:45:04.206 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: m0 ::: insert into c_order0 (company_id, create_time, is_del, position_id, publish_user_id, resume_type, status, update_time, user_id, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [33333, 2020-11-22 16:45:04.204, false, 3242342, 1111, 1, AUTO, 2020-11-22 16:45:04.204, 6, 537311750987448320]
```
> 根据主键查询
```sql
2020-11-22 16:45:03.964 INFO 16228 --- [ main] ShardingSphere-SQL : Logic SQL: select corder0_.id as id1_1_, corder0_.company_id as company_2_1_, corder0_.create_time as create_t3_1_, corder0_.is_del as is_del4_1_, corder0_.position_id as position5_1_, corder0_.publish_user_id as publish_6_1_, corder0_.resume_type as resume_t7_1_, corder0_.status as status8_1_, corder0_.update_time as update_t9_1_, corder0_.user_id as user_id10_1_ from c_order corder0_ where corder0_.id=537303916795133952
2020-11-22 16:45:03.965 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: s0 ::: select corder0_.id as id1_1_, corder0_.company_id as company_2_1_, corder0_.create_time as create_t3_1_, corder0_.is_del as is_del4_1_, corder0_.position_id as position5_1_, corder0_.publish_user_id as publish_6_1_, corder0_.resume_type as resume_t7_1_, corder0_.status as status8_1_, corder0_.update_time as update_t9_1_, corder0_.user_id as user_id10_1_ from c_order0 corder0_ where corder0_.id=537303916795133952
2020-11-22 16:45:03.965 INFO 16228 --- [ main] ShardingSphere-SQL : Actual SQL: s2 ::: select corder0_.id as id1_1_, corder0_.company_id as company_2_1_, corder0_.create_time as create_t3_1_, corder0_.is_del as is_del4_1_, corder0_.position_id as position5_1_, corder0_.publish_user_id as publish_6_1_, corder0_.resume_type as resume_t7_1_, corder0_.status as status8_1_, corder0_.update_time as update_t9_1_, corder0_.user_id as user_id10_1_ from c_order0 corder0_ where corder0_.id=537303916795133952
```
> 根据用户ID和主键ID查询
```sql
2020-11-22 16:53:22.505 INFO 13664 --- [ main] ShardingSphere-SQL : Logic SQL: select corder0_.id as id1_1_, corder0_.company_id as company_2_1_, corder0_.create_time as create_t3_1_, corder0_.is_del as is_del4_1_, corder0_.position_id as position5_1_, corder0_.publish_user_id as publish_6_1_, corder0_.resume_type as resume_t7_1_, corder0_.status as status8_1_, corder0_.update_time as update_t9_1_, corder0_.user_id as user_id10_1_ from c_order corder0_ where corder0_.id=537303916795133952 and corder0_.user_id=1
2020-11-22 16:53:22.506 INFO 13664 --- [ main] ShardingSphere-SQL : Actual SQL: s2 ::: select corder0_.id as id1_1_, corder0_.company_id as company_2_1_, corder0_.create_time as create_t3_1_, corder0_.is_del as is_del4_1_, corder0_.position_id as position5_1_, corder0_.publish_user_id as publish_6_1_, corder0_.resume_type as resume_t7_1_, corder0_.status as status8_1_, corder0_.update_time as update_t9_1_, corder0_.user_id as user_id10_1_ from c_order0 corder0_ where corder0_.id=537303916795133952 and corder0_.user_id=1
```