ShardingSphere-JDBC基础使用
一、ShardingSphere-JDBC 简介
1.1 什么是 ShardingSphere-JDBC?
ShardingSphere-JDBC 是 Apache ShardingSphere 的第二款核心产品,定位为 轻量级 Java 框架。
- 形态:它以 Jar 包形式存在,作为第三方依赖引入项目。
- 原理:它基于 JDBC 协议进行扩展,对 JDBC 驱动进行增强。在 Java 的 JDBC 层提供额外服务,以插件化的形式存在。
- 核心能力:它将数据库视为一个逻辑整体,应用层只需面向逻辑库和逻辑表进行开发。ShardingSphere-JDBC 负责在底层完成 SQL 解析、路由、改写、执行和结果归并,从而透明地实现 分库分表、读写分离、数据加密等功能。
- 特点:无中心化(无需部署额外的中间件服务器),性能损耗极低(仅增加一次 SQL 解析和路由开销),易于集成。
对比 ShardingSphere-Proxy:
- JDBC:嵌入应用内部,适合 Java 语言栈,性能更高,运维成本低(无独立进程)。
- Proxy:独立的数据库代理服务(类似 MySQL 服务器),支持多语言(Python, Go, PHP 等),适合异构语言环境或 DBA 统一管理。
1.2 产品对比
| 产品 | 定位 | 适用场景 | 特点 |
|---|---|---|---|
| ShardingSphere-JDBC | 轻量级 Java 框架 | Java 应用 | jar 包形式,直连数据库,性能高 |
| ShardingSphere-Proxy | 透明数据库代理 | 异构语言、DBA 友好 | 独立部署,支持 MySQL 协议 |
| ShardingSphere-Sidecar | 云原生数据库代理 | Kubernetes 环境 | DaemonSet 形式,Database Mesh |
二、ShardingSphere-JDBC 核心功能
2.1 数据分片
数据分片是 ShardingSphere-JDBC 最核心的功能,包括:
垂直分片:
- 垂直分库:按照业务模块将表分布在不同的数据库中
- 垂直分表:将一张表的字段拆分到多张表(如将大字段拆分)
水平分片:
- 水平分库:将同一张表的数据分散到多个数据库
- 水平分表:将同一张表的数据分散到多张表
2.2 读写分离
- 自动路由:根据 SQL 语义自动将写操作路由到主库,读操作路由到从库
- 负载均衡:支持轮询、随机、权重等多种从库负载均衡策略
- 强制主库路由:支持通过 Hint 强制将读操作路由到主库,保证数据一致性
2.3 分布式事务
- XA 事务:基于两阶段提交的强一致性事务
- BASE 事务:基于 Seata 的柔性事务,最终一致性
- 本地事务:默认使用,适用于单一分片
2.4 数据脱敏
- 动态脱敏:对查询结果中的敏感数据进行加密/解密
- 静态脱敏:对存储的数据进行加密
2.5 数据库治理
- 配置动态化:支持配置的动态更新,无需重启应用
- 链路追踪:集成 OpenTracing,监控 SQL 执行链路
- 弹性伸缩:支持数据节点的动态扩缩容
三、核心概念
3.1 分片相关概念
| 概念 | 说明 |
|---|---|
| 逻辑表 | 水平拆分的数据库表的相同逻辑和数据结构的总称,例如订单表拆分为 t_order_0、t_order_1,逻辑表名为 t_order |
| 真实表 | 实际存在于数据库中的物理表,如 t_order_0、t_order_1 |
| 数据节点 | 数据分片的最小单元,由数据源名称和数据表组成,如 ds0.t_order_0 |
| 分片键 | 用于分片的数据库字段,如 order_id、user_id |
| 分片算法 | 根据分片键的值计算数据应该存储在哪个分片上的算法 |
3.2 分库分表概念详解
什么是分库分表?
分库分表是将大型数据库中的数据按照一定的规则拆分到多个数据库和多个表中的过程。
- 分库:将数据分散到不同的数据库实例
- 分表:将数据分散到同一个数据库中的不同表
为什么要分库分表?
- 性能瓶颈:单表数据量过大时,索引变大,查询性能下降
- 数据库连接数限制:单库连接数有限,无法支撑高并发
- IO 瓶颈:单库磁盘 IO 有限,无法支撑大量读写
- 备份恢复时间:大表备份恢复时间长
什么时候需要分库分表?
| 场景 | 指标 | 建议 |
|---|---|---|
| 单表数据量 | > 500万行 或 > 2GB | 考虑分表 |
| 单库连接数 | > 80% 上限 | 考虑分库 |
| TPS/QPS | > 1000 | 考虑分库分表 |
| 磁盘 IO | > 70% | 考虑分库 |
| 备份时间 | > 4小时 | 考虑分表 |
3.3 表关系类型
| 类型 | 说明 | 示例 | 注意事项 |
|---|---|---|---|
| 绑定表 | 分片规则一致的主表和子表 | t_order 和 t_order_item 都按照 order_id 分片 | 避免关联查询产生笛卡尔积 |
| 广播表 | 所有数据源中都存在的表,表结构和数据完全一致 | 字典表、配置表 | 不正确配置可能导致项目无法启动 |
| 单表 | 所有数据源中只存在唯一一张的表 | 数据量小、无需分片的表 | - |
四、ShardingSphere-JDBC 优缺点
4.1 优点
| 优点 | 说明 |
|---|---|
| 性能高 | 直连数据库,无网络开销 |
| 兼容性好 | 兼容所有 JDBC 规范和 ORM 框架 |
| 功能丰富 | 分库分表、读写分离、分布式事务一站式解决 |
| 配置灵活 | 支持 Java API、YAML、Spring Boot Starter 多种配置方式 |
| 社区活跃 | Apache 顶级项目,文档完善 |
| 扩展性强 | 支持自定义分片算法 |
4.2 缺点
| 缺点 | 说明 |
|---|---|
| 代码侵入性 | 需要在应用层引入依赖 |
| 不支持跨库关联查询 | 多分片关联查询需应用层处理 |
| 分布式事务性能 | XA 事务性能开销大 |
| SQL 支持限制 | 部分复杂 SQL 不支持 |
| 运维复杂度 | 需要管理分片规则 |
环境准备与基础配置
五、数据库初始化脚本
5.1 创建数据库
-- 创建分库
CREATE DATABASE IF NOT EXISTS ds0 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS ds1 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS ds2 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
5.2 创建订单表(分片表示例)
在 ds0 和 ds1 数据库中分别执行:
-- 订单表 - 分3张表
CREATE TABLE `t_order_0` (
`order_id` bigint(20) NOT NULL COMMENT '订单ID(分布式ID)',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`order_name` varchar(100) NOT NULL COMMENT '订单名称',
`amount` decimal(10,2) DEFAULT '0.00' COMMENT '订单金额',
`order_status` tinyint(4) DEFAULT '0' COMMENT '订单状态 0-待支付 1-已支付 2-已取消',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表0';
CREATE TABLE `t_order_1` (
`order_id` bigint(20) NOT NULL,
`user_id` int(11) NOT NULL,
`order_name` varchar(100) NOT NULL,
`amount` decimal(10,2) DEFAULT '0.00',
`order_status` tinyint(4) DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表1';
CREATE TABLE `t_order_2` (
`order_id` bigint(20) NOT NULL,
`user_id` int(11) NOT NULL,
`order_name` varchar(100) NOT NULL,
`amount` decimal(10,2) DEFAULT '0.00',
`order_status` tinyint(4) DEFAULT '0',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表2';
5.3 创建订单项表(绑定表示例)
在 ds0 和 ds1 数据库中分别执行:
-- 订单项表 - 分3张表
CREATE TABLE `t_order_item_0` (
`item_id` bigint(20) NOT NULL COMMENT '订单项ID',
`order_id` bigint(20) NOT NULL COMMENT '订单ID',
`user_id` int(11) NOT NULL COMMENT '用户ID',
`product_name` varchar(100) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '商品价格',
`quantity` int(11) DEFAULT '1' COMMENT '商品数量',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`item_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表0';
CREATE TABLE `t_order_item_1` (
`item_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
`user_id` int(11) NOT NULL,
`product_name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`quantity` int(11) DEFAULT '1',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`item_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表1';
CREATE TABLE `t_order_item_2` (
`item_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
`user_id` int(11) NOT NULL,
`product_name` varchar(100) NOT NULL,
`price` decimal(10,2) NOT NULL,
`quantity` int(11) DEFAULT '1',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`item_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表2';
5.4 创建广播表
在 ds0 和 ds1 数据库中分别执行:
-- 字典表(广播表)
CREATE TABLE `t_dict` (
`dict_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '字典ID',
`dict_code` varchar(50) NOT NULL COMMENT '字典编码',
`dict_name` varchar(100) NOT NULL COMMENT '字典名称',
`dict_value` varchar(200) NOT NULL COMMENT '字典值',
`sort_order` int(11) DEFAULT '0' COMMENT '排序',
`status` tinyint(4) DEFAULT '1' COMMENT '状态 1-启用 0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`dict_id`),
UNIQUE KEY `uk_dict_code` (`dict_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';
-- 配置表(广播表)
CREATE TABLE `t_config` (
`config_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '配置ID',
`config_key` varchar(100) NOT NULL COMMENT '配置键',
`config_value` varchar(500) NOT NULL COMMENT '配置值',
`remark` varchar(200) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`config_id`),
UNIQUE KEY `uk_config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配置表';
5.5 插入测试数据
-- 插入字典数据(在所有库执行)
INSERT INTO t_dict (dict_code, dict_name, dict_value, sort_order) VALUES
('order_status', '待支付', '0', 1),
('order_status', '已支付', '1', 2),
('order_status', '已取消', '2', 3),
('product_type', '电子产品', 'electronic', 1),
('product_type', '服装', 'clothing', 2),
('product_type', '食品', 'food', 3);
-- 插入配置数据(在所有库执行)
INSERT INTO t_config (config_key, config_value, remark) VALUES
('order_timeout', '30', '订单超时时间(分钟)'),
('max_order_amount', '10000', '单笔订单最大金额'),
('support_payment', 'alipay,wechat', '支持的支付方式');
六、Maven 依赖配置
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>sharding-jdbc-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
</parent>
<properties>
<java.version>1.8</java.version>
<shardingsphere.version>5.3.2</shardingsphere.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<druid.version>1.2.15</druid.version>
</properties>
<dependencies>
<!-- Spring Boot 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>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</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>
</project>
七、基础实体类与Mapper
package com.example.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("t_order")
public class Order {
@TableId
private Long orderId;
private Integer userId;
private String orderName;
private BigDecimal amount;
private Integer orderStatus;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@Data
@TableName("t_order_item")
public class OrderItem {
@TableId
private Long itemId;
private Long orderId;
private Integer userId;
private String productName;
private BigDecimal price;
private Integer quantity;
private LocalDateTime createTime;
}
@Data
@TableName("t_dict")
public class Dict {
@TableId
private Integer dictId;
private String dictCode;
private String dictName;
private String dictValue;
private Integer sortOrder;
private Integer status;
private LocalDateTime createTime;
}
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.Order;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}
@Mapper
public interface DictMapper extends BaseMapper<Dict> {
}
分片策略配置与示例
八、标准分片策略(Precise + Range)
8.1 自定义精确分片算法
package com.example.algorithm.standard;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 自定义精确分片算法 - 数据库分片
* 根据 user_id 取模选择数据库
*/
@Component
public class DatabasePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Integer> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Integer> shardingValue) {
// availableTargetNames: [ds0, ds1]
// shardingValue.getValue(): user_id的值
Integer userId = shardingValue.getValue();
String logicTableName = shardingValue.getLogicTableName();
// 根据user_id取模选择数据库
int index = userId % availableTargetNames.size();
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(index))) {
System.out.println("数据库分片 - 逻辑表: " + logicTableName +
", user_id: " + userId +
", 选择: " + targetName);
return targetName;
}
}
throw new UnsupportedOperationException("无法找到合适的数据源");
}
}
package com.example.algorithm.standard;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 自定义精确分片算法 - 表分片
* 根据 order_id 取模选择表
*/
@Component
public class TablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
// availableTargetNames: [t_order_0, t_order_1, t_order_2]
// shardingValue.getValue(): order_id的值
Long orderId = shardingValue.getValue();
String logicTableName = shardingValue.getLogicTableName();
// 根据order_id取模选择表
int index = (int) (orderId % availableTargetNames.size());
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(index))) {
System.out.println("表分片 - 逻辑表: " + logicTableName +
", order_id: " + orderId +
", 选择: " + targetName);
return targetName;
}
}
throw new UnsupportedOperationException("无法找到合适的表");
}
}
package com.example.algorithm.standard;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 自定义范围分片算法 - 表范围查询
* 处理 BETWEEN AND、>、< 等范围查询
*/
@Component
public class TableRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
// availableTargetNames: [t_order_0, t_order_1, t_order_2]
// shardingValue.getValueRange(): 范围值
Set<String> result = new LinkedHashSet<>();
// 获取范围查询的上下界
Long lower = shardingValue.getValueRange().hasLowerBound()
? shardingValue.getValueRange().lowerEndpoint() : null;
Long upper = shardingValue.getValueRange().hasUpperBound()
? shardingValue.getValueRange().upperEndpoint() : null;
System.out.println("范围查询 - 表: " + shardingValue.getLogicTableName() +
", 范围: [" + lower + " - " + upper + "]");
// 简化处理:返回所有可能的分片
// 实际业务中可以根据范围计算需要查询哪些分片
if (lower != null && upper != null) {
// 计算范围覆盖的分片索引
int minIndex = (int) (lower / 1000) % availableTargetNames.size();
int maxIndex = (int) (upper / 1000) % availableTargetNames.size();
for (String targetName : availableTargetNames) {
for (int i = minIndex; i <= maxIndex; i++) {
if (targetName.endsWith(String.valueOf(i))) {
result.add(targetName);
break;
}
}
}
} else {
// 没有明确范围,返回所有分片
result.addAll(availableTargetNames);
}
return result;
}
}
8.2 application.yml 配置(标准分片策略)
spring:
# ShardingSphere配置
shardingsphere:
# 是否启用ShardingSphere
enabled: true
# 数据源配置
datasource:
# 数据源名称列表,多个用逗号分隔
names: ds0, ds1
# ds0数据源配置
ds0:
# 使用Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
# MySQL驱动
driver-class-name: com.mysql.cj.jdbc.Driver
# 数据库连接URL
jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
# 用户名
username: root
# 密码
password: 123456
# Druid连接池配置
druid:
# 初始连接数
initial-size: 5
# 最小空闲连接数
min-idle: 5
# 最大活跃连接数
max-active: 20
# 获取连接等待超时时间
max-wait: 60000
# 检测连接是否有效的SQL
validation-query: SELECT 1
# 空闲连接检测间隔
time-between-eviction-runs-millis: 60000
# 连接最小生存时间
min-evictable-idle-time-millis: 300000
# 是否缓存PreparedStatement
pool-prepared-statements: true
# 缓存PreparedStatement的最大数量
max-pool-prepared-statement-per-connection-size: 20
# ds1数据源配置(与ds0相同结构)
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
username: root
password: 123456
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
validation-query: SELECT 1
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 分片规则配置
rules:
# 分片规则
sharding:
# 表配置
tables:
# 订单表(逻辑表名)
t_order:
# 实际数据节点:ds0和ds1中的t_order_0、t_order_1、t_order_2
actual-data-nodes: ds$->{0..1}.t_order_$->{0..2}
# 分库策略
database-strategy:
# 标准分片策略
standard:
# 分片键
sharding-column: user_id
# 分片算法名称(引用下方定义的算法)
sharding-algorithm-name: database-precise
# 分表策略
table-strategy:
standard:
# 分片键
sharding-column: order_id
# 精确分片算法名称
sharding-algorithm-name: table-precise
# 范围分片算法名称(可选)
range-sharding-algorithm-name: table-range
# 主键生成策略
key-generate-strategy:
# 主键列名
column: order_id
# 主键生成器名称
key-generator-name: snowflake
# 订单项表(绑定表)
t_order_item:
# 实际数据节点
actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..2}
# 分库策略(与订单表一致)
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-precise
# 分表策略(与订单表一致)
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: table-precise
range-sharding-algorithm-name: table-range
# 主键生成策略
key-generate-strategy:
column: item_id
key-generator-name: snowflake
# 绑定表配置(关联查询优化)
binding-tables:
- t_order, t_order_item
# 广播表配置(所有数据源都存在)
broadcast-tables:
- t_dict
- t_config
# 分片算法配置
sharding-algorithms:
# 数据库分片算法
database-precise:
# 使用自定义类
type: CLASS_BASED
props:
# 策略类型:standard(标准)、complex(复合)、hint(Hint)
strategy: standard
# 自定义算法实现类
algorithmClassName: com.example.algorithm.standard.DatabasePreciseShardingAlgorithm
# 表精确分片算法
table-precise:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.example.algorithm.standard.TablePreciseShardingAlgorithm
# 表范围分片算法
table-range:
type: CLASS_BASED
props:
strategy: standard
algorithmClassName: com.example.algorithm.standard.TableRangeShardingAlgorithm
# 主键生成器配置
key-generators:
# 雪花算法主键生成器
snowflake:
type: SNOWFLAKE
props:
# 工作节点ID(分布式环境需唯一)
worker-id: 1
# 属性配置
props:
# 是否打印SQL
sql-show: true
# SQL是否简化打印
sql-simple: false
# 工作线程数
executor-size: 20
九、复合分片策略(Complex Keys)
9.1 自定义复合分片算法
package com.example.algorithm.complex;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.complex.ComplexKeysShardingValue;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.*;
/**
* 复合分片算法
* 根据 user_id 和 order_date 共同决定分片
*/
@Component
public class CustomComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Comparable<?>> shardingValue) {
String logicTableName = shardingValue.getLogicTableName();
// 获取分片键的值
Map<String, Collection<Comparable<?>>> columnValues =
shardingValue.getColumnNameAndShardingValuesMap();
Collection<Comparable<?>> userIdValues = columnValues.getOrDefault("user_id",
Collections.emptyList());
Collection<Comparable<?>> orderDateValues = columnValues.getOrDefault("order_date",
Collections.emptyList());
System.out.println("复合分片 - 逻辑表: " + logicTableName);
System.out.println("user_id值: " + userIdValues);
System.out.println("order_date值: " + orderDateValues);
Set<String> result = new LinkedHashSet<>();
// 如果同时提供了user_id和order_date
if (!userIdValues.isEmpty() && !orderDateValues.isEmpty()) {
for (Comparable<?> userId : userIdValues) {
for (Comparable<?> orderDate : orderDateValues) {
// 组合算法:将user_id和月份组合
int month = 1;
if (orderDate instanceof LocalDateTime) {
month = ((LocalDateTime) orderDate).getMonthValue();
}
int combinedHash = Objects.hash(userId, month);
int index = Math.abs(combinedHash) % availableTargetNames.size();
addTargetByIndex(result, availableTargetNames, index);
}
}
}
// 如果只提供了user_id
else if (!userIdValues.isEmpty()) {
for (Comparable<?> userId : userIdValues) {
int index = Math.abs(userId.hashCode()) % availableTargetNames.size();
addTargetByIndex(result, availableTargetNames, index);
}
}
// 如果只提供了order_date
else if (!orderDateValues.isEmpty()) {
for (Comparable<?> orderDate : orderDateValues) {
int month = 1;
if (orderDate instanceof LocalDateTime) {
month = ((LocalDateTime) orderDate).getMonthValue();
}
int index = month % availableTargetNames.size();
addTargetByIndex(result, availableTargetNames, index);
}
}
return result.isEmpty() ? availableTargetNames : result;
}
private void addTargetByIndex(Set<String> result, Collection<String> targets, int index) {
for (String target : targets) {
if (target.endsWith(String.valueOf(index))) {
result.add(target);
break;
}
}
}
@Override
public Properties getProps() {
return new Properties();
}
@Override
public void init(Properties props) {
}
}
9.2 application.yml 配置(复合分片策略)
spring:
shardingsphere:
enabled: true
# 数据源配置(使用Druid)
datasource:
names: ds0, ds1, ds2
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
min-idle: 5
validation-query: SELECT 1
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
min-idle: 5
validation-query: SELECT 1
ds2:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds2?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
min-idle: 5
validation-query: SELECT 1
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..2}.t_order_$->{0..2}
# 复合分片策略
database-strategy:
complex:
# 多个分片键,用逗号分隔
sharding-columns: user_id, order_date
# 复合分片算法名称
sharding-algorithm-name: complex-algorithm
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 分片算法配置
sharding-algorithms:
# 复合分片算法
complex-algorithm:
type: CLASS_BASED
props:
# 策略类型:complex
strategy: complex
# 自定义算法实现类
algorithmClassName: com.example.algorithm.complex.CustomComplexShardingAlgorithm
# 主键生成器
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
props:
sql-show: true
十、Hint 分片策略
10.1 自定义 Hint 分片算法
package com.example.algorithm.hint;
import org.apache.shardingsphere.sharding.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.hint.HintShardingValue;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* Hint分片算法
* 通过HintManager传入分片值,不依赖SQL
*/
@Component
public class CustomHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
HintShardingValue<Integer> shardingValue) {
String logicTableName = shardingValue.getLogicTableName();
Collection<Integer> values = shardingValue.getValues();
System.out.println("Hint分片 - 逻辑表: " + logicTableName);
System.out.println("Hint值: " + values);
Set<String> result = new LinkedHashSet<>();
if (values.isEmpty()) {
// 没有Hint值,根据默认策略返回
return Collections.emptySet();
}
// 根据Hint值选择分片
for (Integer value : values) {
int index = value % availableTargetNames.size();
for (String targetName : availableTargetNames) {
if (targetName.endsWith(String.valueOf(index))) {
result.add(targetName);
break;
}
}
}
return result;
}
@Override
public Properties getProps() {
return new Properties();
}
@Override
public void init(Properties props) {
}
}
10.2 application.yml 配置(Hint分片策略)
spring:
shardingsphere:
enabled: true
datasource:
names: ds0, ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
validation-query: SELECT 1
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
validation-query: SELECT 1
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..2}
# Hint分片策略
database-strategy:
hint:
# Hint分片算法名称
sharding-algorithm-name: hint-algorithm
# 分片算法配置
sharding-algorithms:
hint-algorithm:
type: CLASS_BASED
props:
# 策略类型:hint
strategy: hint
# 自定义算法实现类
algorithmClassName: com.example.algorithm.hint.CustomHintShardingAlgorithm
props:
sql-show: true
十一、Inline 表达式分片
11.1 application.yml 配置(Inline表达式)
spring:
shardingsphere:
enabled: true
datasource:
names: ds0, ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
validation-query: SELECT 1
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
max-active: 20
validation-query: SELECT 1
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..2}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
# 分表策略
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: table-inline
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 分片算法配置
sharding-algorithms:
# Inline分库算法
database-inline:
type: INLINE
props:
# Groovy表达式:根据user_id取模
algorithm-expression: ds${user_id % 2}
# Inline分表算法
table-inline:
type: INLINE
props:
# Groovy表达式:根据order_id取模
algorithm-expression: t_order_${order_id % 3}
# 是否允许范围查询(可能会全路由)
allow-range-query-with-inline-sharding: true
# 主键生成器
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
props:
sql-show: true
Service 层与 SQL 示例
十二、Service 实现类
package com.example.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.entity.Order;
import com.example.entity.OrderItem;
import com.example.mapper.OrderItemMapper;
import com.example.mapper.OrderMapper;
import org.apache.shardingsphere.infra.hint.HintManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private OrderItemMapper orderItemMapper;
/**
* 1. 插入订单 - 自动分片
* SQL: INSERT INTO t_order (order_id, user_id, order_name, amount, order_status, create_time)
* VALUES (?, ?, ?, ?, ?, ?)
*/
@Transactional
public Order createOrder(Integer userId, String orderName, BigDecimal amount) {
Order order = new Order();
order.setUserId(userId);
order.setOrderName(orderName);
order.setAmount(amount);
order.setOrderStatus(0);
order.setCreateTime(LocalDateTime.now());
// order_id由雪花算法自动生成
orderMapper.insert(order);
System.out.println("插入订单成功,order_id: " + order.getOrderId() +
", user_id: " + userId +
", 路由到: ds" + (userId % 2) + ".t_order_" + (order.getOrderId() % 3));
return order;
}
/**
* 2. 创建订单和订单项(绑定表)
* SQL: INSERT INTO t_order_item (item_id, order_id, user_id, product_name, price, quantity, create_time)
* VALUES (?, ?, ?, ?, ?, ?, ?)
*/
@Transactional
public Order createOrderWithItems(Integer userId, String orderName, BigDecimal amount,
List<String> products) {
// 创建订单
Order order = createOrder(userId, orderName, amount);
// 创建订单项
for (String product : products) {
OrderItem item = new OrderItem();
item.setOrderId(order.getOrderId());
item.setUserId(userId);
item.setProductName(product);
item.setPrice(amount.divide(new BigDecimal(products.size()), 2, BigDecimal.ROUND_HALF_UP));
item.setQuantity(1);
item.setCreateTime(LocalDateTime.now());
orderItemMapper.insert(item);
System.out.println("插入订单项成功,item_id: " + item.getItemId() +
", 与订单在同一分片: ds" + (userId % 2) + ".t_order_item_" + (order.getOrderId() % 3));
}
return order;
}
/**
* 3. 根据订单ID精确查询(携带分片键)
* SQL: SELECT * FROM t_order WHERE order_id = ? AND user_id = ?
*/
public Order getOrderById(Long orderId, Integer userId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_id", orderId);
wrapper.eq("user_id", userId); // 提供分片键,精确定位
Order order = orderMapper.selectOne(wrapper);
System.out.println("精确查询 - order_id: " + orderId +
", user_id: " + userId +
", 路由到: ds" + (userId % 2) + ".t_order_" + (orderId % 3));
return order;
}
/**
* 4. 根据用户ID查询订单列表
* SQL: SELECT * FROM t_order WHERE user_id = ? ORDER BY create_time DESC
*/
public List<Order> getOrdersByUserId(Integer userId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.orderByDesc("create_time");
List<Order> orders = orderMapper.selectList(wrapper);
System.out.println("用户查询 - user_id: " + userId +
", 路由到: ds" + (userId % 2) + " 的所有分表");
return orders;
}
/**
* 5. 范围查询(需要范围分片算法支持)
* SQL: SELECT * FROM t_order WHERE order_id BETWEEN ? AND ? AND user_id = ?
*/
public List<Order> getOrdersByRange(Long startOrderId, Long endOrderId, Integer userId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.between("order_id", startOrderId, endOrderId);
wrapper.eq("user_id", userId); // 提供user_id可以精确定位到数据库
List<Order> orders = orderMapper.selectList(wrapper);
System.out.println("范围查询 - order_id: [" + startOrderId + ", " + endOrderId + "]" +
", user_id: " + userId);
return orders;
}
/**
* 6. 复合查询(多条件,无分片键)
* SQL: SELECT * FROM t_order WHERE order_name LIKE ? AND create_time > ?
* 注意:无分片键会全路由,性能较差
*/
public List<Order> searchOrders(String orderName, LocalDateTime startTime) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.like("order_name", orderName);
wrapper.gt("create_time", startTime);
List<Order> orders = orderMapper.selectList(wrapper);
System.out.println("复合查询 - 无分片键,全路由到所有分片");
return orders;
}
/**
* 7. Hint分片示例 - 强制指定分片
*/
public List<Order> queryWithHint(Integer targetDatabase, Integer targetTable) {
try (HintManager hintManager = HintManager.getInstance()) {
// 强制指定数据库分片
if (targetDatabase != null) {
hintManager.addDatabaseShardingValue("t_order", targetDatabase);
}
// 强制指定表分片
if (targetTable != null) {
hintManager.addTableShardingValue("t_order", targetTable);
}
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_status", 1); // 已支付订单
List<Order> orders = orderMapper.selectList(wrapper);
System.out.println("Hint查询 - 强制路由到: ds" + targetDatabase +
".t_order_" + targetTable);
return orders;
}
}
/**
* 8. 强制读主库(保证数据一致性)
* SQL: SELECT * FROM t_order WHERE order_id = ? FOR UPDATE
*/
public Order queryForUpdate(Long orderId, Integer userId) {
try (HintManager hintManager = HintManager.getInstance()) {
// 强制读主库
hintManager.setMasterRouteOnly();
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_id", orderId);
wrapper.eq("user_id", userId);
return orderMapper.selectOne(wrapper);
}
}
/**
* 9. 关联查询(绑定表优化)
* SQL: SELECT o.*, i.product_name FROM t_order o
* JOIN t_order_item i ON o.order_id = i.order_id
* WHERE o.user_id = ?
*/
public List<Order> getOrdersWithItems(Integer userId) {
// MyBatis Plus 关联查询示例
return orderMapper.selectList(new QueryWrapper<Order>()
.eq("user_id", userId));
// 实际关联查询需要自定义Mapper XML
}
/**
* 10. 统计查询
* SQL: SELECT COUNT(*) FROM t_order WHERE user_id = ?
*/
public long countOrdersByUser(Integer userId) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
long count = orderMapper.selectCount(wrapper);
System.out.println("统计查询 - user_id: " + userId +
", count: " + count);
return count;
}
/**
* 11. 更新订单状态
* SQL: UPDATE t_order SET order_status = ? WHERE order_id = ? AND user_id = ?
*/
@Transactional
public int updateOrderStatus(Long orderId, Integer userId, Integer status) {
Order order = new Order();
order.setOrderStatus(status);
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("order_id", orderId);
wrapper.eq("user_id", userId);
int result = orderMapper.update(order, wrapper);
System.out.println("更新订单 - order_id: " + orderId +
", user_id: " + userId +
", 结果: " + result);
return result;
}
/**
* 12. 广播表示例 - 查询字典
*/
public List<Dict> getDictByCode(String dictCode) {
// 广播表在所有库都存在,随机选择一个查询
return dictMapper.selectList(new QueryWrapper<Dict>()
.eq("dict_code", dictCode));
}
}
十三、测试类
package com.example;
import com.example.entity.Order;
import com.example.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
@SpringBootTest
class ShardingJdbcDemoApplicationTests {
@Resource
private OrderService orderService;
@Test
void testInsert() {
// 插入订单 - user_id=1 路由到 ds1
Order order1 = orderService.createOrder(1, "测试订单1", new BigDecimal("100.00"));
// 插入订单 - user_id=2 路由到 ds0
Order order2 = orderService.createOrder(2, "测试订单2", new BigDecimal("200.00"));
// 插入订单 - user_id=3 路由到 ds1
Order order3 = orderService.createOrder(3, "测试订单3", new BigDecimal("300.00"));
}
@Test
void testInsertWithItems() {
List<String> products = Arrays.asList("商品A", "商品B", "商品C");
Order order = orderService.createOrderWithItems(
1, "批量订单", new BigDecimal("300.00"), products);
}
@Test
void testQuery() {
// 精确查询
Order order = orderService.getOrderById(1678901234567L, 1);
System.out.println("查询结果: " + order);
// 用户查询
List<Order> userOrders = orderService.getOrdersByUserId(1);
System.out.println("用户订单数: " + userOrders.size());
// 范围查询
List<Order> rangeOrders = orderService.getOrdersByRange(
1678900000000L, 1678999999999L, 1);
// 统计查询
long count = orderService.countOrdersByUser(1);
}
@Test
void testHint() {
// 强制路由到 ds1.t_order_1
List<Order> orders = orderService.queryWithHint(1, 1);
}
@Test
void testUpdate() {
int result = orderService.updateOrderStatus(1678901234567L, 1, 1);
}
}
十四、分片算法对比总结
| 算法类型 | 配置方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| INLINE 表达式 | type: INLINE | 简单取模分片 | 配置简单,性能高 | 不支持复杂逻辑 |
| 标准分片算法 | type: CLASS_BASED + strategy: standard | 单分片键,需要范围查询 | 灵活,支持范围查询 | 需要自定义实现 |
| 复合分片算法 | type: CLASS_BASED + strategy: complex | 多分片键共同决定 | 支持复杂业务规则 | 实现复杂,性能稍低 |
| Hint 分片算法 | type: CLASS_BASED + strategy: hint | 分片键不在SQL中 | 动态指定分片 | 代码侵入性强 |
十五、分片键选择原则
- 查询频率高:选择最常用的查询条件字段
- 分布均匀:避免数据倾斜
- 不会频繁变更:分片键变更需要迁移数据
- 具有业务意义:如用户ID、订单ID
十六、性能优化建议
| 优化项 | 说明 |
|---|---|
| 携带分片键 | 查询时尽量携带分片键,避免全路由 |
| 配置绑定表 | 关联查询的表配置为绑定表,避免笛卡尔积 |
| 范围查询谨慎 | 范围查询可能导致全表扫描,尽量带上分片键 |
| 分片数量规划 | 选择2的幂次方便于扩容 |
| 监控数据分布 | 定期检查分片数据是否均匀 |
| 索引优化 | 在每个真实表上创建相同索引 |
十七、注意事项
- 分布式事务性能:XA事务性能开销大,建议使用最终一致性
- SQL支持限制:部分复杂SQL不支持(如存储过程、多表更新)
- 主键策略:推荐使用雪花算法,避免分布式ID重复
- 扩容考虑:提前规划分片数量,后续扩容成本高
总结
ShardingSphere-JDBC 作为一个轻量级的 Java 框架,为应用提供了强大的分库分表和读写分离能力。通过合理的分片策略配置,可以解决单库单表性能瓶颈,支撑高并发、大数据量的业务场景。
核心价值:
- 性能提升:分散数据库压力,提升查询性能
- 容量扩展:突破单库容量限制,支持海量数据
- 高可用:结合读写分离,提高系统可用性
- 透明化:对应用层透明,简化开发复杂度
适用场景:
- 数据量快速增长,单表超过500万行
- 并发量高,单库无法支撑
- 需要读写分离,降低主库压力
- 业务需要水平扩展,降低运维成本