场景
中国现在有9亿网民,我们随便一个人做点什么都会产生大量数据,比如看一下视频发表一下感想。
点赞57万,投币45万,评论1W+,再比如前段时间的618购物节,无数网民疯狂购物产生无数的消费数据,这么庞大的数据量该如何存储?
前言
我们都知道mysql
有性能瓶颈,当数据量到达2100w左右的时候,效率就会大幅下降。
mysql> show global variables like '%page%';
+--------------------------------------+-----------+
| Variable_name | Value |
+--------------------------------------+-----------+
| innodb_log_compressed_pages | ON |
| innodb_max_dirty_pages_pct | 75.000000 |
| innodb_max_dirty_pages_pct_lwm | 0.000000 |
| innodb_page_cleaners | 1 |
| innodb_page_size | 16384 |
| innodb_stats_persistent_sample_pages | 20 |
| innodb_stats_sample_pages | 8 |
| innodb_stats_transient_sample_pages | 8 |
| large_page_size | 0 |
| large_pages | OFF |
+--------------------------------------+-----------+
10 rows in set (0.01 sec)
也就是说innode_page_size
为16kb
,我们常用int
,bigint
和varchar
作为主键,当然mysql
官方推荐使用int
或bigint
作为主键。总所周知mysql
中一个int
类型的所占用字节为4个字节,一个bigint
占了8个字节。
SELECT * FROM `information_schema`.`TABLES` where table_name='table_name'
可以查看有关表的信息,关于索引的大小就在index_length
,所以一个索引的占用是6个字节。
所以一个page
我们可以存多少数据量呢,首先我们B+tree
的degree
是3。
int
类型 16384 / (6+4) = 1638 行,也就是说我们能存
1638 * 1638 * 16 = 42928704
bigint
类型 16384 / (8+6) = 1170,也就是说能存
1170 * 1170 * 16 = 21977344
所以我们说mysql
到了2100w,就到头了,不是因为不能存,而是数据存进去,B+tree
的高度会增加,遍历子节点的次数要增加。
分库分表设计
我们可以采用水平拓展的方式去实现存储。比如9亿网民,单张表我们存2100W。那就多搞张表。
# 数据库1
db_member_0
tb_member_0
tb_member_1
tb_member_2
tb_member_3
tb_member_9
# 数据库2
db_member_1
tb_member_0
tb_member_1
tb_member_9
db_member_9
db_member_x
表示我第x
个数据库,tb_member_x
表示库里的第x
个表。这样就可以存21亿的数据量。
如何存储某一位网民的假设我们的主键id
是202006272319230001,这个id
是用年月日时分秒加一个0001这样的数字组合而成的,我们对单库的表个数取模,比如我数据库db_member_0
中用户表有10个,那id
个位对10取模就存在某个表里,他的十位取模决定了存在某个库里。
也就是说0001存在0号库的1号表里,0072存7号库的2号表里。
Leaf (TBD)
上述说了这么多,但是我们的数据库表id
可能是自增的,那查询的时候如何处理?
这个就是今天第一个要解决的问题leaf
分布式ID
。
There are no two identical leaves in the world.
世界上没有两片完全相同的树叶。
— 莱布尼茨
ShardingSphere
ShardingSphere
是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC
、Sharding-Proxy
和Sharding-Sidecar
(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java
同构、异构语言、云原生等各种多样化的应用场景。
Sharding-JDBC
定位为轻量级Java
框架,在Java
的JDBC
层提供的额外服务。 它使用客户端直连数据库,以jar
包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC
驱动,完全兼容JDBC
和各种ORM
框架。
实际参考项目 hello-sharding-jdbc
本项目采用mysql
+mybatis-Plus
开发,创建2个数据库。
mysql 环境
-- 数据库0
CREATE DATABASE `db_order_0`
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci ;
-- 数据库1
CREATE DATABASE `db_order_1`
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci ;
在每一个数据库创建2张表
-- 数据库0
use db_order_0;
CREATE TABLE `tb_order_0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_order_1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 数据库1
use db_order_1;
CREATE TABLE `tb_order_0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `tb_order_1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
还得创建一个模型表,因为得使用mybatis-Plus
生成代码,所以在0号数据库生成一个逻辑表,表结构和分表的表结构一致
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
添加依赖
主要是mysql
的依赖版本问题需要注意
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- MySQL 驱动的版本号必须是 5.1.48 -->
<version>5.1.48</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!-- Apache ShardingSphere -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC3</version>
</dependency>
配置文件
spring:
main:
allow-bean-definition-overriding: true
shardingsphere:
props:
sql:
# 是否显示sql语句 会将逻辑语句和实际执行的语句打印到控制台
show: true
datasource:
# 分库分表 可以配置多个数据源
names: ds0,ds1
# 数据库0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.8.50:3306/db_order_0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=false
username: root
password: root
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
# 分库规则
sharding:
# 逻辑表 可以多个 ,分隔
binding-tables: tb_order
default-database-strategy:
inline:
# user_id 对2取余,几个数据库就对多少取模,这样就可以选择 ds0,ds1...ds9
algorithm-expression: ds$->{user_id % 2}
sharding-column: user_id
tables:
tb_order:
actual-data-nodes: ds$->{0..1}.tb_order_$->{0..1}
table-strategy:
inline:
# 使用order_id 分别打在不同表上
algorithm-expression: tb_order_$->{order_id % 2}
sharding-column: order_id
执行sql
查询全部
QueryWrapper<TbOrder> queryWrapper = new QueryWrapper<>();
List<TbOrder> tbOrders = mapper.selectList(queryWrapper);
日志,实际查找了4张表
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Rule Type: sharding
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Logic SQL: SELECT id,order_id,user_id FROM tb_order
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : SQLStatement: SelectSQLStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.core.parse.sql.statement.dml.SelectStatement@509a6095, tablesContext=TablesContext(tables=[Table(name=tb_order, alias=Optional.absent())], schema=Optional.absent())), projectionsContext=ProjectionsContext(startIndex=8, stopIndex=26, distinctRow=false, projections=[ColumnProjection(owner=null, name=id, alias=Optional.absent()), ColumnProjection(owner=null, name=order_id, alias=Optional.absent()), ColumnProjection(owner=null, name=user_id, alias=Optional.absent())]), groupByContext=org.apache.shardingsphere.core.preprocessor.segment.select.groupby.GroupByContext@57cabdc3, orderByContext=org.apache.shardingsphere.core.preprocessor.segment.select.orderby.OrderByContext@75bd28d, paginationContext=org.apache.shardingsphere.core.preprocessor.segment.select.pagination.PaginationContext@129c4d19, containsSubquery=false)
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_0
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_1
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT id,order_id,user_id FROM tb_order_0
2020-07-01 12:51:07.113 INFO 21232 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT id,order_id,user_id FROM tb_order_1
插入数据
for (int i = 0; i < 100 ; i++) {
TbOrder tbOrder = new TbOrder();
tbOrder.setUserId(Long.parseLong(""+i));
// 随机打在某个库的2张中的1张表
tbOrder.setOrderId(new Double((Math.random()*2)).longValue());
mapper.insert(tbOrder);
}
日志,库是根据userId
对2取余得到的结果,表是orderId
随机生成后对2取余的结果()
2020-07-01 12:55:01.704 INFO 17012 --- [ main] ShardingSphere-SQL : Rule Type: sharding
2020-07-01 12:55:01.704 INFO 17012 --- [ main] ShardingSphere-SQL : Logic SQL: INSERT INTO tb_order ( order_id,user_id ) VALUES ( ?,? )
2020-07-01 12:55:01.704 INFO 17012 --- [ main] ShardingSphere-SQL : SQLStatement: InsertSQLStatementContext(super=CommonSQLStatementContext(sqlStatement=org.apache.shardingsphere.core.parse.sql.statement.dml.InsertStatement@61b838f2, tablesContext=TablesContext(tables=[Table(name=tb_order, alias=Optional.absent())], schema=Optional.absent())), columnNames=[order_id, user_id], insertValueContexts=[InsertValueContext(parametersCount=2, valueExpressions=[ParameterMarkerExpressionSegment(startIndex=55, stopIndex=55, parameterMarkerIndex=0), ParameterMarkerExpressionSegment(startIndex=58, stopIndex=58, parameterMarkerIndex=1)], parameters=[1, 2])])
2020-07-01 12:55:01.705 INFO 17012 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: INSERT INTO tb_order_1 ( order_id,user_id ) VALUES (?, ?) ::: [1, 2]
查询单条记录
分2种情况
- 如果是走了
sharding-jdbc
的sharding-column
键,那么实际查询查其中分库分表的一部分
QueryWrapper<TbOrder> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id",32);
List<TbOrder> tbOrders = mapper.selectList(queryWrapper);
日志
2020-07-01 13:01:20.389 INFO 20300 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_0 WHERE (user_id = ?) ::: [32]
2020-07-01 13:01:20.389 INFO 20300 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_1 WHERE (user_id = ?) ::: [32]
- 如果没走
sharding-column
键,就会全库全表查询。
QueryWrapper<TbOrder> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id",220);
List<TbOrder> tbOrders = mapper.selectList(queryWrapper);
日志
2020-07-01 13:04:13.088 INFO 13268 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_0 WHERE (id = ?) ::: [220]
2020-07-01 13:04:13.088 INFO 13268 --- [ main] ShardingSphere-SQL : Actual SQL: ds0 ::: SELECT id,order_id,user_id FROM tb_order_1 WHERE (id = ?) ::: [220]
2020-07-01 13:04:13.088 INFO 13268 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT id,order_id,user_id FROM tb_order_0 WHERE (id = ?) ::: [220]
2020-07-01 13:04:13.088 INFO 13268 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: SELECT id,order_id,user_id FROM tb_order_1 WHERE (id = ?) ::: [220]
回头再看这张图,就可以明白实现的基本操作,就可以理解sharding-jdbc
如何分库分表的操作了
Sharding-Proxy (TBD)
[Leaf——美团点评分布式ID生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html)
[B+Tree 数据结构](https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html)
[为什么我的mysql里index_length是0bytes](https://stackoverflow.com/questions/29692421/mysql-why-is-my-index-length-0-0-bytes)