Skip to content

分库分表

介绍

MySQL分库分表是一个比较复杂的话题,它指的是将一个大型数据库拆分成多个更小的数据库(分库)和/或将一个数据库中的表拆分成多个更小的表(分表)。这样做主要为了解决单机数据库性能瓶颈,提高数据库的扩展性和可用性。 但同时也带来了数据一致性、分布式事务等新的挑战。

让我们从几个方面来探讨MySQL分库分表:

1. 为什么需要分库分表?

  • 单机数据库性能瓶颈: 当数据量过大,单机数据库的性能无法满足需求时,需要进行分库分表。 这包括查询速度变慢、写入速度变慢、磁盘I/O压力过大等。
  • 单点故障: 依赖单机数据库存在单点故障的风险,一旦数据库服务器宕机,整个系统就会瘫痪。分库分表可以提高系统的可用性。
  • 硬件成本: 单机数据库的硬件成本会随着数据量的增加而线性增长,分库分表可以降低硬件成本。

2. 分库分表策略:

  • 水平分库分表: 这是最常用的策略,指的是按照一定的规则将数据分散到不同的数据库和表中。常见的规则包括:

    • 按用户ID分库分表: 将用户数据按照用户ID的范围分到不同的数据库或表中。
    • 按地理位置分库分表: 将数据按照地理位置(例如省份、城市)分到不同的数据库或表中。
    • 按哈希值分库分表: 使用哈希函数对主键进行计算,然后根据哈希值将数据分到不同的数据库或表中。 这可以更加均衡地分配数据。
    • 按日期分库分表: 将数据按照日期分到不同的数据库或表中。 适合具有时间属性的数据。
  • 垂直分库分表: 指的是将数据库中的表按照功能模块进行拆分,将不同的表放到不同的数据库中。 例如,可以将用户信息表和订单信息表放到不同的数据库中。

3. 分库分表带来的挑战:

  • 数据一致性: 在分库分表后,需要保证数据的一致性,这需要使用分布式事务或其他一致性保证机制。
  • 全局ID生成: 需要一个全局ID生成器来保证ID的唯一性。
  • 跨库查询: 在分库分表后,跨库查询变得更加复杂,需要使用中间件或其他技术来解决。
  • 数据迁移: 分库分表后,可能需要进行数据迁移,这需要一个完善的数据迁移方案。
  • 复杂度增加: 分库分表增加了系统的复杂度,需要更高的运维成本。

4. 常用的中间件:

  • MyCat: 一个开源的MySQL数据库中间件,可以实现分库分表,以及其他一些高级功能。
  • Sharding-JDBC: 一个开源的JDBC代理,可以实现分库分表,无需修改代码。
  • 360 Atlas: 360公司开源的数据库中间件,功能强大,性能优异。

5. 选择合适的策略:

选择合适的策略需要根据实际情况,考虑数据量、查询模式、业务场景等因素。 如果数据量不大,或者查询模式比较简单,则不需要进行分库分表。 如果数据量很大,或者查询模式比较复杂,则需要选择合适的策略进行分库分表。

总而言之,MySQL分库分表是一个复杂的系统工程,需要仔细规划和设计。 在进行分库分表之前,需要充分评估其利弊,并选择合适的策略和中间件。 你有什么具体的问题吗? 例如,你目前的数据库面临什么挑战? 你想了解哪方面的细节? 告诉我更多信息,我可以更针对性地回答你的问题。

为何需要分库分表的 PoC

todo 使用分区表技术是否能够解决 PoC 问题

场景描述

在电商场景中,订单数据是海量的。在海量数据情况下新增、查询订单的性能要求也是很高的。需求如下:

  • 新增订单
  • 根据订单 ID 查询
  • 用户查询订单
    • 用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。
    • 用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。
  • 商家查询订单
    • 商家查询指定日期范围+所有状态的订单,查询条件商家ID+订单删除状态+日期范围+分页 ,新的订单排在最前。
    • 商家查询指定日期范围+指定状态的订单,查询条件商家ID+订单状态+订单删除状态+日期范围+分页 ,新的订单排在最前。

PoC 过程

PoC 使用的本站 示例

VMware ESXi, 7.0.3, 20328353,Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz

一个数据库实例:8C8G、CentOS8

一个 OpenResty 反向代理实例:8C8G、CentOS8

四个 SpringBoot 应用实例:8C8G、CentOS8

一个 wrk 压力机:10C10G、CentOS8

内核参数文件描述符限制调优(在所有主机中):参考 链接

修改示例中 db/Dockerfile 基础镜像,如下:

dockerfile
FROM mariadb:11.4

修改 application.properties 中的配置指向数据库服务

properties
spring.datasource.url=jdbc:mariadb://192.168.1.190:3306/demo?useSSL=false&allowPublicKeyRetrieval=true

编译并推送镜像

bash
./build.sh && ./push.sh

准备数据库服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-poc
  • 启动数据库服务

    bash
    cd ~/deployer-poc/common && docker compose down -v && docker compose pull && docker compose up -d

准备 SpringBoot 应用服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-poc
  • 启动 SpringBoot 服务

    bash
    cd ~/deployer-poc/service && docker compose pull && docker compose up -d

准备 OpenResty 反向代理服务

  • 复制 代码 到 OpenResty 反向代理实例中

  • 修改 nginx.conf 指向各个 SpringBoot 应用,如下:

    upstream backend {
        server 192.168.1.187:8080;
        server 192.168.1.188:8080;
        server 192.168.1.193:8080;
        server 192.168.1.194:8080;
    }
  • 启动服务

    bash
    docker compose up -d

使用示例中的 rest-api.http 测试接口是否正常

准备 wrk 压力机

500万数据

安装 Apache Bench 工具:参考 链接

初始化数据

bash
ab -n 5000000 -c 64 -k http://192.168.1.185/api/v1/order/create

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID

bash
cd ~/deployer-poc/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/getById
    Running 15s test @ http://192.168.1.185/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    75.36ms   71.48ms 883.57ms   62.86%
        Req/Sec     4.00k   381.05     5.16k    66.42%
      Latency Distribution
         50%   63.17ms
         75%  109.87ms
         90%  170.14ms
         99%  311.79ms
      477663 requests in 15.05s, 266.00MB read
    Requests/sec:  31729.06
    Transfer/sec:     17.67MB

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    86.60ms   85.10ms 994.00ms   81.65%
        Req/Sec     3.54k   279.97     4.36k    69.25%
      Latency Distribution
         50%   73.65ms
         75%  131.35ms
         90%  195.32ms
         99%  370.37ms
      422916 requests in 15.04s, 211.44MB read
    Requests/sec:  28123.32
    Transfer/sec:     14.06MB

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    66.68ms   65.07ms 814.06ms   75.56%
        Req/Sec     4.56k   316.15     5.90k    72.25%
      Latency Distribution
         50%   50.04ms
         75%   95.86ms
         90%  146.21ms
         99%  285.84ms
      544781 requests in 15.05s, 152.34MB read
    Requests/sec:  36203.51
    Transfer/sec:     10.12MB

测试商家查询指定日期范围+所有状态的订单,查询条件商家ID+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.07s   719.06ms   6.29s    66.51%
        Req/Sec   120.41     85.84   686.00     69.97%
      Latency Distribution
         50%    2.12s 
         75%    2.21s 
         90%    3.13s 
         99%    4.24s 
      13576 requests in 15.06s, 73.98MB read
    Requests/sec:    901.19
    Transfer/sec:      4.91MB

测试商家查询指定日期范围+指定状态的订单,查询条件商家ID+订单状态+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.13s   921.72ms   7.84s    59.46%
        Req/Sec   116.40     81.38   600.00     69.16%
      Latency Distribution
         50%    2.21s 
         75%    2.36s 
         90%    3.36s 
         99%    4.65s 
      13244 requests in 15.05s, 17.56MB read
    Requests/sec:    879.85
    Transfer/sec:      1.17MB

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/create
    Running 15s test @ http://192.168.1.185/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   226.85ms  193.31ms   2.45s    73.69%
        Req/Sec     1.28k   201.98     1.70k    74.85%
      Latency Distribution
         50%  185.01ms
         75%  308.02ms
         90%  475.73ms
         99%  893.34ms
      150750 requests in 15.06s, 36.52MB read
    Requests/sec:  10007.00
    Transfer/sec:      2.42MB

1000万数据

安装 Apache Bench 工具:参考 链接

初始化数据

bash
ab -n 10000000 -c 64 -k http://192.168.1.185/api/v1/order/create

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID

bash
cd ~/deployer-poc/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/getById
    Running 15s test @ http://192.168.1.185/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    78.25ms   74.69ms 882.89ms   61.67%
        Req/Sec     3.89k   303.83     4.80k    68.58%
      Latency Distribution
         50%   65.03ms
         75%  116.28ms
         90%  176.62ms
         99%  325.18ms
      464612 requests in 15.05s, 258.79MB read
    Requests/sec:  30870.73
    Transfer/sec:     17.19MB

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    92.18ms   81.54ms 828.07ms   63.75%
        Req/Sec     3.19k   210.79     3.89k    70.33%
      Latency Distribution
         50%   86.80ms
         75%  132.82ms
         90%  197.47ms
         99%  354.56ms
      380810 requests in 15.05s, 297.85MB read
    Requests/sec:  25302.52
    Transfer/sec:     19.79MB

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    79.08ms   77.28ms 988.26ms   77.58%
        Req/Sec     3.83k   384.73     8.66k    73.92%
      Latency Distribution
         50%   55.69ms
         75%  109.90ms
         90%  180.02ms
         99%  339.42ms
      456812 requests in 15.04s, 153.49MB read
    Requests/sec:  30371.01
    Transfer/sec:     10.20MB

测试商家查询指定日期范围+所有状态的订单,查询条件商家ID+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.63s   913.85ms   8.63s    66.99%
        Req/Sec    88.98     45.32   390.00     71.55%
      Latency Distribution
         50%    2.71s 
         75%    2.87s 
         90%    3.95s 
         99%    5.44s 
      10339 requests in 15.05s, 70.99MB read
    Requests/sec:    686.98
    Transfer/sec:      4.72MB

测试商家查询指定日期范围+指定状态的订单,查询条件商家ID+订单状态+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     4.17s     1.77s   13.61s    65.38%
        Req/Sec    52.69     32.77   242.00     75.16%
      Latency Distribution
         50%    4.39s 
         75%    4.94s 
         90%    6.60s 
         99%    9.45s 
      5896 requests in 15.07s, 14.48MB read
    Requests/sec:    391.37
    Transfer/sec:      0.96MB

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/create
    Running 15s test @ http://192.168.1.185/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   220.94ms  186.91ms   2.13s    73.75%
        Req/Sec     1.27k   200.46     1.73k    76.25%
      Latency Distribution
         50%  185.37ms
         75%  301.02ms
         90%  460.18ms
         99%  863.56ms
      151455 requests in 15.06s, 36.69MB read
    Requests/sec:  10053.75
    Transfer/sec:      2.44MB

2000万数据

安装 Apache Bench 工具:参考 链接

初始化数据

bash
ab -n 20000000 -c 64 -k http://192.168.1.185/api/v1/order/create

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID

bash
cd ~/deployer-poc/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/getById
    Running 15s test @ http://192.168.1.185/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    73.51ms   69.61ms 745.84ms   62.05%
        Req/Sec     4.04k   361.41     5.16k    72.50%
      Latency Distribution
         50%   62.19ms
         75%  106.76ms
         90%  163.51ms
         99%  302.40ms
      482404 requests in 15.05s, 269.23MB read
    Requests/sec:  32062.33
    Transfer/sec:     17.89MB

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   102.87ms   84.36ms 908.02ms   67.80%
        Req/Sec     2.73k   188.46     3.25k    70.50%
      Latency Distribution
         50%   94.01ms
         75%  143.47ms
         90%  219.16ms
         99%  370.95ms
      325595 requests in 15.05s, 437.57MB read
    Requests/sec:  21636.41
    Transfer/sec:     29.08MB

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    79.85ms   71.02ms 789.30ms   64.36%
        Req/Sec     3.67k   235.42     5.96k    72.50%
      Latency Distribution
         50%   51.70ms
         75%  112.27ms
         90%  177.94ms
         99%  315.78ms
      438313 requests in 15.05s, 196.53MB read
    Requests/sec:  29119.58
    Transfer/sec:     13.06MB

测试商家查询指定日期范围+所有状态的订单,查询条件商家ID+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.62s   859.24ms   7.96s    69.68%
        Req/Sec    91.60     65.47   830.00     76.10%
      Latency Distribution
         50%    2.70s 
         75%    2.84s 
         90%    3.92s 
         99%    5.31s 
      10302 requests in 15.06s, 71.66MB read
    Requests/sec:    684.17
    Transfer/sec:      4.76MB

测试商家查询指定日期范围+指定状态的订单,查询条件商家ID+订单状态+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     8.44s     3.15s   15.01s    62.39%
        Req/Sec    28.64     28.43   280.00     86.38%
      Latency Distribution
         50%    8.43s 
         75%   10.67s 
         90%   12.81s 
         99%   14.51s 
      2305 requests in 15.06s, 10.78MB read
    Requests/sec:    153.07
    Transfer/sec:    732.82KB

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/create
    Running 15s test @ http://192.168.1.185/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   219.23ms  185.31ms   2.43s    75.79%
        Req/Sec     1.28k   282.11     1.94k    82.97%
      Latency Distribution
         50%  179.89ms
         75%  295.62ms
         90%  452.18ms
         99%  869.48ms
      152491 requests in 15.04s, 36.95MB read
    Requests/sec:  10136.67
    Transfer/sec:      2.46MB

5000万数据

安装 Apache Bench 工具:参考 链接

初始化数据

bash
ab -n 50000000 -c 64 -k http://192.168.1.185/api/v1/order/create

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID

bash
cd ~/deployer-poc/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/getById
    Running 15s test @ http://192.168.1.185/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    95.06ms   85.99ms   1.12s    64.04%
        Req/Sec     3.11k   290.56     3.92k    66.17%
      Latency Distribution
         50%   80.09ms
         75%  138.18ms
         90%  205.66ms
         99%  377.63ms
      371500 requests in 15.05s, 207.18MB read
    Requests/sec:  24679.58
    Transfer/sec:     13.76MB

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   410.63ms  217.84ms   2.04s    81.48%
        Req/Sec   618.96     96.43     1.11k    73.00%
      Latency Distribution
         50%  411.12ms
         75%  580.75ms
         90%  633.87ms
         99%    1.03s 
      73968 requests in 15.04s, 221.51MB read
    Requests/sec:   4917.26
    Transfer/sec:     14.73MB

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByUserIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   162.20ms  114.19ms   1.19s    77.52%
        Req/Sec     1.64k   147.40     2.37k    74.42%
      Latency Distribution
         50%  168.00ms
         75%  215.41ms
         90%  333.17ms
         99%  517.68ms
      195657 requests in 15.06s, 152.32MB read
    Requests/sec:  12990.14
    Transfer/sec:     10.11MB

测试商家查询指定日期范围+所有状态的订单,查询条件商家ID+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.97s     1.01s    9.03s    67.73%
        Req/Sec    79.03     48.80   505.00     67.50%
      Latency Distribution
         50%    3.09s 
         75%    3.24s 
         90%    4.48s 
         99%    6.10s 
      8946 requests in 15.05s, 62.32MB read
    Requests/sec:    594.51
    Transfer/sec:      4.14MB

