分库分表
分库分表包括分库和分表两个维度,每个维度都可以采用两种拆分思路,即:垂直拆分和水平拆分。
垂直拆分
垂直分表
垂直分表指的是将一个表按照字段分成多张表,每个表存储其中一部分字段。
如图所示:
实现上,通常可以把大字段数据或热度较低的数据放在一张独立的表中,将经常需要组合查询的列放在一张表中,这也可以认为是分表操作的一种表现形式。
通过垂直分表能得到一定程度的性能提升,但数据毕竟仍然位于同一个数据库中,也就是把操作范围限制在一台服务器上,每个表还是会竞争同一台服务器中的CPU、内存、网络IO等资源。为此,在垂直分表后,可以进一步引入垂直分库。
垂直分库
垂直分表后,用户信息与商品、订单等信息仍然在同一台服务器中。基于垂直分库思想,可以将用户相关的数据表单独拆分出来,存放到一个独立的数据库中。
如图所示:
垂直分库指的是按照业务对数据表进行分类,将不同业务的数据表分布到不同的数据库中,每个数据库可以位于不同的服务器上,其核心理念是专库专用。垂直分库很大程度上取决于业务的规划和系统边界的划分。如:独立拆分用户数据需要考虑到系统用户体系与其他业务模块之间的关联关系,而不是简单地创建一个用户库。在高并发场景下,垂直分库能够在一定程度上提升IO访问效率和数据库连接数,并降低单机硬件资源的瓶颈。
水平拆分
水平分库
垂直拆分尽管实现起来比较简单,但并不能解决单表数据量过大的问题。因此,往往需要在垂直拆分的基础上再进行水平拆分。如:可以对用户库中的用户信息按照用户ID进行取模,将用户数据分别存储在不同的数据库中,即:水平分库。
如图所示:
水平分库指的是将数据表中的数据按一定规则拆分到不同的数据库中,每个数据库可以位于不同的服务器上。这种方案能够解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,数据的访问需要额外的路由工作,因此大大提升了系统复杂度。
水平分库拆分规则实际上是一系列算法,常见的拆分规则有:
- 取模算法。如:按照用户ID进行取模、通过表的一列或多列字段进行hash求值来取模。
- 范围限定算法。如:可以采用按年份、按时间等策略路由到目标数据库或表。
- 预定义算法。如:事先规划好具体库或表的数量,然后直接路由到指定库或表中。
水平分表
按照水平分库的思路,还可以对用户库中的用户表进行水平拆分。水平分表指的是在同一个数据库内,将同一个表中的数据按一定规则拆到多个表中。
如图所示:
分库分表方案
分库分表可以抽象成一个核心概念:分片(Sharding)。无论是分库还是分表都是将数据划分为不同的数据分片,存储到不同的目标对象中。具体的分片方式涉及实现分库分表的不同解决方案。
常见的数据分片方案有三大类型:客户端分片、代理服务器分片、分布式数据库。
客户端分片
客户端分片指的是在数据库的客户端实现分片规则。这种方式将分片处理逻辑进行前置,客户端管理和维护着所有的分片逻辑,并决定每次SQL执行所对应的目标数据库和数据表。
客户端分片主要有应用层分片和重写JDBC协议两种表现形式。
应用层分片
应用层分片指的是在应用程序中直接维护着分片规则和分片逻辑。
如图所示:
在具体实现上,应用层分片通常会将分片规则的处理逻辑打包成一个公共JAR包,业务开发人员只需要在代码工程中引入该JAR包。这种方案由于没有独立的服务器组件,因此不需要专门维护某一个具体的中间件。这种直接在业务代码中嵌入分片组件的方案存在明显缺陷:
- 由于分片逻辑嵌入到业务代码中,业务开发人员在理解业务的基础上还需要掌握分片规则的处理方式,增加了开发和维护成本。
- 使用该方案一旦出现问题,只能依赖业务开发人员通过分析代码来查找原因,无法将这部分工作抽离出来由专门的中间件团队完成。
重写JDBC协议
重写JDBC协议指的是将分片规则从业务代码中剥离出来,嵌入到JDBC协议中。这样业务开发人员就可以使用与JDBC规范完全兼容的一套API来操作数据库,从而实现对业务代码的零侵入。
如图所示:
重写JDBC协议方案的分片操作对业务而言完全透明,在一定程度上实现了业务开发人员与数据库中间件团队在职责上的分离。业务开发人员只需要理解JDBC规范就可以完成分库分表,降低了开发难度和代码维护成本。
典型的客户端分片中间件有:阿里巴巴的TDDL、ShardingSphere。
代理服务器分片
代理服务器分片方案是采用代理机制,在应用层和数据库层之间添加一个代理层,将分片规则集中维护在该代理层中,并对外提供与JDBC兼容的API。应用层的业务开发人员无需关心具体的分片规则,只需要完成业务逻辑的实现即可。
如图所示:
代理服务器分片的优点是业务开发人员无需维护分片规则,只需专注于业务逻辑实现;缺点是在应用和数据库之间添加了一层代理,会对网络传输性能产生一定的影响。
典型的代理服务器分片中间件有:阿里巴巴的Cobar、开源MyCat。
分布式数据库
在技术发展和演进的过程中,关系型数据库的一大问题是缺乏分布式环境下面对大数据量、高并发访问的有效数据处理机制。如:事务是关系型数据库的基本特征之一,但在分布式环境下,如果想要基于MySQL等传统关系型数据库来实现事务将面临巨大的挑战。
以TiDB为代表的分布式数据库赋予了关系型数据库一定程度的分布式特性。在这些分布式数据库中,数据分片及分布式事务将是其内置的基础功能,对业务开发人员完全透明。业务开发人员只需要使用框架对外提供的JDBC接口,就像在使用MySQL等传统关系型数据库一样。
基于ShardingSphere分库分表示例
添加依赖:
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.28</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<!-- sharding-jdbc -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
添加注解:
@MapperScan("com.sharding.mapper")
添加配置:
server:
port: 8080
spring:
application:
name: shardingsphere
main:
allow-bean-definition-overriding: true
# 设置数据源
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ds0?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
username: xxxx
password: xxxxxxxx
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ds1?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
username: xxxx
password: xxxxxxxx
# 设置分片策略
sharding:
# 设置默认数据库
default-data-source-name: ds0
# 设置分库策略
default-database-strategy:
inline:
# 分片列名
sharding-column: user_id
# 分片行表达式
algorithm-expression: ds$->{user_id % 2}
# 绑定表
binding-tables: t_health_record,t_health_task
# 广播表
broadcast-tables: t_health_level
tables:
t_health_record:
# 数据节点
actual-data-nodes: ds$->{0..1}.t_health_record$->{0..1}
# 设置分表策略
table-strategy:
inline:
sharding-column: record_id
algorithm-expression: t_health_record$->{record_id % 2}
# 设置主键id生成策略
key-generator:
column: record_id
type: SNOWFLAKE
t_health_task:
actual-data-nodes: ds$->{0..1}.t_health_task$->{0..1}
table-strategy:
inline:
sharding-column: record_id
algorithm-expression: t_health_task$->{record_id % 2}
key-generator:
column: task_id
type: SNOWFLAKE
props:
sql:
show: true
# mybatis-plus
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.sharding.entity
ShardingSphere雪花算法生成分布式ID:
- 1)数据库表结构,主键的类型为bigint。
- 2)主键不要设置自增。
- 3)对应的实体类主键类型设置为Long对象,而不是long。
数据库表:
-- 创建用户信息表
drop table if exists t_user;
create table t_user
(
user_id bigint comment '用户ID',
user_name varchar(100) null comment '用户名'
) engine = Innodb default charset = utf8mb4 comment '用户信息表' ;
create index idx_user_user_name on t_user (user_name);
-- 创建健康记录表
drop table if exists t_health_record;
create table t_health_record
(
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
level_id bigint comment '健康等级ID',
remark varchar(100) null comment '描述信息'
) engine = Innodb default charset = utf8mb4 comment '健康记录表';
create index idx_health_record_user_level on t_health_record (user_id,level_id);
-- 创建健康记录表
drop table if exists t_health_record0;
create table t_health_record0
(
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
level_id bigint comment '健康等级ID',
remark varchar(100) null comment '描述信息'
) engine = Innodb default charset = utf8mb4 comment '健康记录表0';
-- 创建健康记录表
drop table if exists t_health_record1;
create table t_health_record1
(
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
level_id bigint comment '健康等级ID',
remark varchar(100) null comment '描述信息'
) engine = Innodb default charset = utf8mb4 comment '健康记录表1';
-- 创建健康等级表
drop table if exists t_health_level;
create table t_health_level
(
level_id bigint comment '健康等级ID',
level_name varchar(100) null comment '健康等级名称'
) engine = Innodb default charset = utf8mb4 comment '健康等级表';
create index idx_health_level_name on t_health_level (level_name);
-- 创建健康任务表
drop table if exists t_health_task;
create table t_health_task
(
task_id bigint comment '健康任务ID',
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
task_name varchar(100) null comment '健康任务名称'
) engine = Innodb default charset = utf8mb4 comment '健康任务表';
create index idx_health_task_name on t_health_task (task_name);
-- 创建健康任务表
drop table if exists t_health_task0;
create table t_health_task0
(
task_id bigint comment '健康任务ID',
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
task_name varchar(100) null comment '健康任务名称'
) engine = Innodb default charset = utf8mb4 comment '健康任务表0';
-- 创建健康任务表
drop table if exists t_health_task1;
create table t_health_task1
(
task_id bigint comment '健康任务ID',
record_id bigint comment '健康记录ID',
user_id bigint comment '用户ID',
task_name varchar(100) null comment '健康任务名称'
) engine = Innodb default charset = utf8mb4 comment '健康任务表1';
数据库表对应的Entity:
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_user")
@ApiModel(value="User对象", description="用户信息表")
public class User extends Model<User> {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "用户ID")
//@TableId(value = "user_id", type = IdType.INPUT)
private Long userId;
@ApiModelProperty(value = "用户名")
private String userName;
@Override
protected Serializable pkVal() {
return this.userId;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_health_level")
@ApiModel(value="HealthLevel对象", description="健康等级表")
public class HealthLevel extends Model<HealthLevel> {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "健康等级ID")
//@TableId(value = "level_id", type = IdType.AUTO)
private Long levelId;
@ApiModelProperty(value = "健康等级名称")
private String levelName;
@Override
protected Serializable pkVal() {
return this.levelId;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_health_record")
@ApiModel(value="HealthRecord对象", description="健康记录表")
public class HealthRecord extends Model<HealthRecord> {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "健康记录ID")
//@TableId(value = "record_id", type = IdType.INPUT)
private Long recordId;
@ApiModelProperty(value = "用户ID")
private Long userId;
@ApiModelProperty(value = "健康等级ID")
private Long levelId;
@ApiModelProperty(value = "描述信息")
private String remark;
@Override
protected Serializable pkVal() {
return this.recordId;
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_health_task")
@ApiModel(value="HealthTask对象", description="健康任务表")
public class HealthTask extends Model<HealthTask> {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "健康任务ID")
//@TableId(value = "task_id", type = IdType.INPUT)
private Long taskId;
@ApiModelProperty(value = "健康记录ID")
private Long recordId;
@ApiModelProperty(value = "用户ID")
private Long userId;
@ApiModelProperty(value = "健康任务名称")
private String taskName;
@Override
protected Serializable pkVal() {
return this.taskId;
}
}
向HealthRecord和HealthTask插入记录:
@Service
public class DemoServiceImpl implements DemoService {
@Resource
private HealthRecordMapper healthRecordMapper;
@Resource
private HealthTaskMapper healthTaskMapper;
@Override
public void processHealthRecords() throws SQLException {
insertHealthRecords();
}
private List<Long> insertHealthRecords() throws SQLException {
List<Long> result = new ArrayList<>(10);
for (int i = 1; i <= 10; i++) {
HealthRecord healthRecord = insertHealthRecord(Long.valueOf(i));
insertHealthTask(Long.valueOf(i), healthRecord);
result.add(healthRecord.getRecordId());
}
return result;
}
private HealthRecord insertHealthRecord(final Long i) throws SQLException {
HealthRecord healthRecord = new HealthRecord();
healthRecord.setUserId(i);
healthRecord.setLevelId(i % 5);
healthRecord.setRemark("Remark" + i);
healthRecordMapper.insertHealthRecord(healthRecord);
return healthRecord;
}
private void insertHealthTask(final Long i, final HealthRecord healthRecord) throws SQLException {
HealthTask healthTask = new HealthTask();
healthTask.setRecordId(healthRecord.getRecordId());
healthTask.setUserId(i);
healthTask.setTaskName("TaskName" + i);
healthTaskMapper.insert(healthTask);
}
}