做后端开发的同学应该都有同感,业务跑起来之后,数据量涨得比预期快得多。我们项目的订单模块刚上线时单库单表跑得好好的,最近半年订单量翻了几倍,查询卡顿、写入延迟的问题越来越明显,排查下来单表数据量过大是核心原因。
趁着版本迭代,直接上手用SpringBoot3整合ShardingSphere-JDBC做水平分库分表,踩了不少版本适配和配置写法的坑,也摸透了5.x版本的实战规范。本文从库表搭建、依赖引入、核心配置到测试验证,一步一步写清楚,看完直接复制代码就能跑通,适合直接落地到实际项目里。
一、为什么需要分库分表?
从性能方面来说,由于关系型数据库大多采用B+树类型的索引,在数据量超过阈值的情况下,索引深度的增加也将使得磁盘访问的IO次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。
从可用性的方面来讲,服务化的无状态型,能够达到较小成本的随意扩容,这必然导致系统的最终压力都落在数据库之上。而单一的数据节点,或者简单的主从架构,已经越来越难以承担。
当一个数据库实例中的数据达到阈值以上,对于DBA的运维压力就会增大。一般来讲,单一数据库实例的数据的阈值在1TB之内,是比较合理的范围。
二、ShardingSphere核心概念
ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款相互独立的产品组成。
ShardingSphere-JDBC:定位为轻量级Java框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。本项目采用这种方式。
ShardingSphere-Proxy:定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,可用于异构语言支持。
本次实战的核心版本:
-
Spring Boot:3.2.5
-
ShardingSphere-JDBC:5.5.2
-
MyBatis-Plus:3.5.15
-
Java:17
-
MySQL:8.0
三、实战场景:按用户ID分库分表
本次实战场景:按用户ID取模实现水平分库分表,搭建2个分库(ds0、ds1),每个库对应2张订单物理表(t_order_0、t_order_1),总共4张表分散数据压力,代码层无感操作,无需改动原有业务逻辑。
3.1 前期准备:手动创建分库分表
实战中一定要先把物理库表搭建到位,而且所有分表结构必须完全一致,这是分库分表正常运行的基础。
-- 1. 创建2个分库
CREATE DATABASE IF NOT EXISTS order_db_0;
CREATE DATABASE IF NOT EXISTS order_db_1;
-- 2. 在order_db_0中创建分表
USE order_db_0;
CREATE TABLE `t_order_0` (
`id` bigint NOT NULL COMMENT '订单主键(分布式ID)',
`order_no` varchar(64) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID(分片键)',
`goods_name` varchar(255) DEFAULT NULL COMMENT '商品名称',
`order_amount` decimal(10,2) DEFAULT NULL COMMENT '订单金额',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';
-- 直接复制表结构,保证完全一致
CREATE TABLE `t_order_1` LIKE `t_order_0`;
-- 3. 在order_db_1中创建分表(表结构完全一致)
USE order_db_1;
CREATE TABLE `t_order_0` LIKE order_db_0.t_order_0;
CREATE TABLE `t_order_1` LIKE order_db_0.t_order_0;
3.2 SpringBoot3项目搭建,引入正确核心依赖
新建普通SpringBoot3项目,以下是适配SpringBoot3的完整实测依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>shardingsphere-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shardingsphere-demo</name>
<description>SpringBoot3+ShardingSphere-JDBC分库分表实战</description>
<properties>
<java.version>17</java.version>
<shardingsphere.version>5.5.2</shardingsphere.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
</properties>
<dependencies>
<!-- SpringBoot Web 基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- ShardingSphere-JDBC 核心依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!-- MySQL8 官方驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus SpringBoot3 适配版 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 常用工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<!-- Lombok 简化实体类代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.3 核心配置:分库分表规则
ShardingSphere 5.x版本推荐使用分离式配置,把核心数据源和分片规则单独抽离成配置文件,相比单yml内嵌配置,加载更稳定、可读性更强,也能彻底避免因配置嵌套缩进导致的规则加载失败问题。
application.yml(放在resources目录下):
spring:
application:
name: shardingsphere-demo
# 关闭自动配置的数据源,使用ShardingSphere的数据源
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
# ShardingSphere配置
shardingsphere:
datasource:
names: ds0, ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db_0?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/order_db_1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 123456
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: table-inline
key-generate-strategy:
column: id
key-generator-name: snowflake
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 2}
key-generators:
snowflake:
type: SNOWFLAKE
props:
sql-show: true # 开启SQL日志,在控制台打印实际执行的SQL语句,便于调试
配置要点说明:
-
actual-data-nodes:
ds$->{0..1}.t_order_$->{0..1}表示数据分布在ds0、ds1两个库,每个库的t_order_0、t_order_1两张表 -
分库策略:根据user_id % 2的结果,决定数据进入ds0还是ds1
-
分表策略:根据user_id % 2的结果,决定数据进入t_order_0还是t_order_1
-
主键生成策略:使用雪花算法生成分布式ID,避免分表后自增ID冲突
四、业务代码编写:和单表操作完全一致
ShardingSphere-JDBC最大的优势就是无侵入性,不需要修改原有业务代码,照常编写实体类、Mapper、Service、Controller即可,和操作单表流程完全一样。
4.1 订单实体类(对应逻辑表t_order)
package com.example.shardingspheredemo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体类
* 仅对应逻辑表t_order,无需关注物理分表
*/
@Data
@TableName("t_order") // 逻辑表名
public class Order {
private Long id; // 分布式ID,由ShardingSphere自动生成
private String orderNo; // 订单编号
private Long userId; // 用户ID(分片键)
private String goodsName; // 商品名称
private BigDecimal orderAmount; // 订单金额
private LocalDateTime createTime; // 创建时间
}
4.2 Mapper层
package com.example.shardingspheredemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shardingspheredemo.entity.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
4.3 Service层
package com.example.shardingspheredemo.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.shardingspheredemo.entity.Order;
import com.example.shardingspheredemo.mapper.OrderMapper;
import org.springframework.stereotype.Service;
@Service
public class OrderService extends ServiceImpl<OrderMapper, Order> {
}
4.4 Controller测试接口
package com.example.shardingspheredemo.controller;
import cn.hutool.core.util.IdUtil;
import com.example.shardingspheredemo.entity.Order;
import com.example.shardingspheredemo.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 新增订单测试接口
* @param userId 用户ID(分片键)
*/
@GetMapping("/add")
public String addOrder(Long userId) {
Order order = new Order();
// ID由ShardingSphere的雪花算法自动生成,无需手动设置
order.setOrderNo(IdUtil.fastSimpleUUID()); // 使用Hutool生成简单订单号
order.setUserId(userId);
order.setGoodsName("测试商品-" + System.currentTimeMillis());
order.setOrderAmount(new BigDecimal("199.00"));
order.setCreateTime(LocalDateTime.now());
orderService.save(order);
return "订单添加成功,用户ID: " + userId;
}
/**
* 根据用户ID查询订单(带分片键查询,能精确路由)
*/
@GetMapping("/list")
public Object listOrders(Long userId) {
return orderService.lambdaQuery()
.eq(Order::getUserId, userId)
.list();
}
/**
* 查询所有订单(不带分片键,会全库全表扫描)
*/
@GetMapping("/all")
public Object listAll() {
return orderService.list();
}
}
五、实战测试:验证分库分表生效
启动项目,调用测试接口,通过控制台日志判断路由是否生效。
5.1 测试接口调用
# 新增订单接口1:userId=1001(1001 % 2 = 1,路由至ds1库t_order_1表)
GET http://localhost:8080/order/add?userId=1001
# 新增订单接口2:userId=1002(1002 % 2 = 0,路由至ds0库t_order_0表)
GET http://localhost:8080/order/add?userId=1002
# 新增订单接口3:userId=1003(1003 % 2 = 1,路由至ds1库t_order_1表)
GET http://localhost:8080/order/add?userId=1003
# 新增订单接口4:userId=1004(1004 % 2 = 0,路由至ds0库t_order_0表)
GET http://localhost:8080/order/add?userId=1004
5.2 查看控制台日志
开启SQL日志后,控制台会清晰打印逻辑SQL和实际执行的物理SQL,能直观看到路由结果:
Logic SQL: INSERT INTO t_order ( order_no, user_id, goods_name, order_amount, create_time ) VALUES ( ?, ?, ?, ?, ? )
Actual SQL: ds1 ::: INSERT INTO t_order_1 (id, order_no, user_id, goods_name, order_amount, create_time) VALUES (?, ?, ?, ?, ?, ?)
Actual SQL: ds0 ::: INSERT INTO t_order_0 (id, order_no, user_id, goods_name, order_amount, create_time) VALUES (?, ?, ?, ?, ?, ?)
5.3 数据库数据校验
连接数据库查看,数据会按照user_id取模规则,精准分散到对应的物理库表中,不会集中在单表,分库分表配置生效。
order_db_0.t_order_0表:存放userId为偶数的订单(1002、1004...)
order_db_0.t_order_1表:暂无数据(userId偶数且取模2=0,不会进这里)
order_db_1.t_order_1表:存放userId为奇数的订单(1001、1003...)
order_db_1.t_order_0表:暂无数据(userId奇数且取模2=1,不会进这里)
5.4 不带分片键查询测试
调用查询所有订单接口:
GET http://localhost:8080/order/all
控制台会显示该查询路由到所有分片表执行,即执行了4次查询(ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1),然后将结果合并返回。
六、分片策略详解
6.1 常用分片算法
ShardingSphere提供了多种分片算法:
| 算法类型 | 说明 | 适用场景 |
|---|---|---|
| INLINE | 基于Groovy表达式,如 ds$->{user_id % 2} | 简单的取模、条件判断 |
| MOD | 取模分片,如 user_id % 4 | 数据分布均匀的场景 |
| HASH_MOD | 哈希取模,先计算哈希值再取模 | 分片键不是数字类型时 |
| RANGE_MOD | 范围+取模结合 | 需要范围查询的场景 |
| INTERVAL | 按时间间隔分片,如按月分表 | 日志、订单等时间序列表 |
| COMPLEX | 复合分片,支持多个分片键 | 复杂业务规则 |
6.2 常见分片键选择原则
分片键的选择直接影响分片效果和查询性能:
-
分布均匀:选择分布均匀的字段,如用户ID、订单ID,避免用枚举、状态字段防止数据倾斜
-
频繁查询:尽量用经常作为查询条件的字段做分片键
-
不变性:分片键的值一旦确定不应修改,否则数据迁移成本高
-
函数处理:查询时分片字段不要使用函数处理,否则分片算法失效导致全节点查询
七、分布式ID生成方案
分库分表后,数据库自增主键无法保证全局唯一,必须使用分布式ID。ShardingSphere内置了多种分布式ID生成器:
7.1 雪花算法(Snowflake)
spring:
shardingsphere:
rules:
sharding:
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1 # 机器ID,集群环境下每个节点不同
雪花算法生成的ID是一个64位的整数,包括时间戳、数据中心ID、机器ID和序列号,能够保证全局唯一且有序递增。
7.2 UUID
key-generators:
uuid:
type: UUID
UUID能保证全局唯一,但不是有序递增,会影响索引性能,不建议作为主键。
八、实战避坑总结
8.1 物理表必须统一
所有分库分表字段、类型、索引必须完全一致,不允许差异化,否则会导致查询写入报错。
8.2 分片键不要使用函数
查询时分片字段不要使用函数处理,否则分片算法失效导致全节点查询:
-- 错误示例:对分片键使用函数,导致全表扫描
SELECT * FROM t_order WHERE DATE_FORMAT(create_time, '%Y%m%d') = '20231001';
-- 正确做法:直接使用分片键
SELECT * FROM t_order WHERE create_time BETWEEN '2023-10-01 00:00:00' AND '2023-10-01 23:59:59';
8.3 主键不能用数据库自增
必须使用分布式ID(雪花算法、UUID等),禁用数据库自增,避免分表主键冲突。
8.4 避免跨库关联查询
分库分表后,跨库的关联查询性能较差,建议在应用层做数据处理,或使用字段冗余设计。
8.5 事务问题
跨库事务需要分布式事务支持,ShardingSphere集成了Seata、XA等分布式事务方案,但会带来性能损耗,尽量设计业务避免跨库事务。
8.6 dynamic-datasource不兼容
如果项目中同时使用了dynamic-datasource-spring-boot-starter动态数据源框架,需要排除,两者不兼容。
九、性能优化进阶建议
除分库分表外,还需配合以下优化措施:
-
建立合理的索引结构:根据查询模式创建索引
-
优化SQL语句编写:避免SELECT *,只查询必要字段
-
引入多级缓存机制:Redis缓存热点数据
-
实施读写分离方案:主库写、从库读,分散压力
-
分页查询优化:带分片键的分页性能更好,避免深分页
十、总结
SpringBoot3集成ShardingSphere-JDBC 5.5.2实现分库分表,核心就是找准正确依赖、写清分片规则,全程实操下来没有复杂原理,对业务代码零侵入,上手难度极低。
单表数据量过大导致的性能问题,通过水平分库分表能快速缓解,适配大部分中小业务场景。后续如果业务扩容,还可以基于这套规范,扩展更多分库分表、读写分离等功能,适配更大的数据量需求。
ShardingSphere作为Java世界中最成熟、最活跃的分库分表中间件,适合绝大多数业务场景,尤其适合Spring Boot用户接入。希望通过本文的实战指南,能帮助你在项目中顺利应用分库分表,彻底告别单表瓶颈。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/peraglobal/article/details/159042733