测试商家查询指定日期范围+指定状态的订单,查询条件商家ID+订单状态+订单删除状态+日期范围+分页 ,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
    Running 15s test @ http://192.168.1.185/api/v1/order/listByMerchantIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    11.07s     3.99s   15.02s    83.17%
        Req/Sec    18.13     22.48   151.00     88.73%
      Latency Distribution
         50%   12.95s 
         75%   14.07s 
         90%   14.72s 
         99%   15.00s 
      909 requests in 15.07s, 6.31MB read
    Requests/sec:     60.32
    Transfer/sec:    428.55KB

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d15s --latency --timeout 30 http://192.168.1.185/api/v1/order/create
    Running 15s test @ http://192.168.1.185/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   248.52ms  206.44ms   2.64s    76.48%
        Req/Sec     1.11k   266.26     1.76k    82.60%
      Latency Distribution
         50%  209.49ms
         75%  331.90ms
         90%  522.49ms
         99%  961.20ms
      126299 requests in 15.06s, 30.62MB read
    Requests/sec:   8388.34
    Transfer/sec:      2.03MB

PoC 结论

大部分业务查询在数据量超过 1000w 后,性能会随着数据量继续增加呈现明显下降趋势,所以一般单表数据不宜超过 1000w。

分库分表的方式

todo MySQL 垂直分表、垂直分库、水平分表、水平分库 PoC

让我们用一个电商系统的例子来说明 MySQL 的垂直分表、垂直分库、水平分表、水平分库。假设这个电商系统包含以下几张表:

  • users (用户表): user_id, username, password, email, address, phone, register_time ...
  • products (商品表): product_id, product_name, description, price, stock, category_id ...
  • orders (订单表): order_id, user_id, product_id, order_time, total_amount, status ...
  • order_items (订单详情表): item_id, order_id, product_id, quantity, price ...

1. 垂直分表 (Vertical Table Sharding):

  • 原理: 将一张表拆分成多张表,每张表包含原表的一部分列。 目的是降低单表数据量,提升查询速度。

  • 例子:users 表拆分成 users_profileusers_info 两张表:

    • users_profile: user_id, username, password (核心信息)
    • users_info: user_id, email, address, phone, register_time (扩展信息)
  • 优点: 降低单表数据量,提高查询特定字段的效率。

  • 缺点: 需要 JOIN 操作才能获取完整用户信息,增加了查询复杂度,可能会降低效率,尤其是在数据量很大的情况下。

2. 垂直分库 (Vertical Database Sharding):

  • 原理: 将不同的表放在不同的数据库实例中。

  • 例子:

    • db_user: users_profile, users_info
    • db_product: products
    • db_order: orders, order_items
  • 优点: 隔离不同业务模块的数据,降低单库压力,提高可用性。 不同的业务模块可以使用不同规格的数据库实例,节省资源。

  • 缺点: 跨库 JOIN 查询变得非常复杂,性能严重下降,增加了分布式事务的难度。

3. 水平分表 (Horizontal Table Sharding):

  • 原理: 将一张表的数据拆分到多个表中,每个表存储原表的一部分数据。

  • 例子:orders 表水平拆分成 orders_0, orders_1, orders_2 三个表,例如使用 order_id % 3 的方式:

    • orders_0: order_id % 3 == 0 的订单
    • orders_1: order_id % 3 == 1 的订单
    • orders_2: order_id % 3 == 2 的订单
  • 优点: 能够处理海量数据,提高数据库的读写性能。

  • 缺点: 需要使用分片键进行路由,增加应用层复杂度;跨分片查询困难;需要考虑数据一致性问题。

4. 水平分库 (Horizontal Database Sharding):

  • 原理: 将一张表的数据拆分到多个数据库实例中,每个数据库实例存储表的一部分数据。

  • 例子:orders 表水平拆分到 db_order_0, db_order_1, db_order_2 三个数据库实例中,同样使用 order_id % 3 的方式。

    • db_order_0: orders 表 (存储 order_id % 3 == 0 的订单)
    • db_order_1: orders 表 (存储 order_id % 3 == 1 的订单)
    • db_order_2: orders 表 (存储 order_id % 3 == 2 的订单)
  • 优点: 大幅提高数据库处理能力,提升扩展性。

  • 缺点: 实现复杂,需要考虑数据一致性、事务、路由等问题;跨库查询变得非常困难;数据迁移和维护成本高。

总结:

不同的分库分表方式适用于不同的场景。 垂直分表和垂直分库比较容易实现,但扩展性有限;水平分表和水平分库扩展性好,但实现复杂。 实际应用中,常常会结合多种方式来解决问题,例如,可以先进行垂直分库,再对某些大表进行水平分库分表。 选择合适的方案需要根据具体的业务需求、数据量、性能要求等因素综合考虑。 同时,也需要选择合适的中间件来简化分库分表的开发和维护工作。

分库分表带来的新问题

  • 事务一致性问题
  • 跨节点关联查询
  • 跨节点分页、排序
  • 主键冲突
  • 公共表

分库分表算法

基因算法

详细细节请参考本站 示例

java
/**
 * 分库分表策略的基因算法
 * https://blog.csdn.net/qq_42875345/article/details/132662916
 */
@Test
public void testDbShardingGeneAlgorithm() {
    Long originalOrderId = IdUtil.getSnowflakeNextId();
    String str = Long.toBinaryString(originalOrderId);
    System.out.println(str + "(原始订单ID)");

    // 原始订单ID去除后4位
    Long orderId = originalOrderId >> 4 << 4;
    str = Long.toBinaryString(orderId);
    System.out.println(str + "(去除后4位后)");

    Long originalUserId = RandomUtil.randomLong(0, Long.MAX_VALUE);
    str = Long.toBinaryString(originalUserId);
    System.out.println(str + "(原始用户ID)");

    // 只保留用户ID的后4位作为订单ID的基因值
    Long userId = originalUserId % 16;
    str = Long.toBinaryString(userId);
    System.out.println(str + "(用户ID后4位)");

    // 替换拼接用户ID基因值到订单ID后4位中
    orderId = orderId | userId;
    str = Long.toBinaryString(orderId);
    System.out.println(str + "(订单ID和用户ID后4位替换拼接后)");

    // 通过订单ID计算分片索引
    System.out.println((orderId % 16 % 5 + 1) + "(订单ID计算得到的分片索引)");
    // 通过用户ID计算分片索引
    System.out.println((originalUserId % 16 % 5 + 1) + "(用户ID计算得到的分片索引)");

    // 追加拼接用户ID基因值到订单ID后
    userId = originalUserId % 16;
    str = Long.toBinaryString(originalOrderId) + StringUtils.leftPad(Long.toBinaryString(userId), 4, "0");
    System.out.println(str + "(订单ID和用户ID后4位追加拼接后)");

    BigInteger bigInteger = new BigInteger(str, 2);
    System.out.println(bigInteger.toString(10) + "(订单ID和用户ID后4位拼接后的十进制数)");

    // 通过订单ID计算分片索引
    bigInteger = bigInteger.mod(new BigInteger("16")).mod(new BigInteger("5")).add(new BigInteger("1"));
    System.out.println(bigInteger.intValue() + "(订单ID计算得到的分片索引)");
    // 通过用户ID计算分片索引
    System.out.println((originalUserId % 16 % 5 + 1) + "(用户ID计算得到的分片索引)");
}

Sharding-JDBC

介绍

Sharding-JDBC 是一个开源的 Java 框架,用于实现 MySQL 的分库分表功能。它通过在 JDBC 层面进行拦截,无需修改原有的数据库访问代码,即可实现数据库分片。 这意味着你只需要在你的 Spring Boot 或其他 Java 应用中添加 Sharding-JDBC 的依赖,并进行简单的配置,就可以将你的数据库访问自动路由到正确的数据库和表中,而无需修改大量的业务代码。

主要功能和特性:

  • 透明化分库分表: 无需修改原有 SQL 语句,Sharding-JDBC 会自动根据配置的规则将 SQL 语句路由到正确的数据库和表。
  • 支持多种分片策略: 支持多种分片算法,包括哈希分片、范围分片、列表分片等,可以根据实际情况选择合适的策略。
  • 数据一致性保证: 提供多种数据一致性解决方案,例如通过分布式事务或乐观锁来保证数据的一致性。
  • 灵活的配置: 通过配置文件或代码配置来实现分库分表,方便灵活地调整配置。
  • 易于扩展: Sharding-JDBC 提供了丰富的扩展接口,方便用户自定义扩展功能。
  • 完善的监控和管理: 提供监控和管理功能,方便用户监控分库分表的状态和性能。
  • 支持多种数据库: 虽然主要针对 MySQL,但理论上支持其他多种关系型数据库。

核心组件:

  • Sharding-JDBC-Core: 核心组件,提供分库分表、数据路由等功能。
  • Sharding-JDBC-Proxy: 代理组件,在数据库连接层实现分库分表,无需修改应用代码。
  • Sharding-JDBC-Spring: Spring 集成组件,方便 Sharding-JDBC 与 Spring 框架集成。

使用场景:

  • 海量数据存储: 处理单表数据量过大的问题。
  • 高并发访问: 提高数据库的并发处理能力。
  • 水平扩展: 方便地进行数据库水平扩展。

优点:

  • 易于使用: 配置简单,无需修改大量代码。
  • 高性能: 性能损耗较低。
  • 功能完善: 提供丰富的功能,满足各种分库分表需求。
  • 开源免费: 可以免费使用。

缺点:

  • 学习成本: 需要学习 Sharding-JDBC 的配置和使用方式。
  • 复杂性: 对于复杂的场景,配置和维护可能比较复杂。
  • 依赖 Java: 只能在 Java 应用中使用。

与其他分库分表方案的比较:

相比于其他分库分表方案(例如自行编写代码实现分库分表),Sharding-JDBC 提供了更完善的功能和更简单的使用方式,减少了开发和维护的成本。 但它也有一定的学习成本,并且在极端复杂的场景下可能需要更深入的了解和定制。

总而言之,Sharding-JDBC 是一个功能强大、易于使用的 MySQL 分库分表框架,适合大多数需要进行分库分表的 Java 应用。 但需要根据实际情况选择合适的策略和配置,并充分了解其优缺点。

基本概念

Sharding-JDBC 的基本概念围绕着如何将一个大的数据库水平拆分成多个小的数据库(分库)以及一个大的表水平拆分成多个小的表(分表)来实现数据库水平扩展。 以下是几个核心概念:

1. 分库分表 (Sharding): 这是 Sharding-JDBC 的核心功能。它允许将数据库和表水平拆分成多个物理单元,以提高数据库的处理能力和存储容量。

  • 分库 (Sharding Databases): 将一个逻辑数据库拆分成多个物理数据库实例。例如,一个用户表可以被拆分成 user_db_0, user_db_1, user_db_2 等多个数据库。
  • 分表 (Sharding Tables): 将一个逻辑表拆分成多个物理表。例如,一个订单表可以被拆分成 order_table_0, order_table_1, order_table_2 等多个表,这些表可以分布在不同的数据库实例上,也可以在一个数据库实例上。

2. 逻辑表和物理表 (Logical Table & Actual Table):

  • 逻辑表 (Logical Table): 应用程序看到的表,它代表了所有分片表的集合。 应用程序不需要关心底层的分库分表细节,只需要操作逻辑表。
  • 物理表 (Actual Table): 实际存在于数据库中的表。 一个逻辑表对应多个物理表。

3. 数据分片策略 (Sharding Strategy): 决定如何将数据分片到不同的物理数据库和表中。 常用的策略包括:

  • 范围分片 (Range Sharding): 根据数据的某个范围进行分片。 例如,用户 ID 从 1 到 1000 的数据放在数据库 0,1001 到 2000 的数据放在数据库 1。
  • 哈希分片 (Hash Sharding): 使用哈希函数将数据映射到不同的分片。 例如,使用用户 ID 的哈希值对分片数量取模,来确定数据应该放在哪个分片。
  • 广播表 (Broadcast Table): 将表的数据复制到所有分片上。 常用于一些公共数据表。
  • 键值分片(Key Sharding): 基于主键或者其他唯一键进行分片,类似于哈希分片,但是对键的处理更加直接。

4. 分片键 (Sharding Key): 用于确定数据应该分片到哪个数据库或表的键。 数据分片策略依赖于分片键。

5. 配置文件 (Configuration File): 用于配置 Sharding-JDBC 的各种参数,例如数据源、分库分表策略、路由规则等等。 通常使用 YAML 或 JSON 文件进行配置。

6. 路由 (Routing): Sharding-JDBC 根据分片键和配置的路由规则,确定 SQL 语句应该发送到哪个物理数据库或表上执行。

7. 透明化 (Transparency): Sharding-JDBC 对应用程序是透明的,应用程序不需要修改代码就能使用分库分表的功能。

8. 分布式事务 (Distributed Transaction): 在跨多个数据库分片的事务场景下,Sharding-JDBC 提供了分布式事务管理,保证数据一致性,这部分通常是最复杂的部分,需要选择合适的策略。

9. 数据节点: “数据节点”指的是实际存储数据的数据库实例。 它可以是一个单一的数据库服务器,也可以是数据库集群中的一个成员。 Sharding-JDBC 将逻辑数据库和表拆分成多个数据节点,实现水平扩展。数据节点概念与逻辑数据库和逻辑表密切相关。 一个逻辑数据库可以对应多个数据节点,一个逻辑表也可以对应多个数据节点上的多个物理表。 数据节点的具体数量和配置都取决于你的分库分表策略和应用需求。

理解这些基本概念是使用 Sharding-JDBC 的前提。 通过灵活运用这些概念和功能,可以方便地进行数据库水平扩展,提高数据库的性能和可用性。

sharding-jdbc 和 MyBatis、SpringBoot 版本兼容性

  • org.apache.shardingsphere:sharding-jdbc-spring-boot-starter 4.0.0-RC2 到 4.1.1 和 SpringBoot 2.2.7.RELEASE、mybatis-spring-boot-starter 2.3.2 兼容
  • org.apache.shardingsphere:sharding-jdbc-spring-boot-starter 4.0.0-RC1 和 SpringBoot 2.2.7.RELEASE、mybatis-spring-boot-starter 2.3.2 不兼容
  • org.apache.shardingsphere:shardingsphere-jdbc-core-spring-boot-starter 5.x 和 SpringBoot 任何版本、mybatis-spring-boot-starter 任何版本不兼容
  • org.apache.shardingsphere:shardingsphere-jdbc-core 5.4.x 和 SpringBoot 3.4.0、mybatis-spring-boot-stater 3.0.4 不兼容
  • org.apache.shardingsphere:shardingsphere-jdbc-core 5.1.2、5.2.x、5.3.x 和 SpringBoot 3.4.0、mybatis-spring-boot-starter 3.0.4、org.yaml:snakeyaml 1.33 兼容

和 SpringBoot 3.4.0 项目集成

https://blog.csdn.net/JingAi_jia917/article/details/140318444

https://shardingsphere.apache.org/document/5.3.2/en/quick-start/shardingsphere-jdbc-quick-start/

详细用法请参考本站 示例

尝试过 SpringBoot 3.x 项目和 org.apache.shardingsphere:sharding-jdbc-spring-boot-starter、org.apache.shardingsphere:shardingsphere-jdbc-core-spring-boot-starter 集成都不能成功启动项目,最终成功和 org.apache.shardingsphere:shardingsphere-jdbc-core 5.1.2、5.2.x、5.3.x 集成。

提示:使用 org.apache.shardingsphere:shardingsphere-jdbc-core 依赖,需要使用 sharding.yaml 配置 sharding-jdbc。

项目 POM 配置如下:

xml
<properties>
    <maven.compiler.target>17</maven.compiler.target>
    <maven.compiler.source>17</maven.compiler.source>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
    </dependency>
    
    <!-- sharding-jdbc 依赖 -->
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core</artifactId>
        <version>5.3.2</version>
    </dependency>
    <dependency>
        <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId>
        <version>1.33</version>
    </dependency>

    <!-- mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.4</version>
    </dependency>
    <!-- 分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.4.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Spring 数据源 application.properties 配置如下:

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

sharding-jdbc 的 sharding.yaml 配置文件如下:

yaml
dataSources:
  ds1:
    # 数据源使用的连接池类型
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: org.mariadb.jdbc.Driver
    jdbcUrl: jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true
    username: root
    password: 123456

rules:
  - !SHARDING
    tables:
      # 配置逻辑表order的分片规则
      order:
        # 列id使用内置的雪花算法生成分布式id
        keyGenerateStrategy:
          column: id
          keyGeneratorName: snowflake
        # 指定order表的数据分布情况,配置数据节点
        actualDataNodes: ds1.order_$->{1..2}
        # order表的分片策略,分片策略包括分片键和分片算法
        tableStrategy:
          standard:
            shardingColumn: id
            shardingAlgorithmName: order_inline
    # 配置order_inline分片算法
    shardingAlgorithms:
      order_inline:
        type: inline
        props:
          algorithm-expression: order_$->{id % 2 + 1}
    # 配置主键自动生成算法
    keyGenerators:
      snowflake:
        type: SNOWFLAKE

# 打开sql输出日志
props:
  sql-show: true

基于 MyBatis 的业务 SQL 编写只需要使用逻辑表 order 编写,不需要引用底层实际表。

和 SpringBoot 2.2.7.RELEASE 项目集成

详细用法请参考本站 示例

org.apache.shardingsphere:sharding-jdbc-spring-boot-starter 4.0.0-RC2 到 4.1.1 和 SpringBoot 2.2.7.RELEASE、mybatis-spring-boot-starter 2.3.2 兼容

项目 POM 配置如下:

xml
<properties>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
    </dependency>
    <!-- sharding-jdbc 依赖 -->
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
        <version>4.1.1</version>
    </dependency>

    <!-- mybatis依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.2</version>
    </dependency>
    <!-- 分页插件 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

application.properties 中的 sharding-jdbc 配置如下:

properties
spring.shardingsphere.datasource.names=ds1
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456

# 指定t_order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE
# 指定order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds1.order_$->{1..2}
# order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.inline.algorithm-expression=order_$->{id % 2 + 1}

# 打开sql输出日志
spring.shardingsphere.props.sql.show=true

基于 MyBatis 的业务 SQL 编写只需要使用逻辑表 order 编写,不需要引用底层实际表。

无法处理 LocalDateTime 数据类型问题

https://blog.51cto.com/u_11906056/7036294

sharding-jdbc 和 SpringBoot 2.2.7.RELEASE(和 SpringBoot 3.4.0 集成不存在此问题)集成后,在处理 LocalDateTime 数据类型时报告错误 org.springframework.dao.InvalidDataAccessApiUsageException: Error attempting to get column 'create_time' from result set. Cause: java.sql.SQLFeatureNotSupportedException: getObject with type ; getObject with type; nested exception is java.sql.SQLFeatureNotSupportedException: getObject with type,通过添加 MyBatis LocalDateTime 类型处理 typehandler 解决此问题:

java
@Component
public class LocalDateTimeTypeHandler extends org.apache.ibatis.type.LocalDateTimeTypeHandler {
    public LocalDateTimeTypeHandler() {
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException {
        ps.setTimestamp(i, new Timestamp(this.toTimeMillis(parameter)));
    }

    @Override
    public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
        Timestamp sqlTimestamp = rs.getTimestamp(columnName);
        return sqlTimestamp != null ? this.toLocalDateTime(sqlTimestamp.getTime()) : null;
    }

    @Override
    public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        Timestamp sqlTimestamp = rs.getTimestamp(columnIndex);
        return sqlTimestamp != null ? this.toLocalDateTime(sqlTimestamp.getTime()) : null;
    }

    @Override
    public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        Timestamp sqlTimestamp = cs.getTimestamp(columnIndex);
        return sqlTimestamp != null ? this.toLocalDateTime(sqlTimestamp.getTime()) : null;
    }

    private long toTimeMillis(LocalDateTime dateTime) {
        return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
    }

    private LocalDateTime toLocalDateTime(long timeMillis) {
        return new java.util.Date(timeMillis).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }
}

配置基于雪花算法的自动生成ID

提醒:指定 key-generator.type=SNOWFLAKE 同时也在 insert 中指定手动赋值并插入 id,此时 sharding-jdbc 会采用指定的 id 而不会自动生产 id

详细用法请参考本站 示例

application.properties 配置如下:

properties
# 指定order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE

OrderMapper 的新增方法 SQL 如下:

java
@Insert("insert into `order`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time," +
        "received_time,cancel_time,delete_status) " +
        "values(#{createTime},#{userId},#{merchantId},#{totalAmount},#{totalCount}," +
        "#{status},#{payTime},#{deliveryTime},#{receivedTime},#{cancelTime},#{deleteStatus})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void add(Order order);

不需要插入主键ID

java
@SpringBootTest
public class OrderTests {

    @Resource
    OrderMapper orderMapper;

    @Test
    public void test() {
        this.orderMapper.truncate();

        for (int i = 0; i < 5; i++) {
            Order order = new Order();
            LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
            order.setCreateTime(now);
            order.setUserId(RandomUtil.randomLong());
            order.setMerchantId(RandomUtil.randomLong());
            order.setTotalAmount(new BigDecimal(1000));
            order.setTotalCount(10);
            order.setStatus(Status.Unpay);
            order.setDeleteStatus(DeleteStatus.Normal);
            this.orderMapper.add(order);

            Long id = order.getId();

            order = this.orderMapper.get(id);
            Assertions.assertEquals(id, order.getId());
            Assertions.assertEquals(Status.Unpay, order.getStatus());
            Assertions.assertEquals(now, order.getCreateTime());
        }
    }
}

插入 Order 时候不需要设置ID,因为会自动生成。

配置选项

spring.shardingsphere.sharding.tables.order.actual-data-nodes

用于配置数据的数据源和数据表的分布节点信息。

properties
spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2}

上面的配置表示,order 表的数据分布在 ds1.order_1、ds1.order_2、ds2.order_1、ds2.order_2 物理表中。

标准分片策略(Standard Sharding Strategy)

策略特性:

  • 提供对SQL语句中的=INBETWEEN AND的分片操作支持。
  • 只支持单分片键。
  • 包含两个分片算法:PreciseShardingAlgorithm(用于处理=IN的分片)和RangeShardingAlgorithm(用于处理BETWEEN AND分片)。其中,PreciseShardingAlgorithm是必选的,而RangeShardingAlgorithm是可选的。

数据源分片配置

配置选项:

  • spring.shardingsphere.sharding.tables.order.database-strategy.standard.sharding-column:指定单一键作为数据源的分片键
  • spring.shardingsphere.sharding.tables.order.database-strategy.standard.precise-algorithm-class-name:指定 = 或者 IN 查询分片算法的实现类
  • spring.shardingsphere.sharding.tables.order.database-strategy.standard.range-algorithm-class-name:指定范围查询分片算法的实现类

样例:

properties
# 配置数据源分片算法
spring.shardingsphere.sharding.tables.order.database-strategy.standard.sharding-column=user_id
spring.shardingsphere.sharding.tables.order.database-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.database.MyDatabaseStandardPreciseShardingAlgorithm
# 配置范围分片算法,否则报告错误 Cause: java.lang.UnsupportedOperationException: Cannot find range sharding strategy in sharding rule.
spring.shardingsphere.sharding.tables.order.database-strategy.standard.range-algorithm-class-name=com.future.demo.algorithm.database.MyDatabaseStandardRangeShardingAlgorithm

当 DML SQL 中有 order 表时,都会调用相应的算法计算分片结果。

数据表分片配置

配置选项:

  • spring.shardingsphere.sharding.tables.order.table-strategy.standard.sharding-column:指定单一键作为表的分片键
  • spring.shardingsphere.sharding.tables.order.table-strategy.standard.precise-algorithm-class-name:指定 = 或者 IN 查询分片算法的实现类
  • spring.shardingsphere.sharding.tables.order.table-strategy.standard.range-algorithm-class-name:指定范围查询分片算法的实现类

样例:

properties
# 配置数据表分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.standard.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.table.MyTableStandardPreciseShardingAlgorithm
# 配置范围分片算法,否则报告错误 Cause: java.lang.UnsupportedOperationException: Cannot find range sharding strategy in sharding rule.
spring.shardingsphere.sharding.tables.order.table-strategy.standard.range-algorithm-class-name=com.future.demo.algorithm.table.MyTableStandardRangeShardingAlgorithm

当 DML SQL 中有 order 表时,都会调用相应的算法计算分片结果。

行表达式分片策略(Inline Sharding Strategy)

配置选项:

  • spring.shardingsphere.sharding.tables.order.database-strategy.inline.sharding-column:指定单一键作为数据源的分片键
  • spring.shardingsphere.sharding.tables.order.database-strategy.inline.algorithm-expression:使用 Groovy 的行表达式,对SQL语句中的=IN的计算分片
  • spring.shardingsphere.sharding.tables.order.table-strategy.inline.sharding-column:指定单一键作为表的分片键
  • spring.shardingsphere.sharding.tables.order.table-strategy.inline.algorithm-expression:使用 Groovy 的行表达式,对SQL语句中的=IN的计算分片

样例:

properties
# 配置order逻辑表的分库策略
spring.shardingsphere.sharding.tables.order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2 + 1}

## order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.inline.algorithm-expression=order_$->{id % 2 + 1}

执行原理

介绍

Sharding-JDBC 的执行流程比较复杂,但可以简要概括为以下几个阶段:

1. SQL 解析与路由:

  • SQL 解析: Sharding-JDBC 首先会拦截数据库操作的SQL语句。它会通过解析器将 SQL 语句解析成抽象语法树 (AST)。 这步至关重要,因为 Sharding-JDBC 需要理解 SQL 的含义才能进行路由。
  • 路由: 根据解析后的 SQL 语句,以及配置的路由规则,Sharding-JDBC 会决定将该 SQL 语句路由到哪个数据库分片(shard)上执行。 路由规则可以基于表名、字段值等多种条件。 这步决定了 SQL 语句最终会发送到哪个实际的数据库实例。 复杂的路由逻辑可能涉及多个分片。

2. SQL改写 (Rewrite):

  • 在路由之后,Sharding-JDBC 会根据路由结果,对 SQL 语句进行改写。 这主要包括:
    • 添加分片键: 如果 SQL 语句缺少分片键,Sharding-JDBC 会添加必要的条件来限制查询范围,确保只查询目标分片上的数据。
    • 修改表名: 将逻辑表名替换成实际分片表名。
    • 拆分 SQL 语句: 对于某些 SQL 语句(例如 JOIN 语句),Sharding-JDBC 可能会将其拆分成多个 SQL 语句,分别在不同的分片上执行,然后再将结果合并。

3. SQL 执行:

  • 改写后的 SQL 语句会被发送到对应的数据库分片上执行。 Sharding-JDBC 使用 JDBC 连接池来管理数据库连接。

4. 结果合并:

  • 如果 SQL 语句被拆分成了多个子查询,Sharding-JDBC 会将各个分片返回的结果进行合并,最终返回一个完整的查询结果集给应用程序。

5. 事务管理:

  • Sharding-JDBC 提供了分布式事务管理功能,确保跨多个分片的事务的一致性。 它支持多种分布式事务模式,例如 XA 事务、BASE 事务等等。 这部分是Sharding-JDBC最复杂的部分,依赖具体的配置和选择的分布式事务策略。

简化流程图:

[应用程序] --> [Sharding-JDBC拦截器] -->
    [SQL解析] --> [路由] --> [SQL改写] -->
    [发送到多个数据库分片] --> [数据库执行] -->
    [结果合并] --> [返回结果给应用程序]

关键组件:

  • 拦截器 (Interceptor): 拦截 JDBC 调用,并执行路由、改写等操作。
  • 路由算法 (Routing Algorithm): 决定将 SQL 语句路由到哪个数据库分片。
  • SQL 解析器 (SQL Parser): 解析 SQL 语句,生成 AST。
  • SQL 改写器 (SQL Rewriter): 根据路由结果改写 SQL 语句。
  • 结果合并器 (Result Merger): 将来自多个分片的查询结果合并。
  • 事务管理器 (Transaction Manager): 管理分布式事务。

需要注意的是,这只是一个高度简化的流程。 Sharding-JDBC 的实际执行流程会根据具体的配置、SQL 语句以及数据分片策略而有所不同。 理解其核心思想在于:它通过拦截、解析、路由和改写 SQL 语句,将一个针对逻辑数据库的请求转换为针对多个物理数据库分片的请求,并最终将结果合并,对应用程序屏蔽了数据库分片的细节。

结果归并算法有哪些呢?

Sharding-JDBC的结果归并算法从功能上主要分为以下几种类型,每种类型都服务于不同的查询需求,它们之间是组合而非互斥的关系:

  1. 遍历归并
    • 当只有一个数据节点返回结果集时,Sharding-JDBC会使用遍历归并。此时,归并过程相对简单,只需将单个结果集的数据返回即可。
  2. 排序归并
    • 当查询语句包含排序要求时,需要使用排序归并。
    • 排序归并属于流式归并的一种,通过优先级队列(如最小堆或最大堆)实现。
    • 初始时,会对各个数据节点返回的结果集进行初始排序,并将其头部元素放入优先级队列。
    • 每次获取元素时,从优先级队列中取出最小的(或最大的)元素,然后将其所在结果集的下一个元素放入队列,并重新调整队列的优先级。
    • 这样可以确保每次获取的元素都是全局有序的。
  3. 分组归并
    • 当查询语句包含分组要求时,需要使用分组归并。
    • 分组归并又可分为流式分组归并和内存分组归并两种。
      • 流式分组归并适用于分组字段较少且内存消耗不大的情况。它会逐条处理结果集中的数据,根据分组字段将数据分组存储。
      • 内存分组归并则适用于分组字段较多或内存消耗较大的情况。它会将结果集的所有数据加载到内存中,再进行分组处理。
  4. 分页归并
    • 当查询语句包含分页要求时,需要使用分页归并。
    • 分页归并通常是在其他归并类型的基础上进行的装饰者归并。
    • 它会根据分页参数(如偏移量offset和限制行数limit)对结果集进行裁剪,只返回所需的数据行。
  5. 聚合归并
    • 当查询语句包含聚合函数(如SUM、COUNT、AVG等)时,需要使用聚合归并。
    • 聚合归并会对各个数据节点返回的结果集进行聚合计算,返回最终的聚合结果。

此外,从结构划分上,Sharding-JDBC的结果归并算法还可以分为流式归并、内存归并和装饰者归并三种:

  1. 流式归并
    • 流式归并是指每次只处理结果集中的一条数据,通过逐条处理的方式返回正确的单条数据。
    • 它的优点是能够极大减少内存的消耗,适用于结果集较大的情况。
    • 遍历、排序以及流式分组都属于流式归并的一种。
  2. 内存归并
    • 内存归并是指将结果集的所有数据都加载到内存中,再进行归并处理。
    • 它的优点是处理速度快,但缺点是内存消耗较大。
    • 内存分组归并属于内存归并的一种。
  3. 装饰者归并
    • 装饰者归并是指在其他归并类型的基础上进行的进一步处理。
    • 它的优点是可以灵活地对结果集进行各种处理,如分页、过滤等。
    • 分页归并通常是在其他归并类型的基础上进行的装饰者归并。

综上所述,Sharding-JDBC提供了多种结果归并算法以满足不同的查询需求。这些归并算法在功能上是组合而非互斥的,可以根据实际情况进行选择和组合使用。

分片策略和分片算法

介绍

Sharding-JDBC 的分片策略和分片算法是其实现数据分片功能的核心组件。以下是对这两者的详细解释:

一、分片策略(Sharding Strategy)

