微服务解决方案 -- 分库分表 ShardingSphere

场景

中国现在有9亿网民,我们随便一个人做点什么都会产生大量数据,比如看一下视频发表一下感想。

微服务解决方案 -- 分库分表 ShardingSphere_第1张图片

点赞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_size16kb,我们常用intbigintvarchar作为主键,当然mysql官方推荐使用intbigint作为主键。总所周知mysql中一个int类型的所占用字节为4个字节,一个bigint占了8个字节。

SELECT * FROM `information_schema`.`TABLES` where table_name='table_name' 

可以查看有关表的信息,关于索引的大小就在index_length,所以一个索引的占用是6个字节。

所以一个page我们可以存多少数据量呢,首先我们B+treedegree是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-JDBCSharding-ProxySharding-Sidecar(计划中)这3款相互独立的产品组成。 他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、云原生等各种多样化的应用场景。

Sharding-JDBC

定位为轻量级Java框架,在JavaJDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

微服务解决方案 -- 分库分表 ShardingSphere_第2张图片

实际参考项目 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的依赖版本问题需要注意


<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    
    <version>5.1.48version>
dependency>

<dependency>
    <groupId>com.baomidougroupId>
    <artifactId>mybatis-plus-boot-starterartifactId>
    <version>3.2.0version>
dependency>

<dependency>
    <groupId>org.apache.shardingspheregroupId>
    <artifactId>sharding-jdbc-spring-boot-starterartifactId>
    <version>4.0.0-RC3version>
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种情况

  1. 如果是走了sharding-jdbcsharding-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]
  1. 如果没走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如何分库分表的操作了
微服务解决方案 -- 分库分表 ShardingSphere_第3张图片

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)

你可能感兴趣的:(微服务解决方案)