MySQL的存储单位是page[16kb],索引使用B+Tree,深度为3(3次 IO便能查出数据)。为了提高查询速度,存储单元中都存储的是索引的指针。MySQL内部索引指针大小在InnoDB源码中设置为 6个字节+例如 ID类型bigint(long)占8个字节 = 14字节,那么一页存储 16*1024/14 ≈ 1170行数据。因为深度为3,表示此时一张表最多存储(这里假设叶子节点一行记录的数据大小为1k,实际上现在很多互联网业务数据记录大小通常就是1K左右)16[叶子节点只能存16行数据] * 1170 * 1170 = 21902400 行数据。如果再添加数据就会导致深度增加,深度增加就会导致查询效率下降,所以单表只能放这么多数据。通常,单个数据库中的数据在 1TB以内是一个相对合理的范围。在传统的关系数据库无法满足Internet需求的情况下,越来越多的尝试将数据存储在本机分布式NoSQL中。但是它与SQL的不兼容性以及生态系统的不完善性使其无法在竞争中击败关系数据库,因此关系数据库仍然处于不可动摇的地位。同时在互联网应用系统下,单库会存在性能瓶颈(高性能,高可能,高并发),就出现了分库分表。MyCat 并发量大的时候会出现性能瓶颈,所以在实际生产中推荐使用 ShardingSphere

# 一、概述

ShardingSphere是一款开源的分布式数据库中间件组成的生态圈。自从2016年开源以来,不断升级开发新功能、重构内核,并于2018年11月进入 Apache基金会孵化器。它由京东集团主导,并由多家公司以及整个ShardingSphere社区共同运营参与贡献。其主要的功能模块为:数据分片(分库分表)、分布式事务、数据库治理三大块内容。

Sharding-JDBCShardingSphere的第一个产品,也是ShardingSphere的前身。 它定位为轻量级Java框架,在JavaJDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。Sharding-JDBC 采用无中心化架构,适用于Java开发的高性能的轻量级 OLTP应用;
【1】适用于任何基于 JDBC的 ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC
【2】支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
【3】支持任意实现 JDBC规范的数据库。目前支持MySQLOracleSQLServerPostgreSQL以及任何遵循SQL92标准的数据库。

ShardingSphere

ShardingSphere主要的三种类型的比较Sharding-JDBC(代码层面的处理)、Sharding-Proxy(专门的分库分表中间件,目前在docker中跑不起来)和Sharding-Sidecar(大规模集群上,例如k8s。计划中,还未开发完成):

ShardingSphere

# 二、数据分片

分片是指将数据按照一定的标准将其存储在多个表和数据库中,从而提高性能和可用性。两种方法都可以有效避免由于数据超出可承受阈值而导致的查询限制。而且,数据库分片还可以有效地分散TPS。表分片虽然不能减轻数据库的压力,但可以提供将分布式事务转移到本地事务的可能性,因为曾经涉及跨数据库升级,所以分布式事务有时会变得非常棘手。使用多个主从分片方法可以有效避免数据集中在一个节点上,提高架构可用性。通过数据库分片和表分片拆分数据是处理高TPS和海量数据系统的有效方法,因为它可以使数据量保持低于阈值并疏散流量。分片方法可以分为垂直分片和水平分片。

数据分片在ShardingSphere中主要被划分为读写分离、数据拆分。读写分离主要是指:为数据库搭建灾备副本,并在访问时将这些生产及灾备库分为主库、从库两种角色。其中主库处理所有的修改、变更操作以及少部分读操作;从库分担主库大部分的读请求。数据拆分在这里主要指数据水平分片,即真正意义上将一个数据库拆分成多个分库,分别存储及访问。具体架构如下图所示:

ShardingSphere

在此基础上,很多业务系统出于性能和安全考虑,会选择这两种方式的混合部署架构,即同时使用读写分离和水平分片策略,如下图所示:

ShardingSphere

