借力Apache ShardingSphere轻松上手分库分表

518 阅读11分钟

借力Apache ShardingSphere轻松上手分库分表

“兵者不祥之器,不得已而用之”

引自《道德经·第三十一章》

分库分表的概念

分库分表顾名思意,即把原有的数据库表按一定的逻辑拆分成多个库或表,已减轻IO压力,提高并发下的TPS。

根据拆分的方向不同可分为垂直拆分和水平拆分,这里主要讨论的是水平拆分,也即数据分片的能力。

垂直拆分

按照业务的聚合进行拆分,

在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案。

垂直分片

水平拆分

分库分表这个概念大家应该不陌生,这里主要指的是水平分库分表。不是按照业务进行归类重新分配到不同的库表的垂直拆分。

相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。

水平分片

分库分表何时用

分库分表的应用场景:

  1. 数据量过大且膨胀过快:当单个数据库表的数据量达到千万级甚至亿级以上时,查询性能会显著下降,特别是对于涉及大量索引扫描、全表扫描或者JOIN操作的SQL查询。分库分表可以将大表的数据分布到多个物理数据库或表中,减少单表数据量,提高查询和写入速度。
  2. 高并发访问:在高并发环境下,单个数据库实例可能无法处理大量的并发请求,导致响应时间变长,系统性能瓶颈出现。通过分库分表,可以分散读写压力,实现水平扩展,提高系统的整体吞吐量。
  3. 业务拆分需求:随着业务发展,不同业务模块之间可能存在不同的访问模式和负载特性,为了更好的隔离性和管理性,可以通过垂直分库将关联度较低的不同业务模块数据放入不同的数据库中。
  4. 分布式事务支持:对于需要支持分布式事务的大型系统,合理的分库分表设计可以帮助控制事务边界,降低分布式事务的影响范围。
  5. 资源限制:单台服务器(包括CPU、内存、磁盘I/O)的硬件资源有限,通过分库分表可以利用多台服务器的资源,避免单一资源成为瓶颈。

不适合的场景:

  1. 小规模应用: 如果应用数据量相对较小,单一数据库能够轻松管理,并且性能满足需求,那么引入分库分表的复杂性可能是不必要的。
  2. 业务不需要横向扩展: 如果业务发展预期不是特别迅猛,不需要频繁的横向扩展,而且单一数据库能够满足当前和未来一段时间的需求,那么分库分表可能是过度设计。有些单表总量很大的数据表,如果膨胀不是很快,而是积累了很多冷的历史数据,可以考虑定期迁出冷数据,而不需要盲目的使用分库分表策略。
  3. 数据关联性强: 当数据表之间有强烈的关联性,而且需要频繁进行关联查询时,引入分库分表可能会增加查询的复杂性和性能开销。
  4. 对系统复杂性要求低: 引入分库分表会增加系统的复杂性,包括对分布式事务、数据迁移、数据一致性等方面的处理。如果对系统的复杂性要求较低,那么可以考虑暂时不引入分库分表。
  5. 维护成本考虑: 分库分表引入了数据迁移、水平扩展、分布式事务等新的维护工作,维护成本较高,可以考虑暂时不采用分库分表。
  6. 业务场景不支持分布式: 有些业务场景不容易适应分布式系统,例如某些涉及到强一致性、低延迟等特性的业务,可能更适合在单一数据库中进行处理。

分库分表中慎重考虑的问题

  1. 查询跨库困难: 在分库分表的环境下,涉及跨库查询的复杂性增加,因为需要在多个数据库中进行联合查询。如果系统的查询模式不适合这种分布式的查询方式,分库分表可能不是一个好的选择。譬如不涉及分片键的聚合查询的情况,往往性能是很低的,这时需要引入其他的存储查询方式。
  2. 事务复杂性: 分布式事务的处理相对复杂,特别是在分库分表的情况下,需要谨慎处理跨库的事务一致性问题。如果业务逻辑对于强一致性要求非常高,而且难以处理分布式事务,那么分库分表可能不是最佳方案。
  3. 数据关联性强: 如果业务中的数据表之间有很强的关联性,而且常常需要进行关联查询,而且不是绑定表和绑定键的关联查询,那么在分库分表的情况下可能会带来额外的复杂性和性能开销。
  4. 数据分布不均匀: 如果按照某个字段进行分库分表,而该字段的取值分布非常不均匀,可能导致一些库或表的负载过重,而另一些却相对空闲,影响整体性能。

