分库分表诞生的前景
随着系统用户运行时间还有用户数量越来越多,整个数据库某些表的体积急剧上升,导致CRUD的时候性能严重下降,还容易造成系统假死
这时候系统都会做一些基本的优化,比如加索引、缓存、读写分离/主从复制,增删改都走主库,查询走从库。但是这样没法提升主库写的能力,因为主库只有一个。这时候就要考虑分库分表了,一般数据库在设计的时候就会提前考虑到是否有分库分表场景需要,避免后期带来迁移的问题,而且最好对表的查询足够简单,尽量避免跨表跨库查询。
阿里的开发规范中建议预估三年内单表数据量上500W,或者大小上2G,就要考虑分库分表
分库分表的方式(垂直拆分,水平复制)
一般系统模块都是访问同一个数据库资源,所有的表都存放在一个库里面
1.垂直拆分
1.1 垂直分库
把单一的数据库进行业务划分,专库专表
经过垂直拆分之后,每个模块都使用各自独立的数据库,减轻了数据库的压力,业务也更加清晰,拓展也更容易了,但是会增加连表查询以及事务处理的复杂度,无法解决单表数据量太大的问题
1.2 垂直分表
垂直拆分表主要解决一张表太多字段某个字段存储值为大文本会对io查询有损耗所以把本来属于同一个对象描述的属性拆分多个表,分布式微服务分库分表尽量不要严苛遵守数据库的3大范式,可参考不可严格遵循
相当于把一个大表根据字段拆分成多个小表 ,一个10w数据的表,变成两个10w数据的表
这样拆分的好处就是,假如只显示列表,不需要显示详细信息就很方便,例如一个订单是包含很多信息的,但是在后台通常不需要去获取订单的详情信息用作展示,一般只需要展示概要信息:下单用户、下单时间、金额等等重要信息。于是可以把一个订单表垂直拆分为两个表来处理
2.水平拆分
2.1 水平分库
把一个数据库分散成多个结构相同的数据库,本质就是复制操作
2.2 水平分表
一个表数据量太大,将一个表按不同的条件分散多个表中,把1000w的表拆分为两个500w的表
分表的规则有很多种:
- 取范围
根据时间范围或者id范围分布到不同的库中,例如把2020年前的数据放到一个表中,之后的数据放到一个表中。把用户ID 0~100000放到一个表中,100000~200000的数据放到一个表中。
优点:使用分片字段范围查询比较方便
缺点:某段范围内热点数据可能被频繁读写,其他数据很少被查询 - 数值hash取模运算
根据某个字段进行运算均匀的分配到不同的表中
优点:分散比较均匀,不容易存在热点数据
缺点:数据太分散,导致范围查询比较麻烦,需要查询分库之后再合并
水平拆分的时候会导致多库多表的联合查询难度变大,以及多数据源管理的问题
分库分库中间件
ShardingSphere
官网:https://shardingsphere.apache.org/
ShardingSphere是一个关系型数据库中间件,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成,主要提供数据库分片以及分布式事务
Sharding-JDBC
Sharding-JDBC简化分库分表之后数据相关操作,它一个轻量级的Java框架,是增强版的JDBC驱动,以jar包的形式提供引入非常简单,适用于很多ORM框架以及数据库连接池,Sharding-JDBC不做分库分表,主要做两个功能:数据分片和读写分离,简化分库分表后对数据的操作
实现水平分表
环境搭建:SpringBoot+mybatisPlus
1、首先创建一个springboot项目,引入相关依赖jar包
<!-- Sharding-JDBC --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.0.0-RC1</version> </dependency> <!-- Mysql驱动依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!-- Druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <!--mybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
2、创建数据库表结构
创建一个mall数据库,然后里面创建两张表分别为:order0、order1
CREATE DATABASE `mall` CREATE TABLE `orders0` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_number` varchar(50) NOT NULL, `create_time` date NOT NULL, `creater` varchar(20) NOT NULL, `money` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE `orders1` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_number` varchar(50) NOT NULL, `create_time` date NOT NULL, `creater` varchar(20) NOT NULL, `money` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
3、约定分片规则
如果订单号order_number
为奇数添加到orders0
,否则添加到orders1
4、建立相关实体类、DAO
/** * @author yanglingcong */ @Data public class Orders implements Serializable { private Long id; private String orderNumber; private Timestamp createTime; private String creater; private double money; }
OrderDao
/** * @author yanglingcong */ @Mapper public interface OrderDao extends BaseMapper<Orders> { }
5、配置分片策略以及数据源
application.properties
server.port=8099 spring.application.name=sharding-jdbc-demo spring.profiles.active=dev #指定数据库连接信息 spring.shardingsphere.datasource.names=db0 spring.shardingsphere.datasource.db0.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.db0.url=jdbc:mysql://xxx:3306/mall?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.db0.username=root spring.shardingsphere.datasource.db0.password=xxx # 一个实体类对应两张表,覆盖 spring.main.allow-bean-definition-overriding=true #指定主键id列的生成策略 SNOWFLAKE表示雪花 spring.shardingsphere.sharding.tables.orders.key-generator.column=id spring.shardingsphere.sharding.tables.orders.key-generator.type=SNOWFLAKE #指定表的分布 order数据库的order1和order2表 spring.shardingsphere.sharding.tables.orders.actual-data-nodes=db0.orders$->{0..1} #指定分片策略 约定根据订单号order_number分片 奇数添加到`order1`,否则添加到`order2` 表索引从1开始的,所以加上1 spring.shardingsphere.sharding.tables.orders.table-strategy.inline.sharding-column=id spring.shardingsphere.sharding.tables.orders.table-strategy.inline.algorithm-expression=orders$->{id % 2} #sql输出日志 spring.shardingsphere.props.sql.show=true
6、测试生成
@RunWith(SpringRunner.class) @SpringBootTest public class ShardingJdbcDemoApplicationTests { @Autowired OrderDao orderDao; @Test void insertTest() { for (int i = 0; i < 1; i++) { Orders order=new Orders(); order.setCreater("ylc"); order.setOrderNumber("6"); order.setCreateTime(new Timestamp(System.currentTimeMillis())); order.setMoney(30.01); orderDao.insert(order); } } @Test void selectTest(){ QueryWrapper<Orders> queryWrapper=new QueryWrapper<>(); queryWrapper.eq("id",1553218859002007553L); Orders orders = orderDao.selectOne(queryWrapper); System.out.println(orders); } }
最后根据生成的雪花id插入到了数据库中
查询也会根据雪花id找到对应的表
sharding-jdbc水平分表就是提前做好分表,然后配置分表规则,就可以让数据插入在不同的表中了,这里只是指定了一个数据源,还可以指定多个数据源
实现水平分库
水平分库规则为:根据表里面的订单号字段orderNumber
,奇数就路由到mall_1库,偶数路由到mall_2库
1、更改配置文件
server.port=8099 spring.application.name=sharding-jdbc-demo spring.profiles.active=dev spring.shardingsphere.sharding.default-data-source-name=db1 #指定数据库连接信息 spring.shardingsphere.datasource.names=db1,db2 spring.shardingsphere.datasource.db1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.db1.url=jdbc:mysql://xxx:3306/mall_1?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.db1.username=root spring.shardingsphere.datasource.db1.password=xxx spring.shardingsphere.datasource.db2.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.db2.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.db2.url=jdbc:mysql://xxx:3306/mall_2?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.db2.username=root spring.shardingsphere.datasource.db2.password=xxx # 一个实体类对应两张表,覆盖 spring.main.allow-bean-definition-overriding=true #指定主键id列的生成策略 SNOWFLAKE表示雪花 spring.shardingsphere.sharding.tables.orders.key-generator.column=id spring.shardingsphere.sharding.tables.orders.key-generator.type=SNOWFLAKE #指定库的分布 spring.shardingsphere.sharding.tables.orders.actual-data-nodes=db$->{1..2}.orders0 #指定数据库分表策略 根据order_number奇数就路由到mall_1库,偶数路由到mall_2库 spring.shardingsphere.sharding.tables.orders.database-strategy.inline.sharding-column=order_number spring.shardingsphere.sharding.tables.orders.database-strategy.inline.algorithm-expression=db$->{order_number % 2+1} #sql输出日志 spring.shardingsphere.props.sql.show=true
2、更改实体
/** * @author yanglingcong */ @Data public class Orders implements Serializable { private Long id; private long orderNumber; private Timestamp createTime; private String creater; private double money; }
3、sql脚本
CREATE DATABASE `mall_0` CREATE TABLE `orders0` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_number` bigint NOT NULL, `create_time` date NOT NULL, `creater` varchar(20) NOT NULL, `money` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE DATABASE `mall_1` CREATE TABLE `orders0` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_number` bigint NOT NULL, `create_time` date NOT NULL, `creater` varchar(20) NOT NULL, `money` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
4、插入数据
@Test void insertTest() { for (int i = 0; i < 3; i++) { Orders order=new Orders(); order.setCreater("ylc"); order.setOrderNumber(i); order.setCreateTime(new Timestamp(System.currentTimeMillis())); order.setMoney(30.01); orderDao.insert(order); } } @Test void selectTest(){ QueryWrapper<Orders> queryWrapper=new QueryWrapper<>(); queryWrapper.eq("order_number",1553314824824299522L); Orders orders = orderDao.selectOne(queryWrapper); System.out.println(orders); }
很简单就实现了分库功能,分库分表就是把这两种方式结合到了一起
分库分表查询,分库分表字段为必传字段,否则不知道去哪个数据库哪张表去查询数据,所以根据非主键查询的不支持。
实现垂直分库
垂直分库相当于专库专表,可以使用多数据源来处理,也可以通过sharding-Jdbc
垂直分库的规则为:往订单表orders插入数据,会自动找到订单表所在的库mall进行操作。往用户表插入数据,会自动找到所在库User进行操作
1、数据库脚本
CREATE DATABASE `user` CREATE TABLE `user`.userInfo ( id BIGINT auto_increment NOT NULL, user_name varchar(20) NOT NULL, password varchar(50) NOT NULL, CONSTRAINT userinfo_PK PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci AUTO_INCREMENT=1; CREATE DATABASE `user` CREATE DATABASE `mall` CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_number` bigint NOT NULL, `create_time` date NOT NULL, `creater` varchar(20) NOT NULL, `money` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
2、创建相关实体类
UserInfo
@Data public class UserInfo { private Long id; private String userName; private String password; }
Orders
@Data public class Orders implements Serializable { private Long id; private long orderNumber; private Timestamp createTime; private String creater; private double money; }
3、Dao类
@Mapper public interface UserInfoDao extends BaseMapper<UserInfo> { }
4、配置垂直分库策略
server.port=8099 spring.application.name=sharding-jdbc-demo spring.profiles.active=dev spring.shardingsphere.sharding.default-data-source-name=db1 #指定数据库连接信息 spring.shardingsphere.datasource.names=db1,db2 spring.shardingsphere.datasource.db1.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.db1.url=jdbc:mysql://xxx:3306/mall?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.db1.username=root spring.shardingsphere.datasource.db1.password=xxx spring.shardingsphere.datasource.db2.type=com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.db2.driver-class-name=com.mysql.cj.jdbc.Driver spring.shardingsphere.datasource.db2.url=jdbc:mysql://xxx:3306/user?serverTimezone=GMT%2B8 spring.shardingsphere.datasource.db2.username=root spring.shardingsphere.datasource.db2.password=xxx # 一个实体类对应两张表,覆盖 spring.main.allow-bean-definition-overriding=true #指定主键id列的生成策略 SNOWFLAKE表示雪花 spring.shardingsphere.sharding.tables.orders.key-generator.column=id spring.shardingsphere.sharding.tables.orders.key-generator.type=SNOWFLAKE spring.shardingsphere.sharding.tables.userInfo.key-generator.column=id spring.shardingsphere.sharding.tables.userInfo.key-generator.type=SNOWFLAKE #指定orders表所在的库 userInfo表所在的库 spring.shardingsphere.sharding.tables.orders.actual-data-nodes=db1.orders spring.shardingsphere.sharding.tables.userInfo.actual-data-nodes=db2.userInfo #指定orders表分片策略 spring.shardingsphere.sharding.tables.orders.table-strategy.inline.sharding-column=id spring.shardingsphere.sharding.tables.orders.table-strategy.inline.algorithm-expression=orders #指定数userInfo分片策略 spring.shardingsphere.sharding.tables.userInfo.table-strategy.inline.sharding-column=id spring.shardingsphere.sharding.tables.userInfo.table-strategy.inline.algorithm-expression=userInfo #sql输出日志 spring.shardingsphere.props.sql.show=true
5、插入数据
Orders表在mall库
@Test void insertOrdersTest() { Orders order=new Orders(); order.setCreater("ylc"); order.setOrderNumber(1); order.setCreateTime(new Timestamp(System.currentTimeMillis())); order.setMoney(30.01); orderDao.insert(order); }
UserInfo表在user库
@Test void insertUserTest() { UserInfo userInfo=new UserInfo(); userInfo.setUserName("ylc"); userInfo.setPassword("123456"); userInfoDao.insert(userInfo); }
这样就实现了不同的表插入了不同的数据库,不过使用多数据源方式会更方便