在这种情况下,底层数据的架构网络就会显得异常复杂和繁琐。因为在整个分布式的数据库系统当中会存在:分库1、分库2……,还有对应的从库1、从库2……对业务开发的同学来讲,自身的精力和注意力不仅要放到跟 KPI挂钩的业务代码开发上,还需要考虑如何实现和维护这样一套分布式数据库系统。所以 ShardingSphere便提供了Sharding-Proxy充当一款分布式数据库中间件,它将为大家解决这些场景下的数据库管理维护的工作(如果生产上不适用 docker部署就可以使用此模式,但我们生产目前使用docker部署,而 Sharding-Proxydocker上跑不起来,镜像有问题,后期官方会进行修复) 。

TIP

数据分片的重中之重是:如何去拆分数据库表。 这将关系到今后整个数据库系统的性能、与业务系统的匹配默契程度。ShardingSphere提供了如哈希取模、范围划分、标签分类、时间范围以及复合分片等多种切分策略。

举例: 业务方有可能会按照订单号后几十位做哈希取模来切分库表;也有可能将日志文件信息按照日、月、年的维度进行切分数据,并存储到数据库;还可能按照业务类型进行分库分表等。针对各式各样的业务场景,ShardingSphere提供了以下多种分片策略。虽然这些分配策略基本可以满足80%以上业务方需求,但还是会存在一些变态的业务场景。为此,我们开放了数据分片策略的接口,业务方可以选择按照自己的需求实现这些数据分片接口,ShardingSphere就会通过 SPI的方式将其加载使用。

ShardingSphere

在确定好数据分片策略后,ShardingSphere将使用该分片策略进行以下操作来完成对某条 SQL的 DDL&DML&DAL&DQL&TCL等操作。但是这个过程对用户来说是透明的,即在用户无感知的情况下,ShardingSphere将用户输入的 SQL进行解析,然后依据用户指定的分片策略对这条不含分片信息的 SQL进行改写,将其改写成为真正在某个或多个数据表上执行的某条或多条真实的SQL。此外,还需要找到每一条真实的SQL究竟需要在哪个库的哪张分表上执行,最终把改写后的真实SQL下发到对应的分表上进行多线程的执行。而用户会将拿到最终汇总后的数据结果集。

ShardingSphere

挑战性: 尽管分片解决了性能,可用性以及单节点备份和恢复等问题,但其分布式体系结构还带来了一些新问题。一个问题是,面对如此分散的数据库和表时,应用程序开发工程师和数据库管理员的操作变得格外费力。他们应该确切地知道从哪个数据库表中获取数据。另一个挑战是,在单节点数据库中正确运行的 SQL在分片数据库中可能不合适。分片后表名称的更改,或由分页,排序依据或聚合分组依据等操作引起的不当行为就是这种情况。

跨数据库事务处理也是分布式数据库需要处理的棘手事情。当单表数据量减少时,合理使用分片表还可以充分利用本地事务。通过明智地使用同一数据库中的不同表,可以避免分布式事务带来的麻烦。当无法避免跨数据库事务时,某些企业仍然需要保持事务一致。互联网巨头尚未大规模采用基于XA的分布式事务,因为它们无法确保其在高并发情况下的性能。它们通常用最终一致的软状态代替强一致的事务。

目标: ShardingSphere数据分片模块的主要设计目标是试图减少分片的影响,以使用户像一个数据库一样使用水平分片数据库组。

# 三、快速入门

【1】将${latest.release.version}更改为实际的版本号,我使用的是4.0.1版本。

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-core</artifactId>
    <version>${latest.release.version}</version>
</dependency>
1
2
3
4
5

【2】数据分片+读写拆分规则配置:Sharding-JDBC可以通过 JavaYAMLSpring命名空间和 Spring Boot Starter四种方式配置,这里使用 SpringBoot进行配置。