分库分表是不得已而为之的重器,不是银弹,引入它之后,系统复杂度会变高,不得已而用之。

ShardingSphere能干什么

Apache ShardingSphere 是一款分布式 SQL 事务和查询引擎,可通过数据分片、弹性伸缩、加密等能力对任意数据库进行增强。到目前为止,它已经发展到支持数据分片,分布式事务,读写分离,数据库网关,流量治理,数据迁移,数据加密,数据脱敏,影子库,可观察性等能力。

数据分片或者说数据库的水平分片是ShardingSphere开源之初就支持的首要能力。

数据分片中表的概念

表是透明化数据分片的关键概念。 Apache ShardingSphere 通过提供多样化的表类型,适配不同场景下的数据分片需求。

1 逻辑表

相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。 例:订单数据根据主键尾数拆分为2 张表,分别是 t_order_0 和 t_order_1,他们的逻辑表名为 t_order

2 真实表

在水平拆分的数据库中真实存在的物理表。 即上个示例中的 t_order_0 到t_order_1

3 绑定表

指分片规则一致的一组分片表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。 例如:t_order 表和 t_order_item 表,均按照 order_id 分片,并且使用 order_id 进行关联,则此两张表互为绑定表关系。 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。

4 广播表

指所有的数据源中都存在的表,表结构及其数据在每个数据库中均完全一致。 适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。

5 单表

指所有的分片数据源中仅唯一存在的表。 适用于数据量不大且无需分片的表。

如何用:sharding-jdbc的简单示例

以下演示使用sharding-jdbc在基于Spring Boot 2.x + Mybatis代码框架针对MySql数据库分库分表的使用。

1 引入maven依赖

引入sharding-jdbc的核心依赖,project.version是指定的版本号,我这里使用的是5.4.0。

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core</artifactId>
            <version>${project.version}</version>
        </dependency>

2 配置属性

application.properties的示例如下