分片策略决定了数据应该被分配到哪些数据源或表中。Sharding-JDBC 提供了多种分片策略,以满足不同的业务需求。

  1. 标准分片策略(Standard Sharding Strategy)
    • 提供对SQL语句中的=INBETWEEN AND的分片操作支持。
    • 只支持单分片键。
    • 包含两个分片算法:PreciseShardingAlgorithm(用于处理=IN的分片)和RangeShardingAlgorithm(用于处理BETWEEN AND分片)。其中,PreciseShardingAlgorithm是必选的,而RangeShardingAlgorithm是可选的。
  2. 复合分片策略(Complex Sharding Strategy)
    • 同样提供对SQL语句中的=INBETWEEN AND的分片操作支持。
    • 支持多分片键。由于多分片键之间的关系复杂,因此Sharding-JDBC并未做过多的封装,而是直接将分片键值组合以及分片操作符交于算法接口,由应用开发者自行实现,以提供最大的灵活度。
  3. 行表达式分片策略(Inline Sharding Strategy)
    • 使用Groovy的行表达式,提供对SQL语句中的=IN的分片操作支持。
    • 只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。例如,t_user_${user_id % 8}表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0t_user_7
  4. Hint分片策略(Hint Sharding Strategy)
    • 不通过SQL解析的方式分片,而是通过代码动态指定路由规则。
  5. 不分片策略(None Sharding Strategy)
    • 不进行分片操作。

二、分片算法(Sharding Algorithm)

分片算法是实现具体分片逻辑的关键。Sharding-JDBC并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供的分片算法接口主要有以下几种:

  1. PreciseShardingAlgorithm
    • 用于处理使用单一键作为分片键的=IN进行分片的场景。
    • 需要配合StandardShardingStrategy使用。
  2. RangeShardingAlgorithm
    • 用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。
    • 需要配合StandardShardingStrategy使用。
  3. ComplexKeysShardingAlgorithm
    • 用于处理使用多键作为分片键进行分片的场景。
    • 需要配合ComplexShardingStrategy使用。
  4. HintShardingAlgorithm
    • 用于处理使用Hint行分片的场景。
    • 需要配合HintShardingStrategy使用。

开发者可以根据自己的业务需求,实现上述接口中的doSharding方法,以定义具体的分片逻辑。例如,在实现PreciseShardingAlgorithm接口时,可以根据分片键的值来计算目标数据源或表的名称;在实现RangeShardingAlgorithm接口时,可以根据分片键的范围来确定目标数据源或表的集合。

总的来说,Sharding-JDBC的分片策略和分片算法为开发者提供了高度的灵活性和可扩展性,使得他们可以根据自己的业务需求来定制数据分片方案。

PreciseShardingAlgorithm

标准分片策略的精准分片算法。用于处理使用单一键作为分片键的=IN进行分片的场景。

详细用法请参考本站 示例

配置的详细解释请参考本站 链接

例子:

  • application.properties 配置如下:

    properties
    spring.shardingsphere.datasource.names=ds1,ds2
    spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
    spring.shardingsphere.datasource.ds1.driver-class-name=org.mariadb.jdbc.Driver
    spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true
    spring.shardingsphere.datasource.ds1.username=root
    spring.shardingsphere.datasource.ds1.password=123456
    
    spring.shardingsphere.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
    spring.shardingsphere.datasource.ds2.driver-class-name=org.mariadb.jdbc.Driver
    spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mariadb://localhost:3307/demo?allowMultiQueries=true
    spring.shardingsphere.datasource.ds2.username=root
    spring.shardingsphere.datasource.ds2.password=123456
    
    spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2}
    
    spring.shardingsphere.sharding.tables.order.key-generator.column=id
    spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE
    spring.shardingsphere.sharding.tables.order.key-generator.props.worker.id=1
    
    # 配置数据源分片算法
    spring.shardingsphere.sharding.tables.order.database-strategy.standard.sharding-column=user_id
    spring.shardingsphere.sharding.tables.order.database-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.database.MyDatabaseStandardPreciseShardingAlgorithm
    
    # 配置数据表分片算法
    spring.shardingsphere.sharding.tables.order.table-strategy.standard.sharding-column=id
    spring.shardingsphere.sharding.tables.order.table-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.table.MyTableStandardPreciseShardingAlgorithm
  • 数据源分片算法

    java
    /**
     * 数据源精准分片算法
     */
    @Slf4j
    public class MyDatabaseStandardPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        /**
         * @param collection           spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2} 中已配置的数据源
         * @param preciseShardingValue 分片上下文参数
         * @return 数据所在的数据源名称
         */
        @Override
        public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
            String logicTableName = preciseShardingValue.getLogicTableName();
            String columnName = preciseShardingValue.getColumnName();
            Long value = preciseShardingValue.getValue();
            log.info("All datasources {} logicTableName {} columnName {} value {}", collection, logicTableName, columnName, value);
    
            String dataSourceName = "ds" + (value % collection.size() + 1);
            if (!collection.contains(dataSourceName)) {
                throw new UnsupportedOperationException("数据源 " + dataSourceName + " 不存在");
            }
    
            return dataSourceName;
        }
    }
  • 数据表分片算法

    java
    /**
     * 数据表精准分片算法
     */
    @Slf4j
    public class MyTableStandardPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        /**
         * @param collection           spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2} 中已配置的数据源
         * @param preciseShardingValue 分片上下文参数
         * @return 数据所在的数据表名称
         */
        @Override
        public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
            String logicTableName = preciseShardingValue.getLogicTableName();
            String columnName = preciseShardingValue.getColumnName();
            Long value = preciseShardingValue.getValue();
            log.info("All tables {} logicTableName {} columnName {} value {}", collection, logicTableName, columnName, value);
    
            String tableName = logicTableName + "_" + (value % collection.size() + 1);
            if (!collection.contains(tableName)) {
                throw new UnsupportedOperationException("数据表 " + tableName + " 不存在");
            }
    
            return tableName;
        }
    }

RangeShardingAlgorithm

标准分片策略的范围分片算法。

详细用法请参考本站 示例

配置的详细解释请参考本站 链接

例子:

application.properties 配置如下:

properties
spring.shardingsphere.datasource.names=ds1,ds2
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456

spring.shardingsphere.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mariadb://localhost:3307/demo?allowMultiQueries=true
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=123456

spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2}

spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.props.worker.id=1

# 配置数据源分片算法
spring.shardingsphere.sharding.tables.order.database-strategy.standard.sharding-column=user_id
spring.shardingsphere.sharding.tables.order.database-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.database.MyDatabaseStandardPreciseShardingAlgorithm
# 配置范围分片算法,否则报告错误 Cause: java.lang.UnsupportedOperationException: Cannot find range sharding strategy in sharding rule.
spring.shardingsphere.sharding.tables.order.database-strategy.standard.range-algorithm-class-name=com.future.demo.algorithm.database.MyDatabaseStandardRangeShardingAlgorithm

# 配置数据表分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.standard.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.standard.precise-algorithm-class-name=com.future.demo.algorithm.table.MyTableStandardPreciseShardingAlgorithm
# 配置范围分片算法,否则报告错误 Cause: java.lang.UnsupportedOperationException: Cannot find range sharding strategy in sharding rule.
spring.shardingsphere.sharding.tables.order.table-strategy.standard.range-algorithm-class-name=com.future.demo.algorithm.table.MyTableStandardRangeShardingAlgorithm

数据源分片算法

java
/**
 * 数据源范围分片算法
 */
@Slf4j
public class MyDatabaseStandardRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
    /**
     * @param collection         spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2} 中已配置的数据源
     * @param rangeShardingValue
     * @return 数据所在的数据源名称集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
        String logicTableName = rangeShardingValue.getLogicTableName();
        String columnName = rangeShardingValue.getColumnName();
        Range<Long> valueRange = rangeShardingValue.getValueRange();
        log.info("All datasources {} logicTableName {} columnName {} valueRange {}", collection, logicTableName, columnName, valueRange);

        // 通过判断 valueRange 计算数据所在的数据源,这里简单地返回所有数据源
        return collection;
    }
}

数据表分片算法

java
/**
 * 数据表范围分片算法
 */
@Slf4j
public class MyTableStandardRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
    /**
     * @param collection         spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2} 中已配置的数据源
     * @param rangeShardingValue
     * @return 数据所在的数据表名称列表
     */
    @Override
    public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
        String logicTableName = rangeShardingValue.getLogicTableName();
        String columnName = rangeShardingValue.getColumnName();
        Range<Long> valueRange = rangeShardingValue.getValueRange();
        log.info("All tables {} logicTableName {} columnName {} value {}", collection, logicTableName, columnName, valueRange);

        // 通过判断 valueRange 计算数据所在的数据表,这里简单地返回所有数据源
        return collection;
    }
}

OrderMapper 范围查询

java
@Select("select * from `order` where user_id>=#{startUserId} and user_id<=#{endUserId}")
List<Order> listByUserIdRange(@Param("startUserId") Long startUserId, @Param("endUserId") Long endUserId);

范围查询测试代码

java
/**
 * 测试标准分片策略的范围分片算法
 */
@Test
public void testStandardStrategyRangeShardingAlgorithm() {
    List<Order> orderList = this.orderMapper.listByUserIdRange(1L, 10000L);
    Assertions.assertEquals(0, orderList.size());
}

不分片策略

通过配置默认数据源,没有配置分片策略的表会自动使用默认数据源操作数据库。

详细用法请参考本站 示例

properties
spring.shardingsphere.datasource.names=default-data-source

# 默认数据源,没有配置分片策略的,使用默认数据源
spring.shardingsphere.sharding.default-data-source-name=default-data-source
spring.shardingsphere.datasource.default-data-source.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.default-data-source.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.default-data-source.jdbc-url=jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.default-data-source.username=root
spring.shardingsphere.datasource.default-data-source.password=123456

ComplexKeysShardingAlgorithm

详细用法请参考本站 示例:通过订单 ID 或者用户 ID 查询订单信息。

参考链接

分库分表基因算法 和复合键分片算法结合实现通过订单ID或者用户ID查询订单信息。

算法实现

java
public class MyComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<BigDecimal> {
    /**
     * 订单id列名
     */
    private static final String COLUMN_ORDER_ID = "id";
    /**
     * 用户id列名
     */
    private static final String COLUMN_USER_ID = "userId";

    private static final BigInteger BIG_INTEGER_16 = new BigInteger("16");
    private static final BigInteger BIG_INTEGER_1 = new BigInteger("1");

    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<BigDecimal> shardingValue) {
        if (!shardingValue.getColumnNameAndRangeValuesMap().isEmpty()) {
            throw new RuntimeException("不支持除了=和in的操作");
        }

        // 获取订单id
        Collection<BigDecimal> orderIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_ORDER_ID, new ArrayList<>(1));
        // 获取客户id
        Collection<BigDecimal> userIds = shardingValue.getColumnNameAndShardingValuesMap().getOrDefault(COLUMN_USER_ID, new ArrayList<>(1));

        // 整合订单id和客户id
        List<BigDecimal> ids = new ArrayList<>();
        if (orderIds != null && !orderIds.isEmpty()) {
            ids.addAll(orderIds);
        }
        if (userIds != null && !userIds.isEmpty()) {
            ids.addAll(userIds);
        }

        // 通过订单ID和用户ID计算所有表名称
        return ids.stream()
                // 截取 订单号或客户id的后2位
                .map(id -> id.toBigInteger().mod(BIG_INTEGER_16))
                // 去重
                .distinct()
                // 对可用的表名求余数,获取到真实的表的后缀
                .map(idSuffix -> idSuffix.mod(new BigInteger(String.valueOf(availableTargetNames.size()))).add(BIG_INTEGER_1))
                // 获取到真实的表
                .map(tableSuffix -> availableTargetNames.stream().filter(targetName -> targetName.endsWith(String.valueOf(tableSuffix))).findFirst().orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
}

配置使用算法

properties
# 指定order表的数据分布情况,配置数据节点
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds1.t_order$->{1..8}
# order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.complex.sharding-columns=id,userId
spring.shardingsphere.sharding.tables.t_order.table-strategy.complex.algorithm-class-name=com.future.demo.util.MyComplexKeysShardingAlgorithm

分库分表数据平衡分布算法

下面演示使用用户id作为基因平衡地分布数据到 3x8 分库分表规模中。

java
@Test
public void test() {
    // 模拟分库分表,分为3个数据库,每个数据库8个表
    // 数据源分片总数
    int datasourceTotalShard = 3;
    // 每个数据源表分片总数
    int eachDatasourceTableTotalShard = 8;
    // 分片总数
    BigInteger totalShard = new BigInteger(String.valueOf(datasourceTotalShard * eachDatasourceTableTotalShard));
    BigInteger eachDatasourceTableTotalShardBigInteger = new BigInteger(String.valueOf(eachDatasourceTableTotalShard));
    // 保留用户id的位数
    int userIdBitsToRetian = 12;
    // 用于保留或者从订单id中提取指定位数用户id
    BigInteger shiftLeftBigInteger = BigInteger.valueOf((1L << userIdBitsToRetian) - 1L);
    // 用于保留或者从订单id中提取指定位数用户id
    long shiftLeftLong = (1L << userIdBitsToRetian) - 1L;

    Map<Integer, Integer> shardIndexToCount = new HashMap<>();
    Map<Integer, Integer> datasourceIndexToCount = new HashMap<>();
    Map<Integer, Integer> tableIndexCount = new HashMap<>();
    for (int i = 0; i < 10000 * 100; i++) {
        // 订单id
        Long orderId = RandomUtil.randomLong(1, Long.MAX_VALUE);
        // 用户id
        Long userId = RandomUtil.randomLong(1, Long.MAX_VALUE);

        // 保留用户id的后31位
        Long userIdStripOff = userId & shiftLeftLong;

        // 完整的订单id和用户截断id拼接为新的订单id(有用户id的基因)
        String orderIdBinaryStr = Long.toBinaryString(orderId) +
                StringUtils.leftPad(Long.toBinaryString(userIdStripOff), userIdBitsToRetian, "0");
        BigInteger orderIdBig = new BigInteger(orderIdBinaryStr, 2);

        // 从新的订单id中提取用户id基因并计算基于3x8分库分表的分片位置
        BigInteger shardIndex = orderIdBig.and(shiftLeftBigInteger).mod(totalShard);
        if (!shardIndexToCount.containsKey(shardIndex.intValue()))
            shardIndexToCount.put(shardIndex.intValue(), 0);
        shardIndexToCount.put(shardIndex.intValue(), shardIndexToCount.get(shardIndex.intValue()) + 1);

        // 计算数据源分片位置
        int datasourceIndex = shardIndex.divide(eachDatasourceTableTotalShardBigInteger).intValue();
        if (!datasourceIndexToCount.containsKey(datasourceIndex))
            datasourceIndexToCount.put(datasourceIndex, 0);
        datasourceIndexToCount.put(datasourceIndex, datasourceIndexToCount.get(datasourceIndex) + 1);

        // 计算表分片位置
        int tableIndex = shardIndex.subtract(BigInteger.valueOf((long) datasourceIndex * eachDatasourceTableTotalShard)).intValue();
        if (!tableIndexCount.containsKey(tableIndex))
            tableIndexCount.put(tableIndex, 0);
        tableIndexCount.put(tableIndex, tableIndexCount.get(tableIndex) + 1);
    }

    System.out.println("Shard index to count:");
    for (Integer key : shardIndexToCount.keySet()) {
        System.out.println("\t" + key + " = " + shardIndexToCount.get(key));
    }
    System.out.println();

    System.out.println("Datasource index to count:");
    for (Integer key : datasourceIndexToCount.keySet()) {
        System.out.println("\t" + key + " = " + datasourceIndexToCount.get(key));
    }
    System.out.println();

    System.out.println("Table index to count:");
    for (Integer key : tableIndexCount.keySet()) {
        System.out.println("\t" + key + " = " + tableIndexCount.get(key));
    }
}

配置 Hikari 连接池

详细用法请参考本站 示例

properties
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
# 设置 Hikari 连接池
spring.shardingsphere.datasource.ds1.maximum-pool-size=256
spring.shardingsphere.datasource.ds1.minimum-idle=10
spring.shardingsphere.datasource.ds1.driver-class-name=org.mariadb.jdbc.Driver
#spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://localhost:3307/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://10.138.0.9:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456

事务

参考链接

介绍

ShardingJDBC支持的分布式事务方式有三种 LOCAL, XA , BASE,这三种事务实现方式都是采用的对代码无侵入的方式实现的。

  • LOCAL 事务:本地事务方式也就是使用Spring的@Transaction注解来进行配置。传统的本地事务是不具备分布式事务特性的,但是ShardingSphere对本地事务进行了增强。在ShardingSphere中,LOCAL本地事务已经完全支持由于逻辑异常导致的分布式事务问题。不过这种本地事务模式IBU支持因网络、硬件导致的跨库事务。例如同一个事务中,跨两个库更新,更新完毕后,提交之前,第一个库宕机了,则只有第二个库数据提交。
  • XA 事务:这种模式下,是由ShardingJDBC所在的应用来作为事务协调者,通过XA方式来协调分布到多个数据库中的分库分表语句的分布式事务。
  • BASE 柔性事务:这种模式,是由Seata作为事务协调者,来进行协调。使用方式需要先部署seata服务。官方建议是使用seata配合nacos作为配置中心来使用。实际上是使用的seata的AT模式进行两阶段提交。

LOCAL 事务

参考本站示例的创建订单业务 链接1 或者 链接2

方式使用 @Transactional 注解即可

java
@Transactional(rollbackFor = Exception.class)
public void createOrderInternal(Long userId, Long productId, Integer amount) throws Exception {

XA 事务

todo

BASE 柔性事务

todo

水平分表

配置逻辑表 order 根据 id%2+1 分片到 order_1 和 order_2 表中

介绍

Sharding-JDBC水平分表是一种数据库分片技术,旨在通过横向拆分表来提高数据库的扩展性和性能。以下是对Sharding-JDBC水平分表的详细解释:

一、基本概念

  • 水平分表:又称为横向拆分,是指将一个表中的数据按照某种规则(如主键、时间等)分散到多个表中。每个表中的数据只是原表数据的一部分,但表的结构与原表相同。
  • Sharding-JDBC:是一个轻量级的Java框架,可以视为增强版的JDBC驱动。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖。Sharding-JDBC支持分库分表、读写分离、分布式主键等功能,并兼容JDBC和各种ORM框架。

二、实现原理

Sharding-JDBC水平分表的实现原理主要包括以下几个方面:

  • 分片规则:根据业务需求,定义数据分片的规则。例如,可以根据主键的奇偶性、时间范围、地域等字段进行分片。
  • 数据源配置:配置多个数据源,每个数据源对应一个或多个物理数据库。Sharding-JDBC会根据分片规则,将数据路由到相应的数据源。
  • SQL解析与改写:Sharding-JDBC会对SQL语句进行解析,根据分片规则将SQL语句改写为多个子查询,然后分别发送到对应的数据源执行。
  • 结果归并:将各个数据源返回的结果集进行归并处理,得到最终的结果集。Sharding-JDBC提供了多种结果归并算法,如遍历归并、排序归并、分组归并等。

三、配置与实现

Sharding-JDBC水平分表的配置与实现通常包括以下几个步骤:

  1. 引入Maven依赖:在项目的pom.xml文件中添加Sharding-JDBC的依赖。
  2. 配置数据源:在application.yml或application.properties文件中配置多个数据源,以及数据源之间的连接信息。
  3. 定义分片规则:在配置文件中定义分片规则,包括分片字段、分片策略等。
  4. 编写代码:根据业务需求编写代码,Sharding-JDBC会根据分片规则自动将数据路由到相应的数据源。

四、优点与限制

Sharding-JDBC水平分表的优点主要包括:

  • 提高数据库扩展性:通过水平分表,可以将数据分散到多个表中,从而提高数据库的存储能力和处理能力。
  • 提高查询性能:由于数据被分散到多个表中,查询时可以并行处理,从而提高查询性能。
  • 简化数据库维护:通过水平分表,可以将大表拆分成小表,便于管理和维护。

然而,Sharding-JDBC水平分表也存在一些限制:

  • 分片规则设计复杂:需要根据业务需求设计合理的分片规则,否则可能导致数据分布不均或查询性能下降。
  • 跨库事务处理困难:由于数据被分散到多个数据源中,跨库事务的处理变得复杂且性能较低。
  • 数据迁移和备份困难:水平分表后,数据的迁移和备份需要考虑到多个数据源之间的数据同步和一致性。

总之,Sharding-JDBC水平分表是一种有效的数据库分片技术,可以提高数据库的扩展性和查询性能。但在使用时需要充分考虑分片规则的设计、跨库事务的处理以及数据迁移和备份等限制因素。

配置

application.properties 配置如下:

properties
# 指定order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE
# 指定order表的数据分布情况,配置数据节点
# 操作所有表的 SQL 需要根据此信息生成实际 SQL,例如:truncate SQL等
spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds1.order_$->{1..2}
# order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.inline.algorithm-expression=order_$->{id % 2 + 1}

预先分别创建表 order_1 和 order_2

sql
CREATE TABLE IF NOT EXISTS order_1(
    id BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '订单ID' ,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    merchant_id BIGINT NOT NULL COMMENT '商家ID',
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '金额总数',
    total_count INT NOT NULL COMMENT '商品总数',
    `status` ENUM('Unpay','Undelivery','Unreceive','Received','Canceled') NOT NULL COMMENT '订单状态:未支付、未发货、未收货、已签收、买家取消',
    pay_time DATETIME DEFAULT NULL COMMENT '付款时间',
    delivery_time DATETIME DEFAULT NULL COMMENT '发货时间',
    received_time DATETIME DEFAULT NULL COMMENT '签收时间',
    cancel_time DATETIME DEFAULT NULL COMMENT '取消时间',
    delete_status ENUM('Normal','Deleted') NOT NULL COMMENT '订单删除状态',
    INDEX idx_order1_user_id(user_id) USING BTREE,
    INDEX idx_order1_merchant_id(merchant_id) USING BTREE,
    INDEX idx_order1_user_id_and_status_and_delete_status_and_create_time(user_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order1_user_id_and_delete_status_and_create_time(user_id,delete_status,create_time) USING BTREE,
    INDEX idx_order1_merchantId_and_status_and_deleteStatus_and_createTime(merchant_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order1_merchantId_and_deleteStatus_and_createTime(merchant_id,delete_status,create_time) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS order_2(
    id BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '订单ID' ,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    merchant_id BIGINT NOT NULL COMMENT '商家ID',
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '金额总数',
    total_count INT NOT NULL COMMENT '商品总数',
    `status` ENUM('Unpay','Undelivery','Unreceive','Received','Canceled') NOT NULL COMMENT '订单状态:未支付、未发货、未收货、已签收、买家取消',
    pay_time DATETIME DEFAULT NULL COMMENT '付款时间',
    delivery_time DATETIME DEFAULT NULL COMMENT '发货时间',
    received_time DATETIME DEFAULT NULL COMMENT '签收时间',
    cancel_time DATETIME DEFAULT NULL COMMENT '取消时间',
    delete_status ENUM('Normal','Deleted') NOT NULL COMMENT '订单删除状态',
    INDEX idx_order2_user_id(user_id) USING BTREE,
    INDEX idx_order2_merchant_id(merchant_id) USING BTREE,
    INDEX idx_order2_user_id_and_status_and_delete_status_and_create_time(user_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order2_user_id_and_delete_status_and_create_time(user_id,delete_status,create_time) USING BTREE,
    INDEX idx_order2_merchantId_and_status_and_deleteStatus_and_createTime(merchant_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order2_merchantId_and_deleteStatus_and_createTime(merchant_id,delete_status,create_time) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

单条插入

OrderMapper 插入 SQL 编写和平常编写 SQL 一致,只是此时 SQL 中的 order 表为逻辑表

java
@Insert("insert into `order`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time," +
        "received_time,cancel_time,delete_status) " +
        "values(#{createTime},#{userId},#{merchantId},#{totalAmount},#{totalCount}," +
        "#{status},#{payTime},#{deliveryTime},#{receivedTime},#{cancelTime},#{deleteStatus})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void add(Order order);

SQL 转换过程如下:

Logic SQL: insert into `order`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time,received_time,cancel_time,delete_status) values(?,?,?,?,?,?,?,?,?,?,?)
Actual SQL: ds1 ::: insert into `order_2`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time,received_time,cancel_time,delete_status, id) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [2025-02-18 08:09:49.0, 5814640730281880984, 8784506027955728107, 1000, 10, Canceled, null, null, null, null, Normal, 1098520888908709889]                     :

根据单个 id 查询

OrderMapper 代码如下:

java
@Select("select * from `order` where id=#{id}")
Order get(@Param("id") Long id);

SQL 转换过程如下:

Logic SQL: select * from `order` where id=?
Actual SQL: ds1 ::: select * from `order_2` where id=? ::: [1098517150592663553]

根据 id 列表查询

OrderMapper 代码如下:

java
/**
 * Sharding-JDBC 会计算所有 id 对应数据所在的实际表
 *
 * @param idList
 * @return
 */
@Select("<script>" +
        "   select * from `order`" +
        "   where id in(" +
        "   <foreach item=\"e\" collection=\"idList\" separator=\",\">" +
        "       #{e}" +
        "   </foreach>)" +
        "</script>")
List<Order> listById(@Param("idList") List<Long> idList);

SQL 转换过程如下:

Logic SQL: select * from `order`   where id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?       )
Actual SQL: ds1 ::: select * from `order_1`   where id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?       ) ::: [1098383195662974976, 1098383197089038337, 1098383197147758592, 1098383197223256065, 1098383197302947840]
Actual SQL: ds1 ::: select * from `order_2`   where id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?       ) ::: [1098383195662974976, 1098383197089038337, 1098383197147758592, 1098383197223256065, 1098383197302947840]

批量插入

OrderMapper 代码如下:

java
@Insert("<script>" +
        "insert into `order`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time," +
        "received_time,cancel_time,delete_status) values " +
        "   <foreach item=\"e\" collection=\"orderList\" separator=\",\">" +
        "       (#{e.createTime},#{e.userId},#{e.merchantId},#{e.totalAmount},#{e.totalCount},#{e.status},#{e.payTime}," +
        "       #{e.deliveryTime},#{e.receivedTime},#{e.cancelTime},#{e.deleteStatus})" +
        "   </foreach>" +
        "</script>")
void addBatch(@Param("orderList") List<Order> orderList);

SQL 转换过程如下:

Logic SQL: insert into `order`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time,received_time,cancel_time,delete_status) values             (?,?,?,?,?,?,?,       ?,?,?,?)    ,        (?,?,?,?,?,?,?,       ?,?,?,?)    ,        (?,?,?,?,?,?,?,       ?,?,?,?)    ,        (?,?,?,?,?,?,?,       ?,?,?,?)    ,        (?,?,?,?,?,?,?,       ?,?,?,?)
Actual SQL: ds1 ::: insert into `order_2`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time,received_time,cancel_time,delete_status, id) values             (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [2025-02-18 08:09:49.0, 6964861732010644788, 2814532415868973489, 1000, 10, Unreceive, null, null, null, null, Normal, 1098520890347356163, 2025-02-18 08:09:49.0, 8207136565814235543, 1265662605470267171, 1000, 10, Undelivery, null, null, null, null, Normal, 1098520890347356161, 2025-02-18 08:09:49.0, 1689313875273878826, 962377927175726351, 1000, 10, Canceled, null, null, null, null, Normal, 1098520890343161857]
Actual SQL: ds1 ::: insert into `order_1`(create_time,user_id,merchant_id,total_amount,total_count,status,pay_time,delivery_time,received_time,cancel_time,delete_status, id) values             (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ::: [2025-02-18 08:09:49.0, 4233041090170371398, 518184872115252607, 1000, 10, Unpay, null, null, null, null, Deleted, 1098520890347356162, 2025-02-18 08:09:49.0, 3533249187271280003, 503543136409760317, 1000, 10, Undelivery, null, null, null, null, Normal, 1098520890347356160]

没有 where 条件的查询

OrderMapper 代码如下:

java
@Select("select * from `order`")
List<Order> listAll();

SQL 转换过程如下:

Logic SQL: select * from `order`
Actual SQL: ds1 ::: select * from `order_1`
Actual SQL: ds1 ::: select * from `order_2`

水平分库

介绍

Sharding-JDBC水平分库是一种数据库分片技术,旨在通过横向拆分数据库来提高系统的扩展性和性能。以下是对Sharding-JDBC水平分库的详细解释:

一、基本概念

  • 水平分库:又称为横向拆分,是指将一个数据库中的数据按照某种规则(如用户ID、订单ID等)分散到多个数据库中。每个数据库中的数据只是原数据库数据的一部分,但数据库的结构与原数据库相同。
  • Sharding-JDBC:是一个开源的数据库分片中间件,它基于JDBC,提供了分库分表、读写分离、分布式事务等功能。Sharding-JDBC可以在客户端对SQL语句进行解析、路由和改写,然后将改写后的SQL语句发送到对应的数据源执行。

二、实现原理

Sharding-JDBC水平分库的实现原理主要包括以下几个方面:

  1. 数据源配置:在Sharding-JDBC中,需要配置多个数据源,每个数据源对应一个物理数据库。这些数据源可以是不同类型的数据库,但通常是同构的,即具有相同的表结构和数据类型。
  2. 分片规则:根据业务需求,定义数据分片的规则。分片规则决定了如何将数据分散到多个数据库中。常见的分片规则包括哈希分片、范围分片、列表分片等。
  3. SQL解析与改写:当应用程序执行SQL语句时,Sharding-JDBC会对SQL语句进行解析,识别出表名、字段名、条件等关键信息。然后,根据分片规则,将SQL语句改写为多个子查询,每个子查询对应一个数据源。
  4. 路由与执行:Sharding-JDBC会根据改写后的SQL语句,将查询请求路由到相应的数据源。然后,在每个数据源上执行子查询,并将结果集返回给应用程序。
  5. 结果归并:如果查询请求涉及多个数据源,Sharding-JDBC会对返回的结果集进行归并处理,得到最终的结果集。结果归并算法包括遍历归并、排序归并、分组归并等。

三、配置与实现

Sharding-JDBC水平分库的配置与实现通常包括以下几个步骤:

  1. 引入Maven依赖:在项目的pom.xml文件中添加Sharding-JDBC的依赖。
  2. 配置数据源:在配置文件(如application.yml或application.properties)中配置多个数据源,包括数据库的连接信息、用户名、密码等。
  3. 定义分片规则:在配置文件中定义分片规则,包括分片字段、分片策略、分片算法等。
  4. 编写代码:在代码中配置Sharding-JDBC的分片上下文,并编写业务逻辑代码。Sharding-JDBC会根据分片规则自动将数据路由到相应的数据源。

四、优点与限制

Sharding-JDBC水平分库的优点主要包括:

  1. 提高系统扩展性:通过水平分库,可以将数据分散到多个数据库中,从而提高系统的存储能力和处理能力。
  2. 提高查询性能:由于数据被分散到多个数据库中,查询时可以并行处理,从而提高查询性能。
  3. 简化数据库维护:通过水平分库,可以将大数据库拆分成小数据库,便于管理和维护。

然而,Sharding-JDBC水平分库也存在一些限制:

  1. 分片规则设计复杂:需要根据业务需求设计合理的分片规则,否则可能导致数据分布不均或查询性能下降。
  2. 跨库事务处理困难:由于数据被分散到多个数据库中,跨库事务的处理变得复杂且性能较低。
  3. 数据迁移和备份困难:水平分库后,数据的迁移和备份需要考虑到多个数据库之间的数据同步和一致性。

综上所述,Sharding-JDBC水平分库是一种有效的数据库分片技术,可以提高系统的扩展性和查询性能。但在使用时需要充分考虑分片规则的设计、跨库事务的处理以及数据迁移和备份等限制因素。

配置

application.properties 配置如下:

properties
spring.shardingsphere.datasource.names=ds1,ds2
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://localhost:3306/demo?allowMultiQueries=true
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456

spring.shardingsphere.datasource.ds2.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=org.mariadb.jdbc.Driver
spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mariadb://localhost:3307/demo?allowMultiQueries=true
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=123456

# 配置order逻辑表的分库策略
spring.shardingsphere.sharding.tables.order.database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.tables.order.database-strategy.inline.algorithm-expression=ds$->{user_id % 2 + 1}

# 指定order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.props.worker.id=1
# 指定order表的数据分布情况,配置数据节点
# 操作所有表的 SQL 需要根据此信息生成实际 SQL,例如:truncate SQL等
spring.shardingsphere.sharding.tables.order.actual-data-nodes=ds$->{1..2}.order_$->{1..2}
# order表的分片策略,分片策略包括分片键和分片算法
spring.shardingsphere.sharding.tables.order.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.order.table-strategy.inline.algorithm-expression=order_$->{id % 2 + 1}

预先分别运行两个数据库实例分别监听 3306 和 3307 并且都创建表 order_1 和 order_2

sql
CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

USE demo;

CREATE TABLE IF NOT EXISTS order_1(
    id BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '订单ID' ,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    merchant_id BIGINT NOT NULL COMMENT '商家ID',
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '金额总数',
    total_count INT NOT NULL COMMENT '商品总数',
    `status` ENUM('Unpay','Undelivery','Unreceive','Received','Canceled') NOT NULL COMMENT '订单状态:未支付、未发货、未收货、已签收、买家取消',
    pay_time DATETIME DEFAULT NULL COMMENT '付款时间',
    delivery_time DATETIME DEFAULT NULL COMMENT '发货时间',
    received_time DATETIME DEFAULT NULL COMMENT '签收时间',
    cancel_time DATETIME DEFAULT NULL COMMENT '取消时间',
    delete_status ENUM('Normal','Deleted') NOT NULL COMMENT '订单删除状态',
    INDEX idx_order1_user_id(user_id) USING BTREE,
    INDEX idx_order1_merchant_id(merchant_id) USING BTREE,
    INDEX idx_order1_user_id_and_status_and_delete_status_and_create_time(user_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order1_user_id_and_delete_status_and_create_time(user_id,delete_status,create_time) USING BTREE,
    INDEX idx_order1_merchantId_and_status_and_deleteStatus_and_createTime(merchant_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order1_merchantId_and_deleteStatus_and_createTime(merchant_id,delete_status,create_time) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS order_2(
    id BIGINT UNSIGNED NOT NULL PRIMARY KEY COMMENT '订单ID' ,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    merchant_id BIGINT NOT NULL COMMENT '商家ID',
    total_amount DECIMAL(15, 2) NOT NULL COMMENT '金额总数',
    total_count INT NOT NULL COMMENT '商品总数',
    `status` ENUM('Unpay','Undelivery','Unreceive','Received','Canceled') NOT NULL COMMENT '订单状态:未支付、未发货、未收货、已签收、买家取消',
    pay_time DATETIME DEFAULT NULL COMMENT '付款时间',
    delivery_time DATETIME DEFAULT NULL COMMENT '发货时间',
    received_time DATETIME DEFAULT NULL COMMENT '签收时间',
    cancel_time DATETIME DEFAULT NULL COMMENT '取消时间',
    delete_status ENUM('Normal','Deleted') NOT NULL COMMENT '订单删除状态',
    INDEX idx_order2_user_id(user_id) USING BTREE,
    INDEX idx_order2_merchant_id(merchant_id) USING BTREE,
    INDEX idx_order2_user_id_and_status_and_delete_status_and_create_time(user_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order2_user_id_and_delete_status_and_create_time(user_id,delete_status,create_time) USING BTREE,
    INDEX idx_order2_merchantId_and_status_and_deleteStatus_and_createTime(merchant_id,status,delete_status,create_time) USING BTREE,
    INDEX idx_order2_merchantId_and_deleteStatus_and_createTime(merchant_id,delete_status,create_time) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

各种情况的插入、查询

各种情况的插入、查询、truncate 和水平分表分析方法一致,运行示例 https://gitee.com/dexterleslie/demonstration/tree/master/demo-mysql-n-mariadb/demo-sharding-jdbc 通过日志分析其逻辑 SQL 和实际 SQL。

垂直分库

介绍

Sharding-JDBC是ShardingSphere的其中一个模块,是一个轻量级Java框架,在Java的JDBC层提供额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。通过Sharding-JDBC,应用可以透明地使用JDBC访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。

垂直分库是指按照业务将表进行分类,分别存放在不同的数据库中,这些库可以分布在不同的服务器,从而使访问压力被分摊在多个服务器,这样不仅能提高性能,同时能提高整体架构的业务清晰度。以下是使用Sharding-JDBC实现垂直分库的步骤:

  1. 需求分析:明确垂直分库的需求和目标,确定哪些表需要被分到不同的数据库中。
  2. 创建数据库和表:根据需求分析的结果,在相应的服务器上创建数据库和表。
  3. 编写代码
    • 创建实体类和对应的Mapper、Controller、Service等。
    • 配置Sharding-JDBC的分片策略。在配置文件中,指定数据源名称、数据库连接池、数据库驱动类名、数据库URL连接、数据库用户名和密码等信息。
    • 配置垂直分库策略,包括指定逻辑表、实际数据节点、分片列和分片算法等。
  4. 测试:编写测试代码,验证垂直分库是否成功。可以通过插入数据并查询来验证分片策略是否正确。

需要注意,垂直分库虽然能提高性能和业务清晰度,但也会带来一些复杂问题,如跨库事务、数据一致性等。因此,在实现垂直分库时,需要充分考虑这些问题,并采取相应的措施来解决。

此外,Sharding-JDBC的版本更新较快,不同版本之间可能存在一些差异。因此,在实现垂直分库时,需要参考当前版本的Sharding-JDBC文档和示例代码,以确保正确实现和配置。

总的来说,使用Sharding-JDBC实现垂直分库是一个复杂但有效的过程,需要充分考虑业务需求和技术实现等因素。通过合理配置和测试,可以实现性能的提升和业务清晰度的提高。

配置

application.properties 配置如下:

properties
# 逻辑表user在数据源ds1的物理表user中
spring.shardingsphere.sharding.tables.user.actual-data-nodes=ds1.user

预先在数据库中创建 user 表

sql
CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

USE demo;

CREATE TABLE IF NOT EXISTS `user`(
     id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID' ,
     create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     `name` VARCHAR(128) NOT NULL COMMENT '用户名称'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

业务 SQL 编写

业务 SQL 编写和没有使用 Sharding-JDBC 时编写规则一致。

垂直分表

介绍

Sharding-JDBC中的垂直分表是一种数据库优化技术,主要用于解决单表数据量过大、字段过多导致的数据库性能下降问题。以下是对Sharding-JDBC垂直分表的详细解释:

一、定义

垂直分表是指将一张宽表(包含较多字段的表)按照字段进行拆分,拆分成多张窄表(包含较少字段的表)。这些窄表在逻辑上仍然属于同一张表,但在物理上被存储在不同的数据库表中。

二、优势

  1. 提高性能:通过将热点字段和冷字段分开存储,可以减少I/O争抢,提高数据库性能。同时,由于每张窄表只包含部分字段,查询时可以更快地定位到所需的数据,减少不必要的字段扫描。
  2. 优化资源使用:垂直分表可以将数据分散到多个表中,从而优化数据库服务器的资源使用。例如,可以将经常访问的字段放在同一个表中,以减少磁盘I/O和内存使用。
  3. 业务清晰度提升:通过垂直分表,可以将不同业务逻辑的字段分开存储,使得数据库结构更加清晰,便于维护和扩展。

三、原则

  1. 冷热数据分离:将经常访问的字段(热点数据)和不常访问的字段(冷数据)分开存储。
  2. 大字段拆分:将大文本、图片、视频等大字段拆分出来单独存储,以减少对数据库性能的影响。
  3. 组合查询优化:将经常组合查询的字段放在同一张表中,以避免多表查询带来的性能开销。

四、实现步骤

  1. 创建数据库和表:首先,需要在数据库中创建多个窄表,这些窄表将存储拆分后的字段。
  2. 配置Sharding-JDBC:在Sharding-JDBC的配置文件中,指定垂直分表的策略,包括分片键、分片算法等。
  3. 编写SQL语句:在编写SQL语句时,需要注意使用逻辑表名而不是物理表名。Sharding-JDBC会根据配置的策略将逻辑表名映射到实际的物理表名上。

五、注意事项

  1. 避免联查:在垂直分表后,尽量避免跨表查询,因为跨表查询会带来额外的性能开销。如果必须跨表查询,可以考虑使用数据库视图或中间层来优化查询性能。
  2. 数据一致性:在垂直分表后,需要确保不同表之间的数据一致性。这可以通过事务管理、数据校验等方式来实现。
  3. 分片策略选择:在选择分片策略时,需要根据业务需求和数据库性能进行权衡。例如,可以根据字段的访问频次、数据分布等因素来选择合适的分片键和分片算法。

综上所述,Sharding-JDBC的垂直分表技术是一种有效的数据库优化手段,可以显著提高数据库性能和资源使用效率。但在实际应用中,需要根据业务需求和数据库性能进行权衡和选择。

实验

垂直分表可以参考绑定表章节,它们本质是同一个事情。

公共表

介绍

Sharding-JDBC中的公共表(也称为广播表)是一个重要的概念,尤其在处理分库分表的场景中。以下是对Sharding-JDBC公共表的详细解析:

一、定义与特点

公共表是指存储固定数据、数据很少发生变化且经常进行关联查询的表。这类表通常包括参数表、数据字典表等。在Sharding-JDBC中,公共表需要在每个分片数据源中都存在,且表结构和表中的数据在每个数据库中都完全一致。

二、实现方式

  1. 创建公共表:首先,需要在每个数据库中手动创建出结构相同的公共表。
  2. 配置公共表:在Sharding-JDBC的配置文件中,需要指定哪些表是公共表。例如,在application.properties或application.yml配置文件中,可以通过设置spring.shardingsphere.sharding.broadcast-tables属性来指定公共表。

三、使用场景与优势

  1. 使用场景:公共表通常用于存储一些全局的、不经常变动的数据,如配置信息、数据字典等。这些数据需要在多个分片之间进行共享和关联查询。
  2. 优势
    • 简化查询:通过将公共表复制到每个分片中,可以简化跨分片的关联查询,提高查询效率。
    • 数据一致性:由于公共表在每个分片中都存在且数据一致,因此可以确保在不同分片上进行查询时得到相同的结果。

四、注意事项

  1. 数据更新:对公共表的更新操作需要同时发送到所有分片执行,以确保数据的一致性。这可能会增加一些额外的开销和复杂性。
  2. 表结构变更:如果需要对公共表的结构进行变更(如添加列、修改数据类型等),需要在所有分片中同时执行相应的DDL操作。

五、示例配置

以下是一个简单的示例配置,展示了如何在Sharding-JDBC中配置公共表:

properties
# 指定t_dict为公共表
spring.shardingsphere.sharding.broadcast-tables=t_dict

# 定义数据源(多个数据源名为m0, m1, m2等)
spring.shardingsphere.datasource.names=m0,m1,m2

# 配置数据源m0(以mysql为例)
spring.shardingsphere.datasource.m0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.m0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.m0.url=jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.shardingsphere.datasource.m0.username=root
spring.shardingsphere.datasource.m0.password=yourpassword

# (类似地配置数据源m1和m2...)

在上面的配置中,t_dict被指定为公共表,它将在所有分片数据源(m0、m1、m2等)中都存在。

综上所述,Sharding-JDBC中的公共表是一个用于存储固定数据、支持跨分片关联查询的重要概念。通过正确配置和使用公共表,可以简化查询逻辑、提高查询效率并确保数据的一致性。

配置

application.properties 配置如下:

properties
spring.shardingsphere.sharding.broadcast-tables=dict

分别在各个数据库创建 dict 表 SQL 脚本如下:

sql
CREATE TABLE IF NOT EXISTS dict (
    id INT NOT NULL PRIMARY KEY COMMENT '字典ID',
    `name` VARCHAR(256) NOT NULL COMMENT '字典记录名称',
    `value` VARCHAR(256) NOT NULL COMMENT '字典记录值'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

业务 SQL 编写

业务 SQL 编写和没有使用 Sharding-JDBC 时编写规则一致。

读写分离

注意:项目中未用到此特性,所以暂时不作研究。

介绍

Sharding-JDBC 是一个轻量级的 Java 框架,它在 Java 的 JDBC 层提供额外服务,使用客户端直连数据库,并以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。Sharding-JDBC 读写分离是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库,从而提升系统性能。以下是对 Sharding-JDBC 读写分离的详细介绍:

一、读写分离的背景与原理

  1. 背景
    • 面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。
    • 对于同一时刻有大量并发读操作和较少写操作类型的应用系统,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够避免由数据更新导致的行锁,从而提升系统的查询性能。
  2. 原理
    • 数据库主从复制技术:把数据复制到多个节点中,分散读多个库以支持高并发的读,而写只在主库上。
    • Sharding-JDBC 通过简单的开发,实现读写分离技术。它提供了一主多从的读写分离配置,可独立使用,也可配合分库分表使用。

二、Sharding-JDBC 读写分离的配置与实现

  1. Maven 依赖

    需要在项目的 Maven 配置文件(pom.xml)中添加 Sharding-JDBC 的相关依赖。例如:

    xml
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
        <version>4.x.x</version> <!-- 使用具体的版本号 -->
    </dependency>
  2. 配置文件

    在 Spring Boot 的配置文件(如 application.yml)中配置数据源和读写分离规则。例如:

    yaml
    spring:
      shardingsphere:
        props:
          sql:
            show: true
        datasource:
          names: ds-master, ds-slave1, ds-slave2
          ds-master:
            type: com.alibaba.druid.pool.DruidDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3307/student?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
            username: root
            password: 123456
          ds-slave1:
            # 从库一的配置
          ds-slave2:
            # 从库二的配置
        master-slave-rules:
          ds_0:
            master-data-source-name: ds-master
            slave-data-source-names: ds-slave1, ds-slave2
  3. Java 代码配置

    在 Java 代码中配置对应的主从数据源和读写分离规则。例如:

    java
    @Configuration
    public class MasterSlaveDataSourceConfig {
     
        @Value("${spring.datasource.type}")
        private Class<? extends DataSource> dataSourceType;
     
        // 其他数据源配置属性
     
        @Bean("masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            DruidDataSource druidDataSource = (DruidDataSource) DataSourceBuilder.create().type(dataSourceType).build();
            // 配置 Druid 数据源
            return druidDataSource;
        }
     
        @Bean("slaveDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            // 注意:这里通常会有多个从库配置,需要遍历配置
            DruidDataSource druidDataSource = (DruidDataSource) DataSourceBuilder.create().type(dataSourceType).build();
            // 配置 Druid 数据源
            return druidDataSource;
        }
     
        // 配置 Sharding-JDBC 的读写分离规则
        @Bean
        public MasterSlaveRuleConfiguration masterSlaveRuleConfiguration() {
            MasterSlaveRuleConfiguration masterSlaveRuleConfig = new MasterSlaveRuleConfiguration();
            masterSlaveRuleConfig.setName("ds_0");
            masterSlaveRuleConfig.setMasterDataSourceName("masterDataSource");
            List<String> slaveDataSourceNames = new ArrayList<>();
            slaveDataSourceNames.add("slaveDataSource");
            // 如果有多个从库,继续添加
            masterSlaveRuleConfig.setSlaveDataSourceNames(slaveDataSourceNames);
            return masterSlaveRuleConfig;
        }
     
        // 配置 ShardingDataSource
        @Bean
        public DataSource shardingDataSource() throws SQLException {
            Map<String, DataSource> dataSourceMap = new HashMap<>();
            dataSourceMap.put("masterDataSource", masterDataSource());
            dataSourceMap.put("slaveDataSource", slaveDataSource());
            // 配置其他数据源
     
            ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
            shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration());
            // 配置分片规则(如果有)
     
            return ShardingDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
        }
    }

三、读写分离的效果与注意事项

  1. 效果
    • 通过读写分离,可以将查询请求均匀地分散到多个数据副本,进一步提升系统的处理能力。
    • 使用多主多从的方式,不仅能提升系统的吞吐量,还能提升系统的可用性,确保在任何一个数据库宕机或磁盘物理损坏的情况下,系统仍能正常运行。
  2. 注意事项
    • 主从节点数据同步延迟可能导致的数据不一致问题。对于时效性比较高的查询,可以强制路由到主节点查询。
    • Sharding-JDBC 不支持类似主库双写或多写这样的特性,需要确保写操作只发生在主库上。

综上所述,Sharding-JDBC 读写分离是一种有效的数据库性能优化手段,适用于读多写少的应用场景。通过合理的配置和实现,可以显著提升系统的查询性能和可用性。

绑定表

介绍

Sharding-JDBC中的绑定表(也称为关联表)是指分片规则一致的主表和子表。以下是对Sharding-JDBC绑定表的详细解释:

一、定义与概念

绑定表是指两张或多张表之间因为分片规则相同而建立的关联关系。在Sharding-JDBC中,如果两张表互为绑定表关系,那么它们在进行多表关联查询时,不会出现笛卡尔积关联,从而可以大大提升关联查询的效率。

二、应用场景

绑定表通常应用于具有主从关系或父子关系的表结构中,例如订单表和订单项表。假设有一个订单表(t_order)和一个订单项表(t_order_item),它们都按照订单ID(order_id)进行分片。在这种情况下,可以将这两张表设置为绑定表关系,以便在进行订单和订单项的关联查询时,能够高效地获取数据。

三、配置方法

在Sharding-JDBC中配置绑定表关系,通常需要在配置文件(如properties文件或yml文件)中进行设置。以下是一个示例配置:

yaml
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        # 数据源配置信息
      ds1:
        # 数据源配置信息
    sharding:
      tables:
        t_order:
          # 分片规则配置
        t_order_item:
          # 分片规则配置,与t_order表相同
      binding-tables:
        - t_order,t_order_item

在上述配置中,binding-tables部分指定了t_order和t_order_item为绑定表关系。这意味着在进行这两张表的关联查询时,Sharding-JDBC将会按照绑定表的规则进行路由和查询。

四、注意事项

  1. 分片键要相同:绑定表之间的分片键必须完全相同,否则无法进行绑定。
  2. 主表策略:在绑定表关系中,ShardingSphere将会以主表(即在FROM子句中最左侧的表)作为整个绑定表的主表。所有路由计算都只会使用主表的策略。
  3. 性能提升:配置绑定表关系后,可以显著提升多表关联查询的性能,避免产生笛卡尔积关联。

五、示例

假设有一个订单系统,其中订单表(t_order)和订单项表(t_order_item)都按照订单ID(order_id)进行分片。以下是一个简单的示例,展示了如何在Sharding-JDBC中配置和使用绑定表:

  1. 创建数据库和表:在数据库中创建订单表和订单项表,并设置相同的分片键。
  2. 配置Sharding-JDBC:在Sharding-JDBC的配置文件中,将订单表和订单项表设置为绑定表关系。
  3. 执行关联查询:在执行订单和订单项的关联查询时,Sharding-JDBC将会按照绑定表的规则进行路由和查询,从而提升查询性能。

综上所述,Sharding-JDBC的绑定表功能是一种高效处理多表关联查询的机制。通过合理配置绑定表关系,可以显著提升查询性能并优化数据库分片策略。

演示

使用商品主表和商品描述子表演示绑定表用法。

在数据库中创建 product 主表和 product_description 子表如下:

sql
-- 用于协助演示绑定表关系
CREATE TABLE IF NOT EXISTS product_1 (
    id BIGINT NOT NULL PRIMARY KEY COMMENT '商品ID',
    `name` VARCHAR(512) NOT NULL COMMENT '商品名称',
    create_time DATETIME NOT NULL COMMENT '创建时间'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS product_description_1 (
   id BIGINT NOT NULL PRIMARY KEY COMMENT '商品描述ID',
   product_id BIGINT NOT NULL COMMENT '商品ID',
   `description` TEXT COMMENT '商品描述'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS product_2 (
    id BIGINT NOT NULL PRIMARY KEY COMMENT '商品ID',
    `name` VARCHAR(512) NOT NULL COMMENT '商品名称',
    create_time DATETIME NOT NULL COMMENT '创建时间'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE IF NOT EXISTS product_description_2 (
     id BIGINT NOT NULL PRIMARY KEY COMMENT '商品描述ID',
     product_id BIGINT NOT NULL COMMENT '商品ID',
     `description` TEXT COMMENT '商品描述'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

application.properties 绑定表配置如下:

properties
spring.shardingsphere.sharding.tables.product.key-generator.column=id
spring.shardingsphere.sharding.tables.product.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.product.key-generator.props.worker.id=1
spring.shardingsphere.sharding.tables.product.actual-data-nodes=ds1.product_$->{1..2}
spring.shardingsphere.sharding.tables.product.table-strategy.inline.sharding-column=id
spring.shardingsphere.sharding.tables.product.table-strategy.inline.algorithm-expression=product_$->{id % 2 + 1}

spring.shardingsphere.sharding.tables.product_description.key-generator.column=id
spring.shardingsphere.sharding.tables.product_description.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.product_description.key-generator.props.worker.id=1
spring.shardingsphere.sharding.tables.product_description.actual-data-nodes=ds1.product_description_$->{1..2}
spring.shardingsphere.sharding.tables.product_description.table-strategy.inline.sharding-column=product_id
spring.shardingsphere.sharding.tables.product_description.table-strategy.inline.algorithm-expression=product_description_$->{product_id % 2 + 1}
# 绑定表关键配置
spring.shardingsphere.sharding.binding-tables[0]=product,product_description

product 和 product_description 在查询 join 时不会发生迪卡尔积(product_1 和 product_description_1 join、product_2 和 product_description_2 join)

java
Logic SQL: select p.id,p.name,p.create_time,pd.id as description_id,pd.description from product p join product_description pd on p.id=pd.product_id where p.id=?
Actual SQL: ds1 ::: select p.id,p.name,p.create_time,pd.id as description_id,pd.description from product_2 p join product_description_2 pd on p.id=pd.product_id where p.id=? ::: [1098738066564059137]
Logic SQL: select p.id,p.name,p.create_time,pd.id as description_id,pd.description from product p   join product_description pd on p.id=pd.product_id   where p.id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ) order by p.id asc
Actual SQL: ds1 ::: select p.id,p.name,p.create_time,pd.id as description_id,pd.description from product_1 p   join product_description_1 pd on p.id=pd.product_id   where p.id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ) order by p.id asc ::: [1098738065595174912, 1098738065859416065, 1098738065943302144, 1098738066081714177, 1098738066173988864, 1098738066295623681, 1098738066358538240, 1098738066429841409, 1098738066501144576, 1098738066564059137]
Actual SQL: ds1 ::: select p.id,p.name,p.create_time,pd.id as description_id,pd.description from product_2 p   join product_description_2 pd on p.id=pd.product_id   where p.id in(            ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ,        ?    ) order by p.id asc ::: [1098738065595174912, 1098738065859416065, 1098738065943302144, 1098738066081714177, 1098738066173988864, 1098738066295623681, 1098738066358538240, 1098738066429841409, 1098738066501144576, 1098738066564059137]

基准测试

5000w 数据基准测试

使用本站 示例 辅助测试

内核参数文件描述符限制调优(在所有主机中):参考 链接

不分库分表

使用 GCE E2 实例

一个数据库实例:8C8G、Ubuntu20.04

一个 OpenResty 反向代理实例:8C8G、Ubuntu20.04

四个 SpringBoot 应用实例:8C8G、Ubuntu20.04

一个 wrk 压力机:10C10G、Ubuntu20.04

修改 application.properties 中的配置指向数据库服务

properties
spring.shardingsphere.datasource.default-data-source.jdbc-url=jdbc:mariadb://10.138.0.4:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true

取消 application.properties 中的 不分库分表 区域中的 sharding-jdbc 配置

编译并推送镜像

bash
./build.sh && ./push.sh

准备数据库服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动数据库服务

    bash
    cd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d

准备 SpringBoot 应用服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动 SpringBoot 服务

    bash
    cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose up -d

准备 OpenResty 反向代理服务

  • 复制 代码 到 OpenResty 反向代理实例中

  • 修改 nginx.conf 指向各个 SpringBoot 应用,如下:

    upstream backend {
        server 10.138.0.187:8080;
        server 10.138.0.188:8080;
        server 10.138.0.193:8080;
        server 10.138.0.194:8080;
    }
  • 启动服务

    bash
    docker compose up -d

使用示例中的 rest-api.http 测试接口是否正常

准备 wrk 压力机

安装 Apache Bench 工具:参考 链接

批量初始化数据

bash
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch

初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址

bash
curl 10.138.0.5:8080/api/v1/order/init

初始化数据后建立索引(批量初始化数据时没有索引会更快)

sql
create index idx_order_userId_createTime_deleteStatus_status on t_order(userId,createTime,deleteStatus,status);
create index idx_order_status_deleteStatus_createTime_id on t_order(status, deleteStatus, createTime, id);

create index idx_order1_userId_createTime_deleteStatus_status on t_order1(userId,createTime,deleteStatus,status);
create index idx_order1_status_deleteStatus_createTime_id on t_order1(status, deleteStatus, createTime, id);

create index idx_order2_userId_createTime_deleteStatus_status on t_order2(userId,createTime,deleteStatus,status);
create index idx_order2_status_deleteStatus_createTime_id on t_order2(status, deleteStatus, createTime, id);

create index idx_order3_userId_createTime_deleteStatus_status on t_order3(userId,createTime,deleteStatus,status);
create index idx_order3_status_deleteStatus_createTime_id on t_order3(status, deleteStatus, createTime, id);

create index idx_order4_userId_createTime_deleteStatus_status on t_order4(userId,createTime,deleteStatus,status);
create index idx_order4_status_deleteStatus_createTime_id on t_order4(status, deleteStatus, createTime, id);

create index idx_order5_userId_createTime_deleteStatus_status on t_order5(userId,createTime,deleteStatus,status);
create index idx_order5_status_deleteStatus_createTime_id on t_order5(status, deleteStatus, createTime, id);

create index idx_order6_userId_createTime_deleteStatus_status on t_order6(userId,createTime,deleteStatus,status);
create index idx_order6_status_deleteStatus_createTime_id on t_order6(status, deleteStatus, createTime, id);

create index idx_order7_userId_createTime_deleteStatus_status on t_order7(userId,createTime,deleteStatus,status);
create index idx_order7_status_deleteStatus_createTime_id on t_order7(status, deleteStatus, createTime, id);

create index idx_order8_userId_createTime_deleteStatus_status on t_order8(userId,createTime,deleteStatus,status);
create index idx_order8_status_deleteStatus_createTime_id on t_order8(status, deleteStatus, createTime, id);

create index idx_orderDetail_merchantId on t_order_detail(merchantId);
create index idx_orderDetail_merchantId_orderId on t_order_detail(merchantId, orderId);

create index idx_orderDetail1_merchantId on t_order_detail1(merchantId);
create index idx_orderDetail1_merchantId_orderId on t_order_detail1(merchantId, orderId);

create index idx_orderDetail2_merchantId on t_order_detail2(merchantId);
create index idx_orderDetail2_merchantId_orderId on t_order_detail2(merchantId, orderId);

create index idx_orderDetail3_merchantId on t_order_detail3(merchantId);
create index idx_orderDetail3_merchantId_orderId on t_order_detail3(merchantId, orderId);

create index idx_orderDetail4_merchantId on t_order_detail4(merchantId);
create index idx_orderDetail4_merchantId_orderId on t_order_detail4(merchantId, orderId);

create index idx_orderDetail5_merchantId on t_order_detail5(merchantId);
create index idx_orderDetail5_merchantId_orderId on t_order_detail5(merchantId, orderId);

create index idx_orderDetail6_merchantId on t_order_detail6(merchantId);
create index idx_orderDetail6_merchantId_orderId on t_order_detail6(merchantId, orderId);

create index idx_orderDetail7_merchantId on t_order_detail7(merchantId);
create index idx_orderDetail7_merchantId_orderId on t_order_detail7(merchantId, orderId);

create index idx_orderDetail8_merchantId on t_order_detail8(merchantId);
create index idx_orderDetail8_merchantId_orderId on t_order_detail8(merchantId, orderId);

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载订单 ID

bash
cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/getById
    Running 1m test @ http://10.138.0.3/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    73.45ms   54.95ms 686.47ms   70.96%
        Req/Sec     3.76k   268.51     4.55k    73.15%
      Latency Distribution
         50%   67.10ms
         75%  101.50ms
         90%  143.51ms
         99%  244.36ms
      1794903 requests in 1.00m, 1.04GB read
    Requests/sec:  29875.98
    Transfer/sec:     17.72MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              60.9%    0.0%   33.4%    0.6%    0.0%    5.2%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop9
      1717.00        26.8M        16.0k         0.0k     133.9M      80.0k       0.0k sda

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     1.52s   377.90ms   4.61s    80.38%
        Req/Sec   166.61     59.35     0.95k    78.56%
      Latency Distribution
         50%    1.57s
         75%    1.62s
         90%    1.68s
         99%    2.43s
      79607 requests in 1.00m, 260.54MB read
    Requests/sec:   1324.61
    Transfer/sec:      4.34MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              12.3%    0.0%   10.9%   76.7%    0.1%    0.1%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop9
     12002.20       187.4M         1.7M         0.0k     936.8M       8.3M       0.0k sda

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   503.38ms  205.89ms   2.14s    66.57%
        Req/Sec   509.04     96.74     1.91k    76.73%
      Latency Distribution
         50%  539.73ms
         75%  603.69ms
         90%  768.55ms
         99%    1.07s
      243316 requests in 1.00m, 203.22MB read
    Requests/sec:   4050.25
    Transfer/sec:      3.38MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              20.1%    0.0%   14.9%   64.8%    0.1%    0.1%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop9
     12001.20       187.5M        17.6k         0.0k     937.4M      88.0k       0.0k sda

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/create
    Running 1m test @ http://10.138.0.3/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     2.24s     1.81s   12.71s    78.76%
        Req/Sec   185.68    159.64     0.97k    72.06%
      Latency Distribution
         50%    1.74s
         75%    3.18s
         90%    4.85s
         99%    7.52s
      59518 requests in 1.00m, 14.43MB read
    Requests/sec:    990.55
    Transfer/sec:    245.92KB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              19.1%    0.0%   19.4%   22.2%    0.1%   39.2%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop9
      6500.60        50.5M        94.7M         0.0k     252.3M     473.6M       0.0k sda
只水平分表

使用 GCE E2 实例

一个数据库实例:8C8G、Ubuntu20.04

一个 OpenResty 反向代理实例:8C8G、Ubuntu20.04

四个 SpringBoot 应用实例:8C8G、Ubuntu20.04

一个 wrk 压力机:10C10G、Ubuntu20.04

修改 application.properties 中的配置指向数据库服务

properties
spring.shardingsphere.datasource.default-data-source.jdbc-url=jdbc:mariadb://10.138.0.4:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true

取消 application.properties 中的 只水平分表 区域中的 sharding-jdbc 配置

编译并推送镜像

bash
./build.sh && ./push.sh

准备数据库服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动数据库服务

    bash
    cd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d

准备 SpringBoot 应用服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动 SpringBoot 服务

    bash
    cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose up -d

准备 OpenResty 反向代理服务

  • 复制 代码 到 OpenResty 反向代理实例中

  • 修改 nginx.conf 指向各个 SpringBoot 应用,如下:

    upstream backend {
        server 10.138.0.187:8080;
        server 10.138.0.188:8080;
        server 10.138.0.193:8080;
        server 10.138.0.194:8080;
    }
  • 启动服务

    bash
    docker compose up -d

使用示例中的 rest-api.http 测试接口是否正常

准备 wrk 压力机

安装 Apache Bench 工具:参考 链接

批量初始化数据

bash
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch

初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址

bash
curl 10.138.0.5:8080/api/v1/order/init

初始化数据后建立索引(批量初始化数据时没有索引会更快)

sql
create index idx_order_userId_createTime_deleteStatus_status on t_order(userId,createTime,deleteStatus,status);
create index idx_order_status_deleteStatus_createTime_id on t_order(status, deleteStatus, createTime, id);

create index idx_order1_userId_createTime_deleteStatus_status on t_order1(userId,createTime,deleteStatus,status);
create index idx_order1_status_deleteStatus_createTime_id on t_order1(status, deleteStatus, createTime, id);

create index idx_order2_userId_createTime_deleteStatus_status on t_order2(userId,createTime,deleteStatus,status);
create index idx_order2_status_deleteStatus_createTime_id on t_order2(status, deleteStatus, createTime, id);

create index idx_order3_userId_createTime_deleteStatus_status on t_order3(userId,createTime,deleteStatus,status);
create index idx_order3_status_deleteStatus_createTime_id on t_order3(status, deleteStatus, createTime, id);

create index idx_order4_userId_createTime_deleteStatus_status on t_order4(userId,createTime,deleteStatus,status);
create index idx_order4_status_deleteStatus_createTime_id on t_order4(status, deleteStatus, createTime, id);

create index idx_order5_userId_createTime_deleteStatus_status on t_order5(userId,createTime,deleteStatus,status);
create index idx_order5_status_deleteStatus_createTime_id on t_order5(status, deleteStatus, createTime, id);

create index idx_order6_userId_createTime_deleteStatus_status on t_order6(userId,createTime,deleteStatus,status);
create index idx_order6_status_deleteStatus_createTime_id on t_order6(status, deleteStatus, createTime, id);

create index idx_order7_userId_createTime_deleteStatus_status on t_order7(userId,createTime,deleteStatus,status);
create index idx_order7_status_deleteStatus_createTime_id on t_order7(status, deleteStatus, createTime, id);

create index idx_order8_userId_createTime_deleteStatus_status on t_order8(userId,createTime,deleteStatus,status);
create index idx_order8_status_deleteStatus_createTime_id on t_order8(status, deleteStatus, createTime, id);

create index idx_orderDetail_merchantId on t_order_detail(merchantId);
create index idx_orderDetail_merchantId_orderId on t_order_detail(merchantId, orderId);

create index idx_orderDetail1_merchantId on t_order_detail1(merchantId);
create index idx_orderDetail1_merchantId_orderId on t_order_detail1(merchantId, orderId);

create index idx_orderDetail2_merchantId on t_order_detail2(merchantId);
create index idx_orderDetail2_merchantId_orderId on t_order_detail2(merchantId, orderId);

create index idx_orderDetail3_merchantId on t_order_detail3(merchantId);
create index idx_orderDetail3_merchantId_orderId on t_order_detail3(merchantId, orderId);

create index idx_orderDetail4_merchantId on t_order_detail4(merchantId);
create index idx_orderDetail4_merchantId_orderId on t_order_detail4(merchantId, orderId);

create index idx_orderDetail5_merchantId on t_order_detail5(merchantId);
create index idx_orderDetail5_merchantId_orderId on t_order_detail5(merchantId, orderId);

create index idx_orderDetail6_merchantId on t_order_detail6(merchantId);
create index idx_orderDetail6_merchantId_orderId on t_order_detail6(merchantId, orderId);

create index idx_orderDetail7_merchantId on t_order_detail7(merchantId);
create index idx_orderDetail7_merchantId_orderId on t_order_detail7(merchantId, orderId);

create index idx_orderDetail8_merchantId on t_order_detail8(merchantId);
create index idx_orderDetail8_merchantId_orderId on t_order_detail8(merchantId, orderId);

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载订单 ID

bash
cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/getById
    Running 1m test @ http://10.138.0.3/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    83.66ms   63.69ms 822.27ms   71.84%
        Req/Sec     3.32k   362.22     4.16k    77.81%
      Latency Distribution
         50%   74.54ms
         75%  114.73ms
         90%  165.95ms
         99%  285.70ms
      1587666 requests in 1.00m, 0.92GB read
    Requests/sec:  26425.95
    Transfer/sec:     15.68MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              61.8%    0.0%   33.5%    1.9%    0.1%    2.7%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
      2802.00        43.7M        19.2k         0.0k     218.7M      96.0k       0.0k sda

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     1.97s   457.23ms   5.11s    82.92%
        Req/Sec   131.20     63.90     1.22k    80.97%
      Latency Distribution
         50%    2.04s
         75%    2.08s
         90%    2.13s
         99%    3.11s
      61249 requests in 1.00m, 200.10MB read
    Requests/sec:   1019.23
    Transfer/sec:      3.33MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              12.5%    0.0%   12.0%   75.2%    0.2%    0.1%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
     12002.00       187.5M        11.2k         0.0k     937.5M      56.0k       0.0k sda

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   550.58ms  216.35ms   2.19s    64.05%
        Req/Sec   465.21     96.13     1.28k    82.25%
      Latency Distribution
         50%  609.83ms
         75%  646.79ms
         90%  720.79ms
         99%    1.16s
      222384 requests in 1.00m, 185.77MB read
    Requests/sec:   3700.93
    Transfer/sec:      3.09MB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              22.4%    0.0%   16.1%   61.3%    0.2%    0.0%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
     12001.20       187.5M        14.4k         0.0k     937.5M      72.0k       0.0k sda

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/create
    Running 1m test @ http://10.138.0.3/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency     3.03s     2.62s   17.12s    80.25%
        Req/Sec   188.92    141.06     0.91k    63.21%
      Latency Distribution
         50%    1.93s
         75%    4.38s
         90%    7.43s
         99%   10.29s
      45586 requests in 1.00m, 11.05MB read
    Requests/sec:    759.30
    Transfer/sec:    188.53KB
  • i/o 监控

    bash
    $ iostat -h 5
    avg-cpu:  %user   %nice %system %iowait  %steal   %idle
              11.3%    0.0%   13.2%   42.6%    0.3%   32.7%
    
          tps    kB_read/s    kB_wrtn/s    kB_dscd/s    kB_read    kB_wrtn    kB_dscd Device
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop0
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop1
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop2
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop3
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop4
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop5
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop6
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop7
         0.00         0.0k         0.0k         0.0k       0.0k       0.0k       0.0k loop8
      8173.80        49.5M       119.1M         0.0k     247.5M     595.4M       0.0k sda
水平分库分表

使用 GCE E2 实例

一个商品数据库、zookeeper 实例:8C8G、Ubuntu20.04

三个订单数据库实例:8C8G、Ubuntu20.04

一个 OpenResty 反向代理实例:8C8G、Ubuntu20.04

四个 SpringBoot 应用实例:8C8G、Ubuntu20.04

一个 wrk 压力机:10C10G、Ubuntu20.04

修改 application.properties 中的配置指向数据库服务

properties
spring.shardingsphere.datasource.default-data-source.jdbc-url=jdbc:mariadb://10.138.0.4:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mariadb://10.138.0.9:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds2.jdbc-url=jdbc:mariadb://10.138.0.10:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true
spring.shardingsphere.datasource.ds3.jdbc-url=jdbc:mariadb://10.138.0.11:3306/demo?allowMultiQueries=true&useSSL=false&allowPublicKeyRetrieval=true

取消 application.properties 中的 水平分库分表 区域中的 sharding-jdbc 配置

编译并推送镜像

bash
./build.sh && ./push.sh

准备数据库服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动商品数据库服务

    bash
    cd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d
  • 启动订单数据库服务

    bash
    cd ~/deployer-order-management-app-sharding/common-order-db && docker compose down -v && docker compose pull && docker compose up -d

准备 SpringBoot 应用服务

  • 复制示例中的 deployer 目录到实例中

    bash
    scp -r deployer [email protected]:~/deployer-order-management-app-sharding
  • 启动 SpringBoot 服务

    bash
    cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose up -d

准备 OpenResty 反向代理服务

  • 复制 代码 到 OpenResty 反向代理实例中

  • 修改 nginx.conf 指向各个 SpringBoot 应用,如下:

    upstream backend {
        server 10.138.0.187:8080;
        server 10.138.0.188:8080;
        server 10.138.0.193:8080;
        server 10.138.0.194:8080;
    }
  • 启动服务

    bash
    docker compose up -d

使用示例中的 rest-api.http 测试接口是否正常

准备 wrk 压力机

安装 Apache Bench 工具:参考 链接

批量初始化数据

bash
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch

初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址

bash
curl 10.138.0.5:8080/api/v1/order/init

初始化数据后建立索引(批量初始化数据时没有索引会更快)

sql
create index idx_order_userId_createTime_deleteStatus_status on t_order(userId,createTime,deleteStatus,status);
create index idx_order_status_deleteStatus_createTime_id on t_order(status, deleteStatus, createTime, id);

create index idx_order1_userId_createTime_deleteStatus_status on t_order1(userId,createTime,deleteStatus,status);
create index idx_order1_status_deleteStatus_createTime_id on t_order1(status, deleteStatus, createTime, id);

create index idx_order2_userId_createTime_deleteStatus_status on t_order2(userId,createTime,deleteStatus,status);
create index idx_order2_status_deleteStatus_createTime_id on t_order2(status, deleteStatus, createTime, id);

create index idx_order3_userId_createTime_deleteStatus_status on t_order3(userId,createTime,deleteStatus,status);
create index idx_order3_status_deleteStatus_createTime_id on t_order3(status, deleteStatus, createTime, id);

create index idx_order4_userId_createTime_deleteStatus_status on t_order4(userId,createTime,deleteStatus,status);
create index idx_order4_status_deleteStatus_createTime_id on t_order4(status, deleteStatus, createTime, id);

create index idx_order5_userId_createTime_deleteStatus_status on t_order5(userId,createTime,deleteStatus,status);
create index idx_order5_status_deleteStatus_createTime_id on t_order5(status, deleteStatus, createTime, id);

create index idx_order6_userId_createTime_deleteStatus_status on t_order6(userId,createTime,deleteStatus,status);
create index idx_order6_status_deleteStatus_createTime_id on t_order6(status, deleteStatus, createTime, id);

create index idx_order7_userId_createTime_deleteStatus_status on t_order7(userId,createTime,deleteStatus,status);
create index idx_order7_status_deleteStatus_createTime_id on t_order7(status, deleteStatus, createTime, id);

create index idx_order8_userId_createTime_deleteStatus_status on t_order8(userId,createTime,deleteStatus,status);
create index idx_order8_status_deleteStatus_createTime_id on t_order8(status, deleteStatus, createTime, id);

create index idx_orderDetail_merchantId on t_order_detail(merchantId);
create index idx_orderDetail_merchantId_orderId on t_order_detail(merchantId, orderId);

create index idx_orderDetail1_merchantId on t_order_detail1(merchantId);
create index idx_orderDetail1_merchantId_orderId on t_order_detail1(merchantId, orderId);

create index idx_orderDetail2_merchantId on t_order_detail2(merchantId);
create index idx_orderDetail2_merchantId_orderId on t_order_detail2(merchantId, orderId);

create index idx_orderDetail3_merchantId on t_order_detail3(merchantId);
create index idx_orderDetail3_merchantId_orderId on t_order_detail3(merchantId, orderId);

create index idx_orderDetail4_merchantId on t_order_detail4(merchantId);
create index idx_orderDetail4_merchantId_orderId on t_order_detail4(merchantId, orderId);

create index idx_orderDetail5_merchantId on t_order_detail5(merchantId);
create index idx_orderDetail5_merchantId_orderId on t_order_detail5(merchantId, orderId);

create index idx_orderDetail6_merchantId on t_order_detail6(merchantId);
create index idx_orderDetail6_merchantId_orderId on t_order_detail6(merchantId, orderId);

create index idx_orderDetail7_merchantId on t_order_detail7(merchantId);
create index idx_orderDetail7_merchantId_orderId on t_order_detail7(merchantId, orderId);

create index idx_orderDetail8_merchantId on t_order_detail8(merchantId);
create index idx_orderDetail8_merchantId_orderId on t_order_detail8(merchantId, orderId);

数据初始化完毕后,分别重启各个 SpringBoot 应用以加载订单 ID

bash
cd ~/deployer-order-management-app-sharding/service && docker compose pull && docker compose restart

测试根据订单 ID 查询

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/getById
    Running 1m test @ http://10.138.0.3/api/v1/order/getById
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency    52.86ms   88.12ms   1.72s    98.15%
        Req/Sec     5.92k   772.84     8.13k    96.18%
      Latency Distribution
         50%   40.95ms
         75%   50.69ms
         90%   63.44ms
         99%  494.52ms
      2821798 requests in 1.00m, 1.64GB read
    Requests/sec:  46976.04
    Transfer/sec:     27.89MB

测试用户查询指定日期范围+所有状态的订单,查询条件用户ID+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndWithoutStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   183.44ms  307.82ms   2.64s    89.25%
        Req/Sec     3.38k     1.78k    5.98k    55.76%
      Latency Distribution
         50%   54.10ms
         75%  173.88ms
         90%  528.64ms
         99%    1.50s
      1614555 requests in 1.00m, 1.96GB read
    Requests/sec:  26865.53
    Transfer/sec:     33.37MB

测试用户查询指定日期范围+指定状态的订单,查询条件用户ID+订单状态+订单删除状态(固定值未删除)+日期范围+分页,新的订单排在最前。

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/listByUserIdAndStatus
    Running 1m test @ http://10.138.0.3/api/v1/order/listByUserIdAndStatus
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   167.76ms  357.78ms   3.06s    90.46%
        Req/Sec     5.19k     2.39k    7.94k    77.52%
      Latency Distribution
         50%   37.82ms
         75%   71.53ms
         90%  489.52ms
         99%    1.79s
      2476466 requests in 1.00m, 1.04GB read
    Requests/sec:  41223.54
    Transfer/sec:     17.67MB

测试新增订单

  • wrk 测试

    bash
    $ wrk -t8 -c2048 -d60s --latency --timeout 30 http://10.138.0.3/api/v1/order/create
    Running 1m test @ http://10.138.0.3/api/v1/order/create
      8 threads and 2048 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   370.12ms  571.49ms   7.55s    88.82%
        Req/Sec     1.45k   697.27     2.42k    73.99%
      Latency Distribution
         50%  206.89ms
         75%  394.52ms
         90%    1.03s
         99%    2.83s
      638159 requests in 1.00m, 154.64MB read
    Requests/sec:  10622.55
    Transfer/sec:      2.57MB
总结
  • 水平分库和分表能够很好地扩展读写性能。

插入

详细用法请参考本站 示例

测试配置:

  • CPU 架构 Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
  • 5台 2C4G 的 CentOS8 主机运行数据库服务,数据库 InnoDB 内存为 2G
  • 7台 4C6G 的 CentOS8 主机运行 Sharding-JDBC 基准测试应用充当压力机

测试结果:

  • 压力机最长耗时为 6 秒

测试结论:

  • Sharding-JDBC 能够水平扩展数据库的写入数据性能。