spring:
  shardingsphere:
    #展示实际操作数据库的语句
    props:
      sql.show: true
    #数据库服务器地址
    datasource:
      names: ds0,ds0_slave0,ds0_slave1,ds1,ds1_slave0,ds1_slave1
      ds0:
        #hikari的高性能得来益于最大源限度的避免锁竞争。
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        #ds0 数据源,这里的从库跟主服务器在同一台机器上,生产上是分开的。
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxxx
      ds0_slave0:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds0_slave0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxxx
      ds0_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds0_slave1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxxx
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxx
      ds1_slave0:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1_slave0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxx
      ds1_slave1:
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 5000
          idle-timeout: 12000
          max-lifetime: 900000
          minimum-idle: 10
          maximum-pool-size: 100
          connection-test-query: SELECT 1
        driverClassName: com.mysql.cj.jdbc.Driver
        jdbcUrl: jdbc:mysql://127.0.0.1:3306/ds1_slave1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: xxx
 
    #读写拆分
    masterSlaveRules:
      ms_ds0:
        masterDataSourceName: ds0
        slaveDataSourceNames:
          - ds0_slave0
          - ds0_slave1
        loadBalanceAlgorithmType: ROUND_ROBIN
      ms_ds1:
        masterDataSourceName: ds1
        slaveDataSourceNames:
          - ds1_slave0
          - ds1_slave1
        loadBalanceAlgorithmType: ROUND_ROBIN
 
    #数据分片
    sharding:
      tables:
        # role 是表的名称,不同的表都需要配置分片规则,这也是 ShandingSphere 的缺点。
        role:
          actualDataNodes: ms_ds${0..1}.role${0..1}
          databaseStrategy:
            inline:
              #id 是role表的字段
              shardingColumn: id
              algorithmExpression: ds${id % 2}
          tableStrategy:
            inline:
              #f_id 是role表的字段
              shardingColumn: f_id
              algorithmExpression: role${id % 2}
            keyGenerator:
              #主见通过雪花算法生成
              type: SNOWFLAKE
              column: id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

【3】创建数据源: 使用ShardingDataSourceFactory和规则配置对象来创建ShardingDataSource,它是通过标准JDBC接口DataSource实现的。然后,用户可以使用本机JDBCJPAMyBatis(我们使用)和其他ORM框架进行开发。

DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, props);
1

# 四、读写拆分

随着系统TPS的增加,数据库容量面临巨大的瓶颈效应。对于具有大量并发读取操作但同时写入操作较少的应用程序系统,我们可以将数据库分为主数据库和从数据库。主数据库负责事务的添加,删除和修改,而从数据库负责查询。通过有效地避免由于数据更新而引起的行锁定,可以显着提高整个系统的查询性能。一个具有多个从属数据库的主数据库可以通过将查询平均分配到多个数据副本中来进一步增强系统处理能力。具有多个从数据库的多个主数据库不仅可以提高系统吞吐量,而且可以提高系统可用性。因此,即使任何数据库已关闭或物理磁盘已损坏,系统仍可以正常运行。与根据分片键将数据分离到所有节点的水平分片不同,读写拆分路由根据SQL含义分析将读操作和写操作分别写入主数据库和从属数据库。读写拆分节点中的数据是一致的,而水平分片中的数据则不一致。水平分片和读写拆分的组合使用将有效地提高系统性能。

【挑战性】: 尽管读写拆分可以提高系统吞吐量和可用性,但它还会带来不一致的数据,包括多个主数据库之间以及主数据库和从数据库之间的数据。此外,它还带来与数据分片相同的问题,使应用程序开发人员和操作员的维护与操作复杂化。下图显示了将分片表和数据库与读写拆分一起使用时,应用程序和数据库组之间的复杂拓扑关系。

ShardingSphere

【主从复制】: 它是指将数据从主数据库异步复制到从数据库的操作。由于主从异步,它们之间可能存在短期数据不一致。

【负载均衡策略】: 通过这种策略,查询被分离到不同的从数据库。

# 五、问题及解决方案

ShardingSphere

【1】SQL兼容程度: 通过上面的讲解,大家可以看到使用上任何一款分布式数据库中间件都会面临一个问题:SQL是否全支持?因为一条不含分片信息的SQL是需要经过解析、改写、路由、执行、归并这些步骤的,所以对SQL的加工处理,有可能会致使中间件对于部分SQL是不支持的。在真正落地XX业务时,也出现了这个问题。XX业务的业务逻辑非常复杂且庞大,同时多样化场景的需求对SQL的兼容程度有较高要求。ShardingSphere为了能全面支撑XX业务,进行了两方面的优化重构:
 ▄ 一方面是重构了SQL解析模块;
 ▄ 另一方面是在除了解析模块之外的模块对更多的SQL进行兼容支持,例如COUNT(DISTINCT *) 等SQL。