mybatis.mapper-locations=classpath*:mappers/*Mapper.xml

spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
spring.datasource.url=jdbc:shardingsphere:classpath:config.yaml
  1. spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver

    • 这是Spring Boot数据源的驱动类的配置。

      • 使用的是Apache ShardingSphere提供的ShardingSphereDriver,这是一个适配ShardingSphere JDBC的JDBC驱动实现,可以通过配置变更即可使用,透明地处理分库分表、读写分离等分布式数据库策略,无需改动代码。
  2. spring.datasource.url=jdbc:shardingsphere:classpath:config.yaml

    • 这个配置项设置了连接到数据库的URL

      • jdbc:shardingsphere:开头表明这是ShardingSphere的特殊URL形式,它告诉应用程序使用ShardingSphere来管理数据库连接。
      • classpath:config.yaml 表示ShardingSphere的配置信息存储在一个名为config.yaml的YAML文件中,并且该文件位于项目的类路径下。
      • 在这个YAML配置文件中,我们指定分片规则、数据源配置以及其他相关的ShardingSphere功能配置。通过这个配置文件,ShardingSphere能够根据业务需求对数据库操作进行路由、分片和聚合等处理。

接下来,我们看看config.yaml的具体配置示例:

mode:
  type: Standalone
  repository:
    type: JDBC
    props:
      path: demo
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    maxPoolSize: 10
  ds_1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/demo_ds_1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    maxPoolSize: 10

rules:
- !SHARDING
    tables:
      t_order:
        actualDataNodes: ds_$->{0..1}.t_order_$->{0..1}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: t_order_inline
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake_generator
      t_order_item:
        actualDataNodes: ds_$->{0..1}.t_order_item_$->{0..1}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: t_order_item_inline
        keyGenerateStrategy:
          column: order_item_id
          keyGeneratorName: snowflake_generator
    bindingTables:
      - t_order,t_order_item
    defaultDatabaseStrategy:
      standard:
        shardingColumn: user_id
        shardingAlgorithmName: database_inline

    shardingAlgorithms:
      database_inline:
        type: INLINE
        props:
          algorithm-expression: ds_${user_id % 2}
          
      t_order_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_$->{order_id % 2}
      t_order_item_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_item_$->{order_id % 2}
    keyGenerators:
      snowflake_generator:
        type: SNOWFLAKE
    
- !BROADCAST
    tables:
      - t_address

props:
  sql-show: true
  1. mode 部分:

    • type: Standalone 表明使用独立模式运行ShardingSphere,适用于单机Java应用程序。生产环境建议配置成集群模式,使用zookeeper作为分布式协同工具。
  2. repository 部分:

    • type: JDBC 指定元数据存储方式为JDBC方式。
    • props 下的 path: demo 是JDBC元数据存储相关的属性,这里指定使用的数据库名为前缀是demo。
  3. dataSources 部分:

    • 定义了2个数据源:ds_0, ds_1 均为HikariCP连接池,拼上前缀即为
    • 各个数据源配置了数据库驱动类名、连接URL、用户名、密码以及最大连接数等信息,它们分别指向不同的MySQL数据库实例。
  4. rules 部分:

    • - !SHARDING 开始定义分片规则。

      • tables:

        • t_order 和 t_order_item 两张表的分片策略和键生成策略被定义在这里。

          • actualDataNodes:定义实际的数据节点分布,如 ds_$->{0..1}.t_order_$->{0..1} 表示订单表会被分到ds_0或ds_1的t_order_0或t_order_1表中,根据shardingColumn进行路由。
          • tableStrategy:标准分片策略,指定了分片列和对应的分片算法名称。
          • keyGenerateStrategy:主键生成策略,包括生成列名和生成器名称。
      • bindingTables

        • t_order,* 表示这两张表是绑定表,它们会根据相同的分片键order_id*进行关联分片。
      • defaultDatabaseStrategy

        • 定义默认数据库级别的分片策略,当没有在表级别指定时使用此策略。此处按照user_id取模来决定数据落到哪个数据库。
      • shardingAlgorithms

        • 定义了三个内联(inline)类型的分片算法,分别为database_inline、t_order_inline和t_order_item_inline,
        • 通过algorithm-expression表达式确定具体分片逻辑。
      • keyGenerators

        • 定义了一个雪花算法(SNOWFLAKE)类型的主键生成器snowflake_generator,用于生成分布式唯一ID。
    • - !BROADCAST

      • 表示t_address表采用广播表策略,即该表会在所有数据库中完全复制一份,便于各个数据库实例都能快速访问到该表的数据。
  5. props 部分:

    • sql-show: true 设置为true表示开启SQL显示功能,方便查看经过ShardingSphere处理后的实际执行SQL语句

以上,我们提前构建好2个database: demo_ds_0和demo_ds_1,有两张逻辑表t_order,和t_order_item ,他们同时也是绑定表,依据order_id

绑定在一起。这两张表按user_id%2分库,按order_id%2分表。最后t_address时广播表,每个库都会有一份。

这里需要注意的是,示例采用的是单机模式,生产环境推荐使用集群模式,使用zookeeper作为配置中心。参考配置如下

mode:
  type: Cluster
  repository:
    type: ZooKeeper
    props: 
      namespace: governance
      server-lists: localhost:2181
      retryIntervalMilliseconds: 500
      timeToLiveSeconds: 60

使用持久化仓库需要额外引入对应的 Maven 依赖,zookeeper作为持久化仓库 引入的依赖如下:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-cluster-mode-repository-zookeeper</artifactId>
    <version>${project.version}</version>
</dependency>

3 sql准备

根据上面配置,我们需要准备创建2个database即可,表的创建将在代码中实现。

DROP DATABASE IF EXISTS demo_ds;
DROP DATABASE IF EXISTS demo_ds_0;
DROP DATABASE IF EXISTS demo_ds_1;
CREATE DATABASE demo_ds;
CREATE DATABASE demo_ds_0;
CREATE DATABASE demo_ds_1;
t_order,t_order_item,t_address order_id,user_id
t_order_0,t_order_1

4 代码示例

初始化逻辑表

    private void initEnvironment() throws SQLException {
        orderRepository.createTableIfNotExists();
        orderItemRepository.createTableIfNotExists();
        addressRepository.createTableIfNotExists();
        orderRepository.truncateTable();
        orderItemRepository.truncateTable();
        addressRepository.truncateTable();
    }

sql代码按逻辑表操作即可。如下是创建t_order逻辑的mapper xml代码。

    <update id="createTableIfNotExists">
        CREATE TABLE IF NOT EXISTS t_order (order_id BIGINT AUTO_INCREMENT, order_type INT(11), user_id INT NOT NULL, address_id BIGINT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id));
    </update>

实际执行完成之后实际会创建t_order_0,t_order_1两张物理表。

常见的CRUD操作,代码操作逻辑表即可,不需要指定物理表。

注意事项

有限支持的 SQL

  • 使用 JDBC 规范 getGeneratedKeys 接口返回自增主键时,需要配合使用支持自增的分布式主键生成器,不支持其他类型的分布式主键生成器

不支持的 SQL

  • CASE WHEN 中包含子查询
  • CASE WHEN 中使用逻辑表名(请使用表别名)
  • INSERT INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ?(SELECT 子句不支持 * 和内置分布式主键生成器)
  • REPLACE INTO tbl_name (col1, col2, …) SELECT * FROM tbl_name WHERE col3 = ?(SELECT 子句不支持 * 和内置分布式主键生成器)
  • SELECT MAX(tbl_name.col1) FROM tbl_name(查询列是函数表达式时,查询列前不能使用表名,可以使用表别名)

其他

  • 分片规则中配置的真实表、分片列和分布式序列需要和数据库中的列保持大小写一致。

分布式主键

使用分库分表策略的表,不可使用物理表的主键自增方式生成主键,同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。

虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。针对需要分库分表的情况,需采用特定的算法生成全局唯一的不重复主键。Apache ShardingSphere 不仅提供了内置的分布式主键生成器,例如 UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

示例代码获取

以上示例的完整版可以在ShardingSphere的源码中获取。获取方法如下:

1 下载和安装shardingSphere

## 下载源码
git clone https://github.com/apache/shardingsphere.git

## 编译代码
cd shardingsphere
./mvnw clean install -DskipITs -DskipTests -Prelease

这里需要注意的是如果用的是windows系统,需要解决Git克隆时文件名过长的问题。

由于 Windows 版本的 Git 是使用 msys 编译的,它使用了旧版本的 Windows Api,限制文件名不能超过 260 个字符。 解决方案如下: 打开 cmd.exe(你需要将 git 添加到环境变量中)并执行下面的命令,可以让 git 支持长文件名:

git config --global core.longpaths true

如果是 Windows 10以上的系统,还需要通过注册表或组策略,解除操作系统的文件名长度限制(需要重启):

在注册表编辑器中创建 HKLM\SYSTEM\CurrentControlSet\Control\FileSystem LongPathsEnabled, 类型为 REG_DWORD,并设置为1

2 生成示例代码

  • 配置YML文件

    源码的examples目录下,是独立的maven工程,用来生成示例代码的,在路径examples/shardingsphere-jdbc-example-generator/src/main/resources/config.yaml中配置代码,可修改数据库地址,运行模式,代码框架,事务类型等等。

    名称描述选项
    mode模式:standlone或clustercluster-zookeeper, cluster-etcd, standalone
    transaction事务类型local, xa-atomikos, xa-narayana
    features特性设置sharding, readwrite-splitting, encrypt, shadow, mask
    frameworks支持的框架设置jdbc, spring-boot-starter-jdbc, spring-boot-starter-jpa, spring-boot-starter-mybatis, spring-namespace-jpa, spring-namespace-mybatis
    host数据库host
    port数据库 port
    username数据库用户名
    password数据库密码
  • 生成代码

运行 org.apache.shardingsphere.example.generator.ExampleGeneratorMain ↳

运行成功后生成代码如下: target/generated-sources/shardingsphere-${product}-sample

示例中给出的代码位于sharding--spring-boot-starter-mybatis--standalone--local

总结

以上,Apache ShardingSphere 是一款分布式数据库生态系统,它常用来作为分库分表的中间件,功能强大,上手简单,开源生态良好,是国内最流行的开源的分库分表解决方案。这里简单做了一个示例帮助不了解的小伙伴快速入门。如果需要更深入的了解,还得去官网多学习学习。

官网地址:shardingsphere.apache.org/

github:github.com/apache/shar…