前言
传统的小型应用通常一个项目一个数据库,单表的数据量在百万以内,对于数据库的操作不会成为系统性能的瓶颈。但是对于互联网应用,单表的数据量动辄上千万、上亿,此时通过数据库优化、索引优化等手段,对数据库操作的性能优化可能无济于事,数据库操作势必称为性能的瓶颈。
究其原因,单机数据库系统的连接数、处理能力等都是有限的。对于这种情况,解决的方案有:
1)提升硬件配置。通过使用更好的CPU、更大的带宽、更大的内存等提升系统的处理能力。但该方案成本较高,而且效果不一定明细;
2)使用商用数据库。商用数据库经过高度优化,性能强大且稳定,提供丰富的管理工具等。但该方案成本较高,而且同样会受到硬件配置的影响;
3)分库分表。将数据分散到不同的数据库;在同一个库中,拆分到多个小的表。从而可以分散到多台硬件设备,减轻单机的压力,提升数据库操作的性能。成本相对较低;
分库分表
分库分表的拆分方式分为垂直拆分和水平拆分。
2.1 分库-垂直拆分
垂直拆分库比较简单,在数据库设计层面就可以实现。可以按照业务模块或实际项目需求,把一个项目中的表拆分到多个数据库中。
如用户维度相关的表放在用户库,业务维度相关的表放在业务库。
这样应用可以根据不同的业务连接不同的数据库,如当前的微服务架构,不同微服务可以使用不同的数据库连接。通过多个数据库分担流量,提高查询速度。
2.2 分表-垂直拆分
垂直拆分表是指把一个大表中的字段拆分成多个表,每个表存储其中一部分字段。
如商品信息表,把基本信息存放在商品表,详情等信息存放在商品详情表,两个表直接通过外键进行关联。商品表访问比较频繁,拆分后,单条记录较小,查询效率较高。而商品详情表,通常在查看单个商品时访问,单条记录较大,但通过主键或其他索引,仍然能够保证效率。
实现相对简单,可以有效的提升查询效率。
2.3 分表-水平拆分
水平拆分表是指把一个表中的数据拆分到多个表中。与垂直拆分表的区别是:垂直拆分表是按列(字段)拆分、水平拆分表是按行(记录)拆分。
如订单表,随着业务规模的提升,订单数据不断的膨胀。可以将订单表按会员、商家、下单时间等进行分表存放。
把一个大表拆分成多个按照一定规律分布的小表,每个表的数据量较小,可以有效的提升数据库操作效率。
2.4 分库-水平拆分
水平拆分库是指把一个表中的数据拆分到多个数据库中,数据库采用分布式部署。在水平拆分表的基础上,如果一个库中的表太多,可以通过水平拆分库,将表数据先分库再进行分表。
如订单表,可以先按商家分库,然后再按会员分表。数据插入时,先按商家,找到匹配的数据库,然后再按会员,找到匹配的表,插入对应数据。
通过水平分库分表,针对分库分表时的指定键的查询时,可以大大的提升查询效率。
分库分表带来的问题
3.1 事务一致性
【源码】SpringBoot事务注册原理-CSDN博客
在上面的博文中分享了何为事务以及单数据库事务在SpringBoot中的实现。对于分库分表,事务的一致性处理将变得更加复杂。在单数据库中,事务原子性、隔离性是由数据库来保证,但在多数据库中,事务的特性中只有持久性能够由数据库保证,其他都需要由程序开发者自行实现。
3.2 跨库跨表查询
对于单库单表,无论是关联查询,还是分页、排序,都是在一张表中通过一条SQL即可实现。而进行分表分库之后,数据分布在不同的库、不同的表,实现变得比较复杂。最坏的情况是需要先在不同的库、不同的表中查询数据,进行排序并返回,然后程序中汇总再获取满足条件的数据。
3.3 主键重复
在单库单表中,可以通过数据库提供的自增长设置主键,但在分库分表中,自增长的主键会存在重复。因此需要使用全局唯一的主键。如UUID、雪花算法等。
3.4 公共表
对于一些字典、配置类等数据量较小、变动少,而且需要高频联合查询的表,可以建立公共表。在分库环境中,每个库都保存一份相同的公共表。
Sharding-JDBC介绍
针对分库分表带来的问题,通过Sharding-JDBC这个框架,都可以有效的解决。
Sharding-JDBC 是一款由当当网开源的分库分表中间件,它可以透明地为 Java 应用程序提供数据库分片功能,无需关心底层的数据库分片细节。它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为 ShardingSphere,2020年4⽉16⽇正式成为 Apache 软件基⾦会的顶级项⽬。
ShardingSphere官网:Apache ShardingSphere
Sharding-JDBC入门
以下以订单分表为例,介绍Sharding-JDBC的基本使用。
1)订单表:订单表的字段有订单id、会员id、总价格、状态、下单时间;
2)创建两个订单表,分表为tb_order_1和tb_order_2;
3)分表规则:以订单id分表,id被2整除的放tb_order_1,不能被2整除的放tb_order_2;
5.1 导入sharding-jdbc-spring-boot-starter依赖
<?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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Sharing-JDBC-demo</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
引入sharding-jdbc-spring-boot-starter,此处的数据库连接池采用Druid。需要注意的是,不能直接使用druid-spring-boot-starter,否则启动会报错,因为该starter会自动注入SqlSessionFactory和SqlSessionTemplate,和Sharding-JDBC冲突。
5.2 sharding-jdbc配置
# 单数据库,inline分片策略测试
server:
port: 8080
#sharding-jdbc分片规则配置
spring:
shardingsphere:
datasource:
names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致
order1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456
#分表策略
#按照id分表,id使用雪花算保证全局唯一,具体算法:tb_order是表前缀,拼接上:$->{id % 2} 的值
sharding:
tables:
tb_order: #逻辑表
actual-data-nodes: order1.tb_order_$->{1..2} #order1:数据源名称;两个tb_order表,分别为tb_order_1和tb_order_2
key-generator: # 指定主键生成策略
column: order_id
type: SNOWFLAKE
table-strategy: #分表策略
inline:
sharding-column: order_id #分片键。对id进行分表
algorithm-expression: tb_order_$->{order_id % 2 + 1} #分片算法
props:
sql:
show: true # 是否打印sql
1)所有sharding-jdbc的相关配置都在spring.shardingsphere开头的属性中;
2)数据源的配置在spring.shardingsphere.datasource开头的属性中;
3)分库分表策略的配置在spring.shardingsphere.sharding.tables开头的属性中;
分库分表策略都是针对表进行的。先指定逻辑表,然后指定分库策略、分表策略等。
5.3 实体类
package com.jingai.sharing.jdbc.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.ToString;
import java.util.Date;
@Data
@ToString
@TableName("tb_order")
public class OrderEntity {
private long orderId;
private long memberId;
private float totalPrice;
private String status;
private Date orderTime;
}
在实体类中,@TableName指定配置中的逻辑表。
5.4 Mapper类
package com.jingai.sharing.jdbc.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jingai.sharing.jdbc.entity.OrderEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
public interface OrderMapper extends BaseMapper<OrderEntity> {
@Insert("insert into tb_order(member_id, total_price, status, order_time) values " +
"(#{memberId}, #{totalPrice}, #{status}, #{orderTime})")
@Options(useGeneratedKeys = true, keyProperty = "orderId")
int insert2(OrderEntity order);
}
在5.2的配置中,通过key-generator设置了逻辑表的主键生成策略为雪花算法。当进行数据插入时,需要编写新的插入接口,不能直接使用Mybatis-plus中的insert()接口。因为在默认的insert()接口中,实体对象的orderId为0,不会走配置的雪花算法。
5.5 Service类
package com.jingai.sharing.jdbc.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jingai.sharing.jdbc.dao.OrderMapper;
import com.jingai.sharing.jdbc.entity.OrderEntity;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class OrderService extends ServiceImpl<OrderMapper, OrderEntity> {
@Resource
private OrderMapper orderMapper;
public long insert2(OrderEntity order) {
int rs = orderMapper.insert2(order);
return rs > 0 ? order.getOrderId() : 0;
}
}
为了便于测试,此处省略了Service的接口类。
5.6 Controller类
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@RequestMapping("order")
public String order(OrderEntity order) {
order.setOrderTime(new Date());
long insert = orderService.insert2(order);
return insert > 0 ? "success" : "fail";
}
}
访问以上的order接口,系统打印的日志如下:
插入的数据为:
结合日志,执行流程如下:
1)在插入之前,解析逻辑SQL,获取分片键值tb_order;
2)配置的分库分表规则为以order_id作为分表的键,id被2整除的放tb_order_1,不能被2整除的放tb_order_2,以上的order_id为1014578121078210560,所以实际存放的表为tb_order_1;
3)根据实际操作的表tb_order_1,修改SQL语句;
4)执行真实SQL语句;
结尾
限于篇幅,本篇先分享到这里。
关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。