SQL解析模块是中间件的基石,如果基石不牢靠,上层建筑将岌岌可危。从第一代的解析引擎使用 Druid的内置解析引擎到第二代自研了 SQL解析引擎,再到现在使用 Antlr解析器作为 SQL解析器,经历了2年之久。耗时费力如此之多,只为了真正搭建好基石,做到解析引擎自主可控、对SQL高效全面支持。当前,SQL支持情况为:
 ➊ 路由至单节点,SQL100%支持;
 ➋ 路由至多节点,全面支持DQL、DML、DDL、DCL、TCL和MySQL的部分DAL。支持分页、去重、排序、分组、聚合、关联查询(不支持跨库关联);

【2】分布式主键: 传统数据库软件开发中,主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持,比如MySQL的自增键、Oracle的自增序列等。数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。目前有许多第三方解决方案可以完美解决这个问题,如 UUID等依靠特定算法自生成不重复键,或者通过引入主键生成服务等。为了方便用户使用、满足不同用户不同使用场景的需求,ShardingSphere提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE等分布式主键生成器,用户仅需简单配置即可使用,生成全局性的唯一自增ID。此外,我们还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成算法,以满足用户特殊场景的需求。

【3】业务分片键值注入: 通过解析SQL语句提取分片键列与值并进行分片,是ShardingSphere对SQL零侵入的实现方式。若SQL语句中没有分片条件,则无法进行分片,需要全路由。在一些应用场景中,分片条件并不存在于SQL,而存在于外部业务逻辑。因此需要提供一种通过外部指定分片结果的方式,在 ShardingSphere中叫做Hint。ShardingSphere使用 ThreadLocal管理分片键值。可以通过编程的方式向 HintManager中添加分片条件,该分片条件仅在当前线程内生效。除了通过编程的方式使用强制分片路由,ShardingSphere还计划通过SQL中的特殊注释的方式引用Hint,使开发者可以采用更加透明的方式使用该功能。指定了强制分片路由的SQL将会无视原有的分片逻辑,直接路由至指定的真实数据节点。下面的图片将给出这一场景的具体实施案例:

ShardingSphere

通过向 HintManager注入 status和具体路由表的关系,ShardingSphere将按照用户指定规则,强制到 db_0.t_order_1执行SQL,并将结果返回给用户。

【4】性能优化: 性能问题是任何一个上线系统在面临业务高峰时都必须要考虑的问题。面对XX白条这个量级的应用,ShardingSphere为了满足白条业务对TPS/QPS的强制要求,做了多方面优化,主要为:
 ★ SQL解析结果缓存;
 ★ JDBC元数据信息缓存;
 ★ Bind表&广播表的使用;
 ★ 自动化执行引擎&流式归并;
受篇幅所限,这里主要为大家介绍 Bind表和广播表使用。这两种表的配置使用,主要是为了优化表关联问题中,切分表与切分表之间笛卡尔积表关联的情况,以及解决跨库表关联不支持的情况。

绑定表是指分片规则一致的主表和子表。例如:t_order表和t_order_item表,均按照order_id分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,从而关联查询效率将大大提升。因为主表和子表使用相同的分片策略,数据在主表和子表的分布情况将一模一样,所以表关联查询的时候就能避免笛卡尔积。举例说明,如果SQL为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
1

在不配置绑定表关系时,假设分片键order_id将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
1
2
3
4

在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
1
2

其中 t_order在 FROM的最左侧,ShardingSphere将会以它作为整个绑定表的主表。所有路由计算将会只使用主表的策略,那么t_order_item表的分片计算将会使用 t_order的条件。故绑定表之间的分区键要完全相同。广播表是指所有底层分片数据源中都存在的表,表结构和表中的数据在每个分库中完全一致。这样在进行关联查询的时候,由于广播表在所有分库均存在,就避免了笛卡尔积关联查询以及跨库关联的情况。比较适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

#

(adsbygoogle = window.adsbygoogle || []).push({});