分库分表
介绍
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 基础镜像,如下:
FROM mariadb:11.4修改 application.properties 中的配置指向数据库服务
spring.datasource.url=jdbc:mariadb://192.168.1.190:3306/demo?useSSL=false&allowPublicKeyRetrieval=true编译并推送镜像
./build.sh && ./push.sh准备数据库服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-poc启动数据库服务
bashcd ~/deployer-poc/common && docker compose down -v && docker compose pull && docker compose up -d
准备 SpringBoot 应用服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-poc启动 SpringBoot 服务
bashcd ~/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; }启动服务
bashdocker compose up -d
使用示例中的 rest-api.http 测试接口是否正常
准备 wrk 压力机
- 参考 链接 编译 wrk
500万数据
安装 Apache Bench 工具:参考 链接
初始化数据
ab -n 5000000 -c 64 -k http://192.168.1.185/api/v1/order/create数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID
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 工具:参考 链接
初始化数据
ab -n 10000000 -c 64 -k http://192.168.1.185/api/v1/order/create数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID
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 工具:参考 链接
初始化数据
ab -n 20000000 -c 64 -k http://192.168.1.185/api/v1/order/create数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID
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 工具:参考 链接
初始化数据
ab -n 50000000 -c 64 -k http://192.168.1.185/api/v1/order/create数据初始化完毕后,分别重启各个 SpringBoot 应用以加载所有订单 ID
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_profile和users_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 的订单)
优点: 大幅提高数据库处理能力,提升扩展性。
缺点: 实现复杂,需要考虑数据一致性、事务、路由等问题;跨库查询变得非常困难;数据迁移和维护成本高。
总结:
不同的分库分表方式适用于不同的场景。 垂直分表和垂直分库比较容易实现,但扩展性有限;水平分表和水平分库扩展性好,但实现复杂。 实际应用中,常常会结合多种方式来解决问题,例如,可以先进行垂直分库,再对某些大表进行水平分库分表。 选择合适的方案需要根据具体的业务需求、数据量、性能要求等因素综合考虑。 同时,也需要选择合适的中间件来简化分库分表的开发和维护工作。
分库分表带来的新问题
- 事务一致性问题
- 跨节点关联查询
- 跨节点分页、排序
- 主键冲突
- 公共表
分库分表算法
基因算法
详细细节请参考本站 示例
/**
* 分库分表策略的基因算法
* 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 配置如下:
<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 配置如下:
spring.datasource.driver-class-name=org.apache.shardingsphere.driver.ShardingSphereDriver
spring.datasource.url=jdbc:shardingsphere:classpath:sharding.yamlsharding-jdbc 的 sharding.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 配置如下:
<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 配置如下:
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 解决此问题:
@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 配置如下:
# 指定order表的主键生成策略为SNOWFLAKE
spring.shardingsphere.sharding.tables.order.key-generator.column=id
spring.shardingsphere.sharding.tables.order.key-generator.type=SNOWFLAKEOrderMapper 的新增方法 SQL 如下:
@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
@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
用于配置数据的数据源和数据表的分布节点信息。
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语句中的
=、IN和BETWEEN 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:指定范围查询分片算法的实现类
样例:
# 配置数据源分片算法
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:指定范围查询分片算法的实现类
样例:
# 配置数据表分片算法
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的计算分片
样例:
# 配置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的结果归并算法从功能上主要分为以下几种类型,每种类型都服务于不同的查询需求,它们之间是组合而非互斥的关系:
- 遍历归并:
- 当只有一个数据节点返回结果集时,Sharding-JDBC会使用遍历归并。此时,归并过程相对简单,只需将单个结果集的数据返回即可。
- 排序归并:
- 当查询语句包含排序要求时,需要使用排序归并。
- 排序归并属于流式归并的一种,通过优先级队列(如最小堆或最大堆)实现。
- 初始时,会对各个数据节点返回的结果集进行初始排序,并将其头部元素放入优先级队列。
- 每次获取元素时,从优先级队列中取出最小的(或最大的)元素,然后将其所在结果集的下一个元素放入队列,并重新调整队列的优先级。
- 这样可以确保每次获取的元素都是全局有序的。
- 分组归并:
- 当查询语句包含分组要求时,需要使用分组归并。
- 分组归并又可分为流式分组归并和内存分组归并两种。
- 流式分组归并适用于分组字段较少且内存消耗不大的情况。它会逐条处理结果集中的数据,根据分组字段将数据分组存储。
- 内存分组归并则适用于分组字段较多或内存消耗较大的情况。它会将结果集的所有数据加载到内存中,再进行分组处理。
- 分页归并:
- 当查询语句包含分页要求时,需要使用分页归并。
- 分页归并通常是在其他归并类型的基础上进行的装饰者归并。
- 它会根据分页参数(如偏移量offset和限制行数limit)对结果集进行裁剪,只返回所需的数据行。
- 聚合归并:
- 当查询语句包含聚合函数(如SUM、COUNT、AVG等)时,需要使用聚合归并。
- 聚合归并会对各个数据节点返回的结果集进行聚合计算,返回最终的聚合结果。
此外,从结构划分上,Sharding-JDBC的结果归并算法还可以分为流式归并、内存归并和装饰者归并三种:
- 流式归并:
- 流式归并是指每次只处理结果集中的一条数据,通过逐条处理的方式返回正确的单条数据。
- 它的优点是能够极大减少内存的消耗,适用于结果集较大的情况。
- 遍历、排序以及流式分组都属于流式归并的一种。
- 内存归并:
- 内存归并是指将结果集的所有数据都加载到内存中,再进行归并处理。
- 它的优点是处理速度快,但缺点是内存消耗较大。
- 内存分组归并属于内存归并的一种。
- 装饰者归并:
- 装饰者归并是指在其他归并类型的基础上进行的进一步处理。
- 它的优点是可以灵活地对结果集进行各种处理,如分页、过滤等。
- 分页归并通常是在其他归并类型的基础上进行的装饰者归并。
综上所述,Sharding-JDBC提供了多种结果归并算法以满足不同的查询需求。这些归并算法在功能上是组合而非互斥的,可以根据实际情况进行选择和组合使用。
分片策略和分片算法
介绍
Sharding-JDBC 的分片策略和分片算法是其实现数据分片功能的核心组件。以下是对这两者的详细解释:
一、分片策略(Sharding Strategy)
分片策略决定了数据应该被分配到哪些数据源或表中。Sharding-JDBC 提供了多种分片策略,以满足不同的业务需求。
- 标准分片策略(Standard Sharding Strategy):
- 提供对SQL语句中的
=、IN和BETWEEN AND的分片操作支持。 - 只支持单分片键。
- 包含两个分片算法:
PreciseShardingAlgorithm(用于处理=和IN的分片)和RangeShardingAlgorithm(用于处理BETWEEN AND分片)。其中,PreciseShardingAlgorithm是必选的,而RangeShardingAlgorithm是可选的。
- 提供对SQL语句中的
- 复合分片策略(Complex Sharding Strategy):
- 同样提供对SQL语句中的
=、IN和BETWEEN AND的分片操作支持。 - 支持多分片键。由于多分片键之间的关系复杂,因此Sharding-JDBC并未做过多的封装,而是直接将分片键值组合以及分片操作符交于算法接口,由应用开发者自行实现,以提供最大的灵活度。
- 同样提供对SQL语句中的
- 行表达式分片策略(Inline Sharding Strategy):
- 使用Groovy的行表达式,提供对SQL语句中的
=和IN的分片操作支持。 - 只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发。例如,
t_user_${user_id % 8}表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0到t_user_7。
- 使用Groovy的行表达式,提供对SQL语句中的
- Hint分片策略(Hint Sharding Strategy):
- 不通过SQL解析的方式分片,而是通过代码动态指定路由规则。
- 不分片策略(None Sharding Strategy):
- 不进行分片操作。
二、分片算法(Sharding Algorithm)
分片算法是实现具体分片逻辑的关键。Sharding-JDBC并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供的分片算法接口主要有以下几种:
- PreciseShardingAlgorithm:
- 用于处理使用单一键作为分片键的
=与IN进行分片的场景。 - 需要配合StandardShardingStrategy使用。
- 用于处理使用单一键作为分片键的
- RangeShardingAlgorithm:
- 用于处理使用单一键作为分片键的
BETWEEN AND进行分片的场景。 - 需要配合StandardShardingStrategy使用。
- 用于处理使用单一键作为分片键的
- ComplexKeysShardingAlgorithm:
- 用于处理使用多键作为分片键进行分片的场景。
- 需要配合ComplexShardingStrategy使用。
- HintShardingAlgorithm:
- 用于处理使用Hint行分片的场景。
- 需要配合HintShardingStrategy使用。
开发者可以根据自己的业务需求,实现上述接口中的doSharding方法,以定义具体的分片逻辑。例如,在实现PreciseShardingAlgorithm接口时,可以根据分片键的值来计算目标数据源或表的名称;在实现RangeShardingAlgorithm接口时,可以根据分片键的范围来确定目标数据源或表的集合。
总的来说,Sharding-JDBC的分片策略和分片算法为开发者提供了高度的灵活性和可扩展性,使得他们可以根据自己的业务需求来定制数据分片方案。
PreciseShardingAlgorithm
标准分片策略的精准分片算法。用于处理使用单一键作为分片键的
=与IN进行分片的场景。详细用法请参考本站 示例
配置的详细解释请参考本站 链接
例子:
application.properties 配置如下:
propertiesspring.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 配置如下:
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数据源分片算法
/**
* 数据源范围分片算法
*/
@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;
}
}数据表分片算法
/**
* 数据表范围分片算法
*/
@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 范围查询
@Select("select * from `order` where user_id>=#{startUserId} and user_id<=#{endUserId}")
List<Order> listByUserIdRange(@Param("startUserId") Long startUserId, @Param("endUserId") Long endUserId);范围查询测试代码
/**
* 测试标准分片策略的范围分片算法
*/
@Test
public void testStandardStrategyRangeShardingAlgorithm() {
List<Order> orderList = this.orderMapper.listByUserIdRange(1L, 10000L);
Assertions.assertEquals(0, orderList.size());
}不分片策略
通过配置默认数据源,没有配置分片策略的表会自动使用默认数据源操作数据库。
详细用法请参考本站 示例
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=123456ComplexKeysShardingAlgorithm
详细用法请参考本站 示例:通过订单 ID 或者用户 ID 查询订单信息。
分库分表基因算法 和复合键分片算法结合实现通过订单ID或者用户ID查询订单信息。
算法实现
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());
}
}配置使用算法
# 指定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 分库分表规模中。
@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 连接池
详细用法请参考本站 示例
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 事务
方式使用 @Transactional 注解即可
@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水平分表的配置与实现通常包括以下几个步骤:
- 引入Maven依赖:在项目的pom.xml文件中添加Sharding-JDBC的依赖。
- 配置数据源:在application.yml或application.properties文件中配置多个数据源,以及数据源之间的连接信息。
- 定义分片规则:在配置文件中定义分片规则,包括分片字段、分片策略等。
- 编写代码:根据业务需求编写代码,Sharding-JDBC会根据分片规则自动将数据路由到相应的数据源。
四、优点与限制
Sharding-JDBC水平分表的优点主要包括:
- 提高数据库扩展性:通过水平分表,可以将数据分散到多个表中,从而提高数据库的存储能力和处理能力。
- 提高查询性能:由于数据被分散到多个表中,查询时可以并行处理,从而提高查询性能。
- 简化数据库维护:通过水平分表,可以将大表拆分成小表,便于管理和维护。
然而,Sharding-JDBC水平分表也存在一些限制:
- 分片规则设计复杂:需要根据业务需求设计合理的分片规则,否则可能导致数据分布不均或查询性能下降。
- 跨库事务处理困难:由于数据被分散到多个数据源中,跨库事务的处理变得复杂且性能较低。
- 数据迁移和备份困难:水平分表后,数据的迁移和备份需要考虑到多个数据源之间的数据同步和一致性。
总之,Sharding-JDBC水平分表是一种有效的数据库分片技术,可以提高数据库的扩展性和查询性能。但在使用时需要充分考虑分片规则的设计、跨库事务的处理以及数据迁移和备份等限制因素。
配置
application.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
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 表为逻辑表
@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 代码如下:
@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 代码如下:
/**
* 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 代码如下:
@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 代码如下:
@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水平分库的实现原理主要包括以下几个方面:
- 数据源配置:在Sharding-JDBC中,需要配置多个数据源,每个数据源对应一个物理数据库。这些数据源可以是不同类型的数据库,但通常是同构的,即具有相同的表结构和数据类型。
- 分片规则:根据业务需求,定义数据分片的规则。分片规则决定了如何将数据分散到多个数据库中。常见的分片规则包括哈希分片、范围分片、列表分片等。
- SQL解析与改写:当应用程序执行SQL语句时,Sharding-JDBC会对SQL语句进行解析,识别出表名、字段名、条件等关键信息。然后,根据分片规则,将SQL语句改写为多个子查询,每个子查询对应一个数据源。
- 路由与执行:Sharding-JDBC会根据改写后的SQL语句,将查询请求路由到相应的数据源。然后,在每个数据源上执行子查询,并将结果集返回给应用程序。
- 结果归并:如果查询请求涉及多个数据源,Sharding-JDBC会对返回的结果集进行归并处理,得到最终的结果集。结果归并算法包括遍历归并、排序归并、分组归并等。
三、配置与实现
Sharding-JDBC水平分库的配置与实现通常包括以下几个步骤:
- 引入Maven依赖:在项目的pom.xml文件中添加Sharding-JDBC的依赖。
- 配置数据源:在配置文件(如application.yml或application.properties)中配置多个数据源,包括数据库的连接信息、用户名、密码等。
- 定义分片规则:在配置文件中定义分片规则,包括分片字段、分片策略、分片算法等。
- 编写代码:在代码中配置Sharding-JDBC的分片上下文,并编写业务逻辑代码。Sharding-JDBC会根据分片规则自动将数据路由到相应的数据源。
四、优点与限制
Sharding-JDBC水平分库的优点主要包括:
- 提高系统扩展性:通过水平分库,可以将数据分散到多个数据库中,从而提高系统的存储能力和处理能力。
- 提高查询性能:由于数据被分散到多个数据库中,查询时可以并行处理,从而提高查询性能。
- 简化数据库维护:通过水平分库,可以将大数据库拆分成小数据库,便于管理和维护。
然而,Sharding-JDBC水平分库也存在一些限制:
- 分片规则设计复杂:需要根据业务需求设计合理的分片规则,否则可能导致数据分布不均或查询性能下降。
- 跨库事务处理困难:由于数据被分散到多个数据库中,跨库事务的处理变得复杂且性能较低。
- 数据迁移和备份困难:水平分库后,数据的迁移和备份需要考虑到多个数据库之间的数据同步和一致性。
综上所述,Sharding-JDBC水平分库是一种有效的数据库分片技术,可以提高系统的扩展性和查询性能。但在使用时需要充分考虑分片规则的设计、跨库事务的处理以及数据迁移和备份等限制因素。
配置
application.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
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实现垂直分库的步骤:
- 需求分析:明确垂直分库的需求和目标,确定哪些表需要被分到不同的数据库中。
- 创建数据库和表:根据需求分析的结果,在相应的服务器上创建数据库和表。
- 编写代码:
- 创建实体类和对应的Mapper、Controller、Service等。
- 配置Sharding-JDBC的分片策略。在配置文件中,指定数据源名称、数据库连接池、数据库驱动类名、数据库URL连接、数据库用户名和密码等信息。
- 配置垂直分库策略,包括指定逻辑表、实际数据节点、分片列和分片算法等。
- 测试:编写测试代码,验证垂直分库是否成功。可以通过插入数据并查询来验证分片策略是否正确。
需要注意,垂直分库虽然能提高性能和业务清晰度,但也会带来一些复杂问题,如跨库事务、数据一致性等。因此,在实现垂直分库时,需要充分考虑这些问题,并采取相应的措施来解决。
此外,Sharding-JDBC的版本更新较快,不同版本之间可能存在一些差异。因此,在实现垂直分库时,需要参考当前版本的Sharding-JDBC文档和示例代码,以确保正确实现和配置。
总的来说,使用Sharding-JDBC实现垂直分库是一个复杂但有效的过程,需要充分考虑业务需求和技术实现等因素。通过合理配置和测试,可以实现性能的提升和业务清晰度的提高。
配置
application.properties 配置如下:
# 逻辑表user在数据源ds1的物理表user中
spring.shardingsphere.sharding.tables.user.actual-data-nodes=ds1.user预先在数据库中创建 user 表
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垂直分表的详细解释:
一、定义
垂直分表是指将一张宽表(包含较多字段的表)按照字段进行拆分,拆分成多张窄表(包含较少字段的表)。这些窄表在逻辑上仍然属于同一张表,但在物理上被存储在不同的数据库表中。
二、优势
- 提高性能:通过将热点字段和冷字段分开存储,可以减少I/O争抢,提高数据库性能。同时,由于每张窄表只包含部分字段,查询时可以更快地定位到所需的数据,减少不必要的字段扫描。
- 优化资源使用:垂直分表可以将数据分散到多个表中,从而优化数据库服务器的资源使用。例如,可以将经常访问的字段放在同一个表中,以减少磁盘I/O和内存使用。
- 业务清晰度提升:通过垂直分表,可以将不同业务逻辑的字段分开存储,使得数据库结构更加清晰,便于维护和扩展。
三、原则
- 冷热数据分离:将经常访问的字段(热点数据)和不常访问的字段(冷数据)分开存储。
- 大字段拆分:将大文本、图片、视频等大字段拆分出来单独存储,以减少对数据库性能的影响。
- 组合查询优化:将经常组合查询的字段放在同一张表中,以避免多表查询带来的性能开销。
四、实现步骤
- 创建数据库和表:首先,需要在数据库中创建多个窄表,这些窄表将存储拆分后的字段。
- 配置Sharding-JDBC:在Sharding-JDBC的配置文件中,指定垂直分表的策略,包括分片键、分片算法等。
- 编写SQL语句:在编写SQL语句时,需要注意使用逻辑表名而不是物理表名。Sharding-JDBC会根据配置的策略将逻辑表名映射到实际的物理表名上。
五、注意事项
- 避免联查:在垂直分表后,尽量避免跨表查询,因为跨表查询会带来额外的性能开销。如果必须跨表查询,可以考虑使用数据库视图或中间层来优化查询性能。
- 数据一致性:在垂直分表后,需要确保不同表之间的数据一致性。这可以通过事务管理、数据校验等方式来实现。
- 分片策略选择:在选择分片策略时,需要根据业务需求和数据库性能进行权衡。例如,可以根据字段的访问频次、数据分布等因素来选择合适的分片键和分片算法。
综上所述,Sharding-JDBC的垂直分表技术是一种有效的数据库优化手段,可以显著提高数据库性能和资源使用效率。但在实际应用中,需要根据业务需求和数据库性能进行权衡和选择。
实验
垂直分表可以参考绑定表章节,它们本质是同一个事情。
公共表
介绍
Sharding-JDBC中的公共表(也称为广播表)是一个重要的概念,尤其在处理分库分表的场景中。以下是对Sharding-JDBC公共表的详细解析:
一、定义与特点
公共表是指存储固定数据、数据很少发生变化且经常进行关联查询的表。这类表通常包括参数表、数据字典表等。在Sharding-JDBC中,公共表需要在每个分片数据源中都存在,且表结构和表中的数据在每个数据库中都完全一致。
二、实现方式
- 创建公共表:首先,需要在每个数据库中手动创建出结构相同的公共表。
- 配置公共表:在Sharding-JDBC的配置文件中,需要指定哪些表是公共表。例如,在application.properties或application.yml配置文件中,可以通过设置
spring.shardingsphere.sharding.broadcast-tables属性来指定公共表。
三、使用场景与优势
- 使用场景:公共表通常用于存储一些全局的、不经常变动的数据,如配置信息、数据字典等。这些数据需要在多个分片之间进行共享和关联查询。
- 优势:
- 简化查询:通过将公共表复制到每个分片中,可以简化跨分片的关联查询,提高查询效率。
- 数据一致性:由于公共表在每个分片中都存在且数据一致,因此可以确保在不同分片上进行查询时得到相同的结果。
四、注意事项
- 数据更新:对公共表的更新操作需要同时发送到所有分片执行,以确保数据的一致性。这可能会增加一些额外的开销和复杂性。
- 表结构变更:如果需要对公共表的结构进行变更(如添加列、修改数据类型等),需要在所有分片中同时执行相应的DDL操作。
五、示例配置
以下是一个简单的示例配置,展示了如何在Sharding-JDBC中配置公共表:
# 指定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 配置如下:
spring.shardingsphere.sharding.broadcast-tables=dict分别在各个数据库创建 dict 表 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 读写分离的详细介绍:
一、读写分离的背景与原理
- 背景:
- 面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。
- 对于同一时刻有大量并发读操作和较少写操作类型的应用系统,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够避免由数据更新导致的行锁,从而提升系统的查询性能。
- 原理:
- 数据库主从复制技术:把数据复制到多个节点中,分散读多个库以支持高并发的读,而写只在主库上。
- Sharding-JDBC 通过简单的开发,实现读写分离技术。它提供了一主多从的读写分离配置,可独立使用,也可配合分库分表使用。
二、Sharding-JDBC 读写分离的配置与实现
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>配置文件:
在 Spring Boot 的配置文件(如 application.yml)中配置数据源和读写分离规则。例如:
yamlspring: 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-slave2Java 代码配置:
在 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()); } }
三、读写分离的效果与注意事项
- 效果:
- 通过读写分离,可以将查询请求均匀地分散到多个数据副本,进一步提升系统的处理能力。
- 使用多主多从的方式,不仅能提升系统的吞吐量,还能提升系统的可用性,确保在任何一个数据库宕机或磁盘物理损坏的情况下,系统仍能正常运行。
- 注意事项:
- 主从节点数据同步延迟可能导致的数据不一致问题。对于时效性比较高的查询,可以强制路由到主节点查询。
- Sharding-JDBC 不支持类似主库双写或多写这样的特性,需要确保写操作只发生在主库上。
综上所述,Sharding-JDBC 读写分离是一种有效的数据库性能优化手段,适用于读多写少的应用场景。通过合理的配置和实现,可以显著提升系统的查询性能和可用性。
绑定表
介绍
Sharding-JDBC中的绑定表(也称为关联表)是指分片规则一致的主表和子表。以下是对Sharding-JDBC绑定表的详细解释:
一、定义与概念
绑定表是指两张或多张表之间因为分片规则相同而建立的关联关系。在Sharding-JDBC中,如果两张表互为绑定表关系,那么它们在进行多表关联查询时,不会出现笛卡尔积关联,从而可以大大提升关联查询的效率。
二、应用场景
绑定表通常应用于具有主从关系或父子关系的表结构中,例如订单表和订单项表。假设有一个订单表(t_order)和一个订单项表(t_order_item),它们都按照订单ID(order_id)进行分片。在这种情况下,可以将这两张表设置为绑定表关系,以便在进行订单和订单项的关联查询时,能够高效地获取数据。
三、配置方法
在Sharding-JDBC中配置绑定表关系,通常需要在配置文件(如properties文件或yml文件)中进行设置。以下是一个示例配置:
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将会按照绑定表的规则进行路由和查询。
四、注意事项
- 分片键要相同:绑定表之间的分片键必须完全相同,否则无法进行绑定。
- 主表策略:在绑定表关系中,ShardingSphere将会以主表(即在FROM子句中最左侧的表)作为整个绑定表的主表。所有路由计算都只会使用主表的策略。
- 性能提升:配置绑定表关系后,可以显著提升多表关联查询的性能,避免产生笛卡尔积关联。
五、示例
假设有一个订单系统,其中订单表(t_order)和订单项表(t_order_item)都按照订单ID(order_id)进行分片。以下是一个简单的示例,展示了如何在Sharding-JDBC中配置和使用绑定表:
- 创建数据库和表:在数据库中创建订单表和订单项表,并设置相同的分片键。
- 配置Sharding-JDBC:在Sharding-JDBC的配置文件中,将订单表和订单项表设置为绑定表关系。
- 执行关联查询:在执行订单和订单项的关联查询时,Sharding-JDBC将会按照绑定表的规则进行路由和查询,从而提升查询性能。
综上所述,Sharding-JDBC的绑定表功能是一种高效处理多表关联查询的机制。通过合理配置绑定表关系,可以显著提升查询性能并优化数据库分片策略。
演示
使用商品主表和商品描述子表演示绑定表用法。
在数据库中创建 product 主表和 product_description 子表如下:
-- 用于协助演示绑定表关系
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 绑定表配置如下:
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_descriptionproduct 和 product_description 在查询 join 时不会发生迪卡尔积(product_1 和 product_description_1 join、product_2 和 product_description_2 join)
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 中的配置指向数据库服务
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 配置
编译并推送镜像
./build.sh && ./push.sh准备数据库服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动数据库服务
bashcd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d
准备 SpringBoot 应用服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动 SpringBoot 服务
bashcd ~/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; }启动服务
bashdocker compose up -d
使用示例中的 rest-api.http 测试接口是否正常
准备 wrk 压力机
- 参考 链接 编译 wrk
安装 Apache Bench 工具:参考 链接
批量初始化数据
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址
curl 10.138.0.5:8080/api/v1/order/init初始化数据后建立索引(批量初始化数据时没有索引会更快)
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
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.72MBi/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.34MBi/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.38MBi/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.92KBi/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 中的配置指向数据库服务
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 配置
编译并推送镜像
./build.sh && ./push.sh准备数据库服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动数据库服务
bashcd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d
准备 SpringBoot 应用服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动 SpringBoot 服务
bashcd ~/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; }启动服务
bashdocker compose up -d
使用示例中的 rest-api.http 测试接口是否正常
准备 wrk 压力机
- 参考 链接 编译 wrk
安装 Apache Bench 工具:参考 链接
批量初始化数据
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址
curl 10.138.0.5:8080/api/v1/order/init初始化数据后建立索引(批量初始化数据时没有索引会更快)
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
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.68MBi/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.33MBi/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.09MBi/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.53KBi/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 中的配置指向数据库服务
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 配置
编译并推送镜像
./build.sh && ./push.sh准备数据库服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动商品数据库服务
bashcd ~/deployer-order-management-app-sharding/common && docker compose down -v && docker compose pull && docker compose up -d启动订单数据库服务
bashcd ~/deployer-order-management-app-sharding/common-order-db && docker compose down -v && docker compose pull && docker compose up -d
准备 SpringBoot 应用服务
复制示例中的 deployer 目录到实例中
bashscp -r deployer [email protected]:~/deployer-order-management-app-sharding启动 SpringBoot 服务
bashcd ~/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; }启动服务
bashdocker compose up -d
使用示例中的 rest-api.http 测试接口是否正常
准备 wrk 压力机
- 参考 链接 编译 wrk
安装 Apache Bench 工具:参考 链接
批量初始化数据
ab -n 50000 -c 64 -k http://10.138.0.3/api/v1/order/initInsertBatch初始化 id 缓存辅助数据,10.138.0.5 是其中一个 SpringBoot 应用的节点 ip 地址
curl 10.138.0.5:8080/api/v1/order/init初始化数据后建立索引(批量初始化数据时没有索引会更快)
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
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 能够水平扩展数据库的写入数据性能。