1. 分库分表核心认知
很多新手最大误区:上来就做分片。分片属于架构重构,开发、运维、排查成本成倍提升,非必要绝不使用。
我直接记结论:单表数据超500W、QPS过高、索引失效、大表DDL卡死,就必须考虑分片。
普通优化优先级(我自己的排序):
索引优化 → 读写分离 → 冷热归档 → 分库分表
分片是最后手段,不是首选!
读写分离只能分摊读压力,解决不了单表存储上限,这是分片存在的唯一核心意义。
2. Sharding-JDBC 核心原理
2.1 是什么?(通俗类比)
Sharding-JDBC 就是一个内嵌在项目里的数据库路由代理。
类比:它像一个“数据库调度员”,我们代码只写一张表的SQL,它自动帮我们:
-
识别分片字段
-
算出数据该进哪个库、哪张表
-
自动改写SQL、执行、合并结果
全程无感知、不用改业务SQL。
2.2 整体执行链路(极简版)
接收SQL → 解析分片键 → 路由目标库表 → 改写SQL → 多分片执行 → 结果聚合返回
2.3 为什么选它不选MyCat?(个人总结)
-
Sharding-JDBC:客户端嵌入,无中间件转发、性能高、运维简单
-
MyCat:独立服务代理,多一层网络损耗,新项目基本淘汰
3. 核心概念详解
所有术语第一次出现全部通俗注释,零基础直接看懂。
3.1 分片键(Sharding Column)✅核心中的核心
通俗解释:用来决定数据“去哪张表、哪个库”的字段,就是分片的导航键。
常用分片键:order_id、user_id
我的笔记重点:
-
分片键一旦确定不能改,改了数据全部错乱
-
业务绝大多数查询必须带分片键,否则全表广播、性能雪崩
3.2 分片算法
3.2.1 哈希取模算法(生产首选)
原理:分片键 % 分片数 = 目标位置
优点:数据均匀、无热点
缺点:只能2/4/8倍数扩容,否则全量迁移数据
适用:订单、交易核心业务
3.2.2 范围算法
原理:按时间、ID区间分片
优点:扩容不用迁移历史数据
缺点:最新分片永远热点,流量全部打在最后一张表
适用:日志、流水、非核心数据
3.3 广播表
通俗解释:所有分片库都存一份一模一样的数据,全局统一。
场景:字典表、配置表、地区表
作用:避免查字典数据跨库查询,提升性能
3.4 绑定表
通俗解释:分片键一致的关联表,绑定后数据一定落在同一个分片。
场景:订单表 & 订单详情表
作用:彻底解决跨分片不能JOIN的问题
3.5 分库 & 分表 区别(极简记忆)
-
分表:同一个库,多张表 → 解决单表数据量大
-
分库:多个库,分摊连接数、CPU、IO → 解决单库并发瓶颈
3.6 范围分片算法(RangeShardingAlgorithm)完整补充
通俗解释:专门用于时间、ID区间分片的算法,和精准算法成对配套使用,面试必问、时间分片必备。
适用场景:按天/按月分表、日志数据、监控数据、时序流水数据。
核心特点:支持区间查询自动路由对应分片,无需全表扫描。
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.Collections;
/**
* 自定义范围分片算法
* 适配时间、ID区间范围查询
*/
public class CustomRangeAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
// 简单适配:范围查询直接匹配所有分片(可自定义区间精准匹配)
return Collections.singletonList(availableTargetNames.iterator().next());
}
}
3.7 分片全局ID解决方案(补齐自增ID失效核心问题)
新手致命坑:分库分表后,MySQL原生自增ID完全作废!多库同时自增会出现主键重复,直接报错。
通俗原理:每个数据库独立维护自增计数器,多库写入必然ID冲突。
生产唯一解决方案:全局分布式ID
我整理的三种主流方案(初学者直接背):
-
雪花算法(Snowflake)【首选】:高性能、有序、无中心化、适配所有交易业务。
-
UidGenerator:百度优化版雪花算法,彻底解决时钟回拨,大厂标配。
-
Redis自增ID:简单易用,低并发场景可用,高并发存在单点瓶颈。
雪花算法核心隐患:时钟回拨(面试高频)
问题:服务器时间同步、校准时间会导致时间回退,生成重复ID,引发分片主键冲突。
极简解决方案:代码缓存上一次时间戳,检测到回拨则阻塞等待时间追平,禁止生成重复ID。
4. 核心实战代码
本节覆盖面试高频核心类:ShardingDataSource、ShardingRuleConfiguration、ShardingDataSourceFactory、PreciseShardingAlgorithm
所有代码我亲自精简,无冗余、可直接复制运行。
4.1 数据库建表语句(生产完整可执行)
适配本文 ds0、ds1 双库 + 每库2表 分片结构,包含订单表、订单详情表、广播字典表,可直接在本地/测试库执行。
4.1.1 新建分片数据库
-- 创建分片库0、分片库1
CREATE DATABASE IF NOT EXISTS sharding_db0 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS sharding_db1 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
4.1.2 业务分片表(订单+订单详情)
-- 【sharding_db0、sharding_db1 均需执行】
-- 订单表:t_order_0 / t_order_1
USE sharding_db0;
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT NOT NULL COMMENT '分片键-订单ID(雪花ID)',
user_id BIGINT NOT NULL COMMENT '用户ID',
order_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (order_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分片表0';
CREATE TABLE IF NOT EXISTS t_order_1 (
order_id BIGINT NOT NULL COMMENT '分片键-订单ID(雪花ID)',
user_id BIGINT NOT NULL COMMENT '用户ID',
order_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (order_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分片表1';
USE sharding_db1;
CREATE TABLE IF NOT EXISTS t_order_0 (
order_id BIGINT NOT NULL COMMENT '分片键-订单ID(雪花ID)',
user_id BIGINT NOT NULL COMMENT '用户ID',
order_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (order_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分片表0';
CREATE TABLE IF NOT EXISTS t_order_1 (
order_id BIGINT NOT NULL COMMENT '分片键-订单ID(雪花ID)',
user_id BIGINT NOT NULL COMMENT '用户ID',
order_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单金额',
order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (order_id),
KEY idx_user_id (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单分片表1';
-- 订单详情绑定表(与订单表同分片规则)
USE sharding_db0;
CREATE TABLE IF NOT EXISTS t_order_item_0 (
item_id BIGINT NOT NULL COMMENT '详情ID',
order_id BIGINT NOT NULL COMMENT '分片键-关联订单ID',
goods_id BIGINT NOT NULL COMMENT '商品ID',
goods_num INT NOT NULL DEFAULT 1 COMMENT '购买数量',
goods_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (item_id),
KEY idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情分片表0';
CREATE TABLE IF NOT EXISTS t_order_item_1 (
item_id BIGINT NOT NULL COMMENT '详情ID',
order_id BIGINT NOT NULL COMMENT '分片键-关联订单ID',
goods_id BIGINT NOT NULL COMMENT '商品ID',
goods_num INT NOT NULL DEFAULT 1 COMMENT '购买数量',
goods_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (item_id),
KEY idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情分片表1';
USE sharding_db1;
CREATE TABLE IF NOT EXISTS t_order_item_0 (
item_id BIGINT NOT NULL COMMENT '详情ID',
order_id BIGINT NOT NULL COMMENT '分片键-关联订单ID',
goods_id BIGINT NOT NULL COMMENT '商品ID',
goods_num INT NOT NULL DEFAULT 1 COMMENT '购买数量',
goods_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (item_id),
KEY idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情分片表0';
CREATE TABLE IF NOT EXISTS t_order_item_1 (
item_id BIGINT NOT NULL COMMENT '详情ID',
order_id BIGINT NOT NULL COMMENT '分片键-关联订单ID',
goods_id BIGINT NOT NULL COMMENT '商品ID',
goods_num INT NOT NULL DEFAULT 1 COMMENT '购买数量',
goods_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '商品单价',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (item_id),
KEY idx_order_id (order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单详情分片表1';
4.1.3 全局广播字典表(全库同步)
-- 广播表:两个库均创建,数据保持一致
USE sharding_db0;
CREATE TABLE IF NOT EXISTS t_dict (
dict_id INT NOT NULL AUTO_INCREMENT COMMENT '字典ID',
dict_type VARCHAR(64) NOT NULL COMMENT '字典类型',
dict_label VARCHAR(128) NOT NULL COMMENT '字典名称',
dict_value VARCHAR(128) NOT NULL COMMENT '字典值',
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (dict_id),
UNIQUE KEY uk_type_value (dict_type,dict_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局字典广播表';
USE sharding_db1;
CREATE TABLE IF NOT EXISTS t_dict (
dict_id INT NOT NULL AUTO_INCREMENT COMMENT '字典ID',
dict_type VARCHAR(64) NOT NULL COMMENT '字典类型',
dict_label VARCHAR(128) NOT NULL COMMENT '字典名称',
dict_value VARCHAR(128) NOT NULL COMMENT '字典值',
sort INT NOT NULL DEFAULT 0 COMMENT '排序',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (dict_id),
UNIQUE KEY uk_type_value (dict_type,dict_value)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局字典广播表';
笔记重点:所有表结构完全对齐,保证多分片DDL一致,杜绝生产结构不一致BUG;order_id 统一作为分片键,与前文算法、配置完全匹配。
4.2 必备Maven依赖
4.3 自定义精确分片算法
适用场景:等值查询、IN查询,精准路由单分片
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
/**
* 自定义精确分片算法
* 我个人笔记:处理 =、IN 查询,精准定位单库/单表
*/
public class CustomPreciseAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
// 获取分片键值
Long value = shardingValue.getValue();
// 2分片取模路由
long index = value % 2;
// 返回目标表/库名称
for (String name : availableTargetNames) {
if (name.endsWith(index + "")) {
return name;
}
}
throw new RuntimeException("分片路由失败");
}
}
4.4 代码方式手动配置分片规则
不用yml配置,纯代码实现,看懂底层核心组装逻辑
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shardingsphere.api.config.sharding.ShardingRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.TableRuleConfiguration;
import org.apache.shardingsphere.api.config.sharding.strategy.StandardShardingStrategyConfiguration;
import org.apache.shardingsphere.shardingjdbc.api.ShardingDataSourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* Sharding-JDBC 核心配置类
* 核心类全覆盖:ShardingDataSource、ShardingRuleConfiguration、ShardingDataSourceFactory
*/
@Configuration
public class ShardingConfig {
// 1. 构建多数据源
private Map<String, DataSource> createDataSourceMap() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 数据源0
DruidDataSource ds0 = new DruidDataSource();
ds0.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds0.setUrl("jdbc:mysql://localhost:3306/sharding_db0?useUnicode=true&serverTimezone=Asia/Shanghai");
ds0.setUsername("root");
ds0.setPassword("123456");
dataSourceMap.put("ds0", ds0);
// 数据源1
DruidDataSource ds1 = new DruidDataSource();
ds1.setDriverClassName("com.mysql.cj.jdbc.Driver");
ds1.setUrl("jdbc:mysql://localhost:3306/sharding_db1?useUnicode=true&serverTimezone=Asia/Shanghai");
ds1.setUsername("root");
ds1.setPassword("123456");
dataSourceMap.put("ds1", ds1);
return dataSourceMap;
}
// 2. 构建分片规则
private ShardingRuleConfiguration createShardingRule() {
ShardingRuleConfiguration rule = new ShardingRuleConfiguration();
// 配置t_order表规则:2库2表
TableRuleConfiguration orderTable = new TableRuleConfiguration("t_order", "ds${0..1}.t_order_${0..1}");
// 标准分片策略:分片键order_id + 自定义精确算法
orderTable.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("order_id", new CustomPreciseAlgorithm())
);
rule.getTableRuleConfigs().add(orderTable);
// 绑定表:订单、订单详情关联绑定,禁止跨库join
rule.getBindingTableGroups().add("t_order,t_order_item");
// 广播表:全局字典表
rule.getBroadcastTables().add("t_dict");
return rule;
}
// 3. 工厂创建最终Sharding数据源(核心入口)
@Bean
public DataSource shardingDataSource() throws Exception {
return ShardingDataSourceFactory.createDataSource(
createDataSourceMap(),
createShardingRule(),
new Properties()
);
}
}
4.5 生产通用YAML配置
前面是代码硬编码配置(学习底层用),工作中全部使用yml配置,零侵入、可直接上线。
spring:
shardingsphere:
# 多数据源配置
datasource:
names: ds0,ds1
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db0?useUnicode=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db1?useUnicode=true&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 分片规则
rules:
sharding:
# 分片表规则
tables:
t_order:
actual-data-nodes: ds->{0..1}.t_order_->{0..1}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-mod
# 分片算法
sharding-algorithms:
order-mod:
type: MOD
props:
sharding-count: 2
# 绑定表、广播表
binding-tables:
- t_order,t_order_item
broadcast-tables:
- t_dict
# 开启SQL日志(调试必备)
props:
sql-show: true
4.6 核心类总结
-
ShardingDataSourceFactory:工厂类,唯一创建分片数据源的入口
-
ShardingRuleConfiguration:承载所有分片规则、绑定表、广播表、算法配置
-
PreciseShardingAlgorithm:精准分片算法,面试必考,处理等值查询
-
StandardShardingStrategyConfiguration:标准策略,绑定「分片键+算法」
4.7 SpringBoot 生产级完整集成方案
适配 SpringBoot2.x 主流生产版本,整合 Druid连接池 + 分片规则 + SQL日志 + 事务适配,解决原生配置连接泄漏、日志缺失、事务失效等生产问题。
4.7.1 生产完整依赖(剔除冗余、规避冲突)
<!-- SpringBoot 分片核心依赖 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
<!-- 排除默认日志,避免冲突 -->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 生产级Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- MyBatis 适配 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
4.7.2 生产YAML终极配置(多分片+连接池优化+调试可控)
适配生产:连接池参数优化、关闭冗余日志、支持动态SQL调试、绑定表/广播表完备、事务兼容
spring:
# 数据源分片配置
shardingsphere:
# 多数据源定义
datasource:
names: ds0,ds1
# 库0配置
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 123456
# 生产连接池核心参数
initial-size: 5
max-active: 20
min-idle: 5
max-wait: 60000
test-on-borrow: false
test-on-return: false
test-while-idle: true
# 库1配置
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 123456
initial-size: 5
max-active: 20
min-idle: 5
max-wait: 60000
test-on-borrow: false
test-on-return: false
test-while-idle: true
# 分片规则
rules:
sharding:
# 自定义分片算法注册
sharding-algorithms:
order-mod:
type: MOD
props:
sharding-count: 2
# 表分片规则
tables:
t_order:
actual-data-nodes: ds->{0..1}.t_order_->{0..1}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-mod
# 绑定表(解决跨分片JOIN)
binding-tables:
- t_order,t_order_item
# 全局广播表(字典/配置)
broadcast-tables:
- t_dict
# 生产环境参数
props:
sql-show: false # 生产关闭SQL打印,测试开启
max.connections.size.per.query: 2 # 限制单查询最大连接,防雪崩
# MyBatis生产配置
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
default-statement-timeout: 3000
4.7.3 分布式ID生产配置(全局唯一,替代自增ID)
内置Sharding-JDBC原生雪花ID,无需额外引入组件,生产零成本使用。
spring:
shardingsphere:
rules:
sharding:
# 全局分布式ID配置
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 0 # 单机0,集群不同节点配置不同ID(0-31)
笔记重点:集群部署必须保证 worker-id 唯一,否则出现ID重复!
4.7.4 生效测试 Controller(可直接自测)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class ShardingTestController {
@Resource
private OrderMapper orderMapper;
// 分片路由测试
@GetMapping("/test/sharding/{orderId}")
public String testSharding(@PathVariable Long orderId){
return "路由分片成功:" + orderMapper.selectById(orderId);
}
}
4.8 SpringCloud 微服务生产适配方案
微服务核心原则:分片只在数据服务层实现,网关、业务服务不感知分片规则
4.8.1 微服务落地规范
-
分片配置、数据源、算法统一下沉至数据中台/基础服务
-
业务微服务只调用Feign接口,零分片配置
-
全局分布式ID统一由数据服务生成,避免多服务ID冲突
-
微服务禁止跨服务JOIN,彻底规避分片关联问题
4.8.2 微服务事务适配(生产刚需)
Sharding-JDBC 本地分片事务无法跨微服务,微服务场景必须搭配 Seata AT 实现最终一致性。
<!-- SpringCloud 分布式事务适配依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
4.8.3 微服务避坑要点
-
❌ 禁止多微服务重复配置分片规则
-
❌ 禁止微服务内直接跨库查询
-
✅ 所有分片CRUD收敛至数据服务
-
✅ 分页、聚合、排序统一在数据层处理,上层只拿结果
4.9 生产上线校验清单
-
✅ 关闭 sql-show,避免日志刷屏、泄露SQL
-
✅ 雪花算法 worker-id 集群唯一
-
✅ 连接池参数适配业务QPS,避免连接耗尽
-
✅ 所有核心查询强制携带分片键
-
✅ 绑定表、广播表配置完整,无遗漏关联表
-
✅ 禁用所有分片黑名单SQL语法
4.10 生产核心补全:雪花ID 时钟回拨终极解决源码
原生Sharding-JDBC雪花算法存在时钟回拨ID重复生产致命BUG,本章节提供可直接上线的优化版雪花算法,彻底杜绝时间回拨导致的主键冲突,适配集群多节点部署。
4.10.1 优化版雪花算法(无时钟回拨、生产可用)
/**
* 优化版雪花算法(彻底解决时钟回拨)
* 生产落地标准:支持集群worker-id配置、时间回拨阻塞、杜绝ID重复
*/
public class SnowflakeIdGenerator {
// 起始时间戳(固定:2025-01-01)
private static final long EPOCH = 1735689600000L;
// 机器ID位数 5位 0-31
private static final long WORKER_ID_BITS = 5L;
// 数据中心ID位数 5位 0-31
private static final long DATA_CENTER_ID_BITS = 5L;
// 序列号位数 12位 每毫秒0-4095
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1;
private static final long MAX_DATA_CENTER_ID = (1L << DATA_CENTER_ID_BITS) - 1;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;
// 机器ID
private final long workerId;
// 数据中心ID
private final long dataCenterId;
// 最后时间戳
private long lastTimestamp = -1L;
// 序列号
private long sequence = 0L;
public SnowflakeIdGenerator(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("workerId 超出范围(0-31)");
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException("dataCenterId 超出范围(0-31)");
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
// 线程安全、同步生成ID,彻底解决时钟回拨
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 核心:时钟回拨检测 & 阻塞等待
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
// 短时间回拨阻塞等待,时间追平再生成
if (offset <= 1000) {
try {
Thread.sleep(offset);
currentTimestamp = System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException("时钟回拨,生成ID失败", e);
}
} else {
throw new RuntimeException("严重时钟回拨,禁止生成ID");
}
}
// 同一毫秒,序列号自增
if (lastTimestamp == currentTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
// 毫秒内序列号耗尽,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
4.10.2 生产Spring容器配置(集群唯一ID)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IdGeneratorConfig {
/**
* 集群部署:不同节点配置不同 workerId、dataCenterId(0-31唯一)
* 单机部署:固定 0 即可
*/
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator() {
return new SnowflakeIdGenerator(0, 0);
}
}
生产核心要点:集群多节点必须保证 workerId+dataCenterId 全局唯一,彻底杜绝ID重复冲突,替代Sharding原生雪花算法。
4.11 生产主流:分片 + 读写分离 混合架构完整配置
企业90%高并发项目采用 分库分表 + 读写分离 组合架构:写请求走主库、读请求分摊从库,既解决单表海量数据瓶颈,又解决读并发压力,填补文档单一分片短板。
4.11.1 架构说明
-
主库:ds0、ds1(负责写入、更新、删除)
-
从库:ds0-slave、ds1-slave(负责查询读流量)
-
路由规则:写入自动走主库,查询自动负载均衡走从库
-
兼容原有分片规则、绑定表、广播表,零业务改造
4.11.2 完整生产YAML配置
spring:
shardingsphere:
# 主从多数据源配置
datasource:
names: ds0,ds0-slave,ds1,ds1-slave
# 主库0
ds0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 从库0
ds0-slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db0_slave?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 主库1
ds1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/sharding_db1?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 从库1
ds1-slave:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/sharding_db1_slave?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# 核心规则:读写分离 + 分片混合
rules:
# 读写分离规则
readwrite-splitting:
data-sources:
# 库0主从组
ds0-group:
write-data-source-name: ds0
read-data-source-names: [ds0-slave]
load-balancer-name: round_robin
# 库1主从组
ds1-group:
write-data-source-name: ds1
read-data-source-names: [ds1-slave]
load-balancer-name: round_robin
# 负载均衡算法
load-balancers:
round_robin:
type: ROUND_ROBIN
# 分片规则(完全兼容原有配置)
sharding:
sharding-algorithms:
order-mod:
type: MOD
props:
sharding-count: 2
tables:
t_order:
actual-data-nodes: ds${0..1}-group.t_order_${0..1}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-mod
binding-tables:
- t_order,t_order_item
broadcast-tables:
- t_dict
props:
sql-show: false
max.connections.size.per.query: 2
落地重点:读写分离与分片可无缝叠加,无需修改业务代码,Sharding-JDBC 自动接管读写路由,是企业生产标准架构。
4.12 生产零停机:分片平滑扩容 + 双写数据迁移完整方案
补充前文仅讲流程、无代码的短板,提供可直接落地的双写迁移代码、存量迁移方案、数据校验逻辑,支持2库扩4库、4库扩8库倍数扩容,全程零停机、零数据丢失。
4.12.1 核心扩容原理
哈希取模分片仅支持2的倍数扩容,扩容核心:新旧规则共存、双写兜底、存量迁移、灰度切流,杜绝停机维护、数据错乱。
4.12.2 双写兜底核心代码(增量数据无遗漏)
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 分片扩容双写工具类(扩容过渡阶段专用)
* 同时写入旧分片、新分片,保证增量数据不丢失
* 扩容完成、数据一致后关闭双写
*/
@Component
public class ShardingDoubleWriteUtil {
@Resource
private OrderMapper oldOrderMapper;
@Resource
private OrderMapper newOrderMapper;
/**
* 新增数据双写
*/
public void insertOrderDoubleWrite(Order order) {
// 写入旧分片库
oldOrderMapper.insert(order);
// 同步写入新分片库
newOrderMapper.insert(order);
}
/**
* 更新数据双更
*/
public void updateOrderDoubleWrite(Order order) {
oldOrderMapper.updateById(order);
newOrderMapper.updateById(order);
}
/**
* 删除数据双删
*/
public void deleteOrderDoubleWrite(Long orderId) {
oldOrderMapper.deleteById(orderId);
newOrderMapper.deleteById(orderId);
}
}
4.12.3 存量数据迁移 + 数据校验方案
/**
* 分片存量数据迁移任务
* 分批查询旧库、写入新库、去重校验,避免全量查询OOM
*/
@Component
public class ShardingDataMigrateTask {
@Resource
private OrderMapper oldOrderMapper;
@Resource
private OrderMapper newOrderMapper;
// 分批每页大小
private static final int PAGE_SIZE = 1000;
public void migrateData() {
long startId = 0;
while (true) {
// 分批分页查询旧库存量数据
List<Order> oldDataList = oldOrderMapper.selectPageByStartId(startId, PAGE_SIZE);
if (oldDataList == null || oldDataList.isEmpty()) {
break;
}
// 批量写入新库
newOrderMapper.batchInsert(oldDataList);
// 更新游标ID
startId = oldDataList.get(oldDataList.size() - 1).getOrderId();
}
// 迁移完成后数据比对校验
checkDataConsistency();
}
/**
* 新旧库数据一致性校验
*/
public void checkDataConsistency() {
// 1. 总条数校验
Long oldCount = oldOrderMapper.selectCount();
Long newCount = newOrderMapper.selectCount();
if (!oldCount.equals(newCount)) {
throw new RuntimeException("数据迁移不一致,存在丢失数据");
}
// 2. 抽样详情校验
// 可扩展字段级比对、差异数据修复
}
}
4.12.4 完整零停机扩容落地步骤
-
环境搭建:部署新分片数据库,配置新旧双分片路由规则
-
开启双写:业务层接入双写代码,所有增量数据同步写入新旧库
-
存量迁移:执行分批迁移任务,同步历史数据,避免OOM
-
数据校验:条数比对、抽样校验,修复差异数据
-
灰度切流:逐步将读、写流量切换至新分片架构
-
下线旧库:观测线上稳定无报错后,关闭双写、下线旧分片库
4.13 生产监控告警 + 线上故障排查手册(生产刚需)
补齐文档最大生产短板:无监控、无排查方案、无故障预案。本章节覆盖核心监控指标、日志排查、常见线上故障、告警规则、修复方案,支撑线上稳定运行。
4.13.1 分片核心监控指标(必须接入监控平台)
-
分片路由异常率:路由失败、分片键为空、路由错乱异常统计
-
分片查询耗时:单分片、全分片广播查询耗时监控
-
数据倾斜指标:各分片数据量、QPS差值监控,提前发现热点
-
连接池使用率:Druid连接池活跃连接、等待连接监控,防连接耗尽
-
分布式ID异常:ID生成失败、时钟回拨异常监控告警
-
事务异常率:跨分片事务回滚、超时、不一致异常监控
4.13.2 线上日志排查规范
-
生产关闭 sql-show,避免日志刷屏,自定义业务日志打印分片键、路由节点
-
异常日志必须打印:分片键、目标库表、原始SQL、异常堆栈
-
全分片广播查询日志单独打标,定时统计优化慢查询
4.13.3 线上高频故障 & 一键修复方案
-
故障1:分片路由错乱、数据找不到 原因:分片键修改、路由规则变更、扩容未迁移全量数据 修复:核对分片算法、校验数据分布、补全迁移差异数据
-
故障2:接口超时、数据库压力暴涨 原因:无分片键查询触发全分片广播 修复:强制业务携带分片键、新增无分片键查询黑名单拦截
-
故障3:主键ID重复报错 原因:雪花算法worker-id重复、时钟回拨 修复:替换优化版雪花算法、集群节点唯一ID配置
-
故障4:跨分片JOIN报错/数据错乱 原因:关联表未配置绑定表、分片规则不一致 修复:补充绑定表配置、统一关联表分片键与算法
-
故障5:分页数据重复、丢失、错乱 原因:多分片原生limit offset分页 修复:替换为内存分页/游标分页方案
-
故障6:连接池耗尽、数据库卡死 原因:单查询连接数过高、慢查询堆积 修复:优化连接池参数、限制单查询最大连接、清理慢SQL
4.13.4 生产告警阈值配置(通用标准)
-
路由异常率 > 0.1%:触发告警,排查路由规则与分片键
-
单分片QPS远超平均分片2倍:判定数据热点,触发热点优化告警
-
查询耗时 > 1s:分片慢查询告警,优化SQL与索引
-
ID生成异常、事务异常:紧急告警,立即排查集群与事务问题
5. 开发坑点与解决方案
全部是我初学踩过的坑,直接记死:
-
❌ 无分片键查询:触发全分片广播,数据量大直接雪崩
-
❌ 跨分片JOIN:非绑定表关联直接报错/数据错乱
-
❌ 随意改分片键值:数据路由错乱,无法自动修复
-
❌ 非倍数扩容:取模分片强行加库,数据全部错位
5.1 分片SQL黑名单(初学者必记)
以下语法分片不支持/高危,开发直接禁止使用:
-
❌
limit n,-1无边界分页,直接数据错乱 -
❌ 无分片键 GROUP BY、COUNT、SUM 全局聚合
-
❌ 跨分片复杂JOIN、嵌套子查询
-
❌ 存储过程、自定义函数、批量跨分片DDL
5.2 分片分页BUG 终极解决方案(可直接运行)
问题回顾:多分片下原生limit offset分页错乱、丢数据、重复数据。
方案一:内存全局分页(中小数据通用)
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 分片通用分页工具类
* 解决多分片分页错乱问题
*/
public class ShardingPageUtil {
// 全局排序+分页
public static <T extends Comparable<T>> List<T> page(List<T> allShardingData, int pageNum, int pageSize) {
// 全局排序
List<T> sorted = allShardingData.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
// 分页截取
int start = (pageNum - 1) * pageSize;
int end = Math.min(start + pageSize, sorted.size());
return sorted.subList(start, end);
}
}
方案二:游标分页(深分页/大数据量最优)
彻底抛弃offset,通过最大ID游标查询,无性能衰减,生产首选。
// 游标分页SQL示例
// 只传上一页最后一条ID,不传偏移量
SELECT * FROM t_order WHERE order_id > #{lastId} ORDER BY order_id LIMIT 10
5.3 数据倾斜 & 热点分片 完整落地解决方案
成因:大用户流量集中、热点商品、单一哈希打散不均、时序分片天然热点。
全套解决思路(面试满分答案):
-
热点隔离:超大用户、热点数据单独路由专属分片,隔离流量
-
复合分片:分片键+随机后缀二次哈希,打散集中数据
-
虚拟分片映射:逻辑分片与物理分片解耦,支持动态重分布
-
冷热分离:热数据哈希分片保均衡,冷数据范围分片方便归档
5.4 平滑扩容 & 数据迁移极简流程(初学者能懂)
哈希分片只能倍数扩容,标准零停机迁移流程:
-
新库新表环境搭建,配置新旧双路由规则
-
开启新旧库双写,保证增量数据无遗漏
-
分批迁移历史存量数据
-
全量数据比对、去重、修复差异
-
灰度切流:10%→50%→100% 切换新分片架构
-
稳定观测后下线旧库
5.5 分布式事务等级完整补全
分片事务分3个等级,按需选用:
-
弱XA(最终一致性):Sharding原生支持、高性能、无锁,普通业务通用,允许极短时间数据不一致
-
强XA(强一致性):两阶段提交、性能差、阻塞风险高,生产极少使用
-
BASE柔性事务:Sharding+Seata AT,高并发高可用,支付、交易核心业务必备
5.6 冷热数据分层方案(工程落地标配)
核心思想:热数据走哈希分片保证均衡读写,冷数据走范围分片归档,减轻主分片压力。
落地逻辑:
-
近3个月热数据:多库多表哈希分片,支撑高并发读写
-
3个月前冷数据:按月范围分片归档,单独存储、不占用核心性能
6. 高频面试题(21道)
本章整理21道分片核心面试真题,无重复、无冗余,覆盖原理、概念、实战坑点、架构落地,答案精简标准,可直接背诵用于面试和复盘。
6.1 基础原理题(Q1-Q5)
Q1:为什么需要分库分表?什么场景下才使用?
答:单表数据量超500W、单库连接数/IO瓶颈、大表DDL阻塞、索引失效查询变慢时需要分片。分片是数据库优化的最后手段,优先使用索引优化、读写分离、冷热归档,非必要不分片。
Q2:分库分表为什么不能用MySQL自增主键?如何解决?
答:多数据库独立维护自增计数器,多库写入会出现主键ID重复。生产统一使用全局分布式ID,主流方案为雪花算法、百度UidGenerator,杜绝ID冲突。
Q3:简述Sharding-JDBC的核心执行流程?
答:整体流程:SQL解析 → 分片键提取与路由计算 → SQL改写 → 多分片并行执行 → 结果聚合、排序、分页 → 返回最终数据,业务层无感知。
Q4:雪花算法时钟回拨问题成因及解决方案?
答:服务器时间同步、时间回退时,会生成重复ID,引发主键冲突。解决方案:代码缓存上一次时间戳,检测到时间回拨则阻塞等待,或使用优化版UidGenerator规避该问题。
Q5:分片架构下为什么不建议无分片键查询?
答:无分片键无法精准路由,会触发全库全表广播查询,遍历所有分片,数据量大时会导致接口超时、数据库压力雪崩,是生产高危操作。
6.2 核心概念题(Q6-Q10)
Q6:哈希分片和范围分片的区别及适用场景?
答:哈希分片按分片键取模路由,数据均匀无热点,适合订单、交易核心业务,仅支持2的倍数扩容;范围分片按时间、ID区间路由,扩容无需迁移历史数据,但末尾分片易产生热点,适合日志、流水等非核心时序数据。
Q7:绑定表、广播表的作用和业务场景?
答:绑定表:分片规则一致的关联表绑定,保证关联数据落在同一分片,解决跨分片无法JOIN问题,适用于订单与订单详情表;广播表:所有分片库同步存储相同数据,适用于字典、配置、地区等小表,避免跨库查询。
Q8:分库和分表的核心区别是什么?
答:分表是单库拆分为多张表,解决单表数据量大、查询慢的问题;分库是拆分多个数据库,分摊单库CPU、IO、连接数压力,解决高并发瓶颈,生产大多采用分库+分表组合方案。
Q9:Sharding-JDBC和MyCat的选型区别?
答:Sharding-JDBC是客户端嵌入式框架,无中间件转发、性能高、部署简单、无单点故障,适配Spring生态,是新项目首选;MyCat是独立代理中间件,存在网络损耗、运维复杂、有单点风险,目前基本被淘汰。
Q10:分片键的设计核心原则是什么?
答:1、业务绝大多数查询必须携带分片键;2、分片键打散均匀,避免数据热点;3、关联业务尽量共用分片键,适配绑定表规则;4、分片键确定后不可修改。
6.3 实战坑点题(Q11-Q15)
Q11:分片分页错乱的根本原因和解决方案?
答:根本原因:多分片独立分页,单分片局部结果无法代表全局数据,直接使用limit offset会出现数据重复、丢失、错乱。解决方案:中小数据使用内存全局排序分页,大数据量使用游标ID分页。
Q12:分片架构下有哪些SQL黑名单,禁止使用?
答:禁止无分片键的GROUP BY、SUM、COUNT等全局聚合;禁止跨分片复杂JOIN、嵌套子查询;禁止limit -1无边界分页;禁止跨分片批量DDL、存储过程、自定义函数。
Q13:什么是数据热点与数据倾斜?如何解决?
答:部分分片数据量、QPS远超其他分片,造成单点性能瓶颈。解决方案:热点数据单独隔离、采用复合分片二次打散、虚拟分片映射、冷热数据分层存储。
Q14:哈希分片为什么只能2的倍数扩容?
答:2/4/8倍数扩容可保证半数数据路由规则不变,仅需迁移半数数据,扩容成本低;非倍数扩容会改变所有取模路由规则,需要全量迁移所有数据,风险和成本极高。
Q15:分片场景下如何解决跨库JOIN问题?
答:四种主流方案:1、业务设计绑定表,同分片直接JOIN;2、核心字段冗余,消除关联查询;3、业务层单次查询多分片,手动组装数据;4、核心业务引入ES替代复杂关联查询。
6.4 架构落地题(Q16-Q21)
Q16:分片架构如何实现平滑扩容、零停机数据迁移?
答:1、搭建新分片环境,配置新旧双路由规则;2、开启新旧库双写,保障增量数据无遗漏;3、分批迁移历史存量数据;4、数据校验比对、修复差异;5、灰度切流逐步切换新架构;6、稳定后下线旧分片。
Q17:分片架构下分布式事务如何处理?
答:普通业务使用Sharding-JDBC原生弱XA最终一致性事务,性能高、无锁;核心支付、交易业务,结合Seata AT实现强一致性柔性事务,规避跨分片事务不一致问题。
Q18:冷热数据分层的落地思路?
答:近3个月热数据采用哈希分片,保证数据均匀、支撑高并发读写;历史冷数据采用范围分片按月归档,单独存储,不占用核心数据库性能,实现性能与存储平衡。
Q19:垂直拆分和水平拆分的区别和适用场景?
答:垂直拆分按业务、字段拆分,解决单表字段冗余、业务耦合问题;水平拆分按规则拆分数据,解决单表海量数据、高并发瓶颈。海量数据高并发场景,优先使用水平分片。
Q20:SpringCloud微服务如何适配Sharding-JDBC?
答:分片规则、数据源、算法全部下沉至数据服务,业务微服务仅通过Feign调用数据接口,零分片配置;全局分布式ID统一由数据服务生成,禁止微服务跨库查询、跨服务JOIN。
Q21:生产环境分片上线核心校验要点有哪些?
答:1、生产关闭SQL打印,避免日志泄露与刷屏;2、集群环境雪花算法worker-id保证唯一;3、优化连接池参数,防止连接耗尽;4、核心查询强制携带分片键;5、绑定表、广播表配置完整;6、严格禁用黑名单SQL语法。
7. 学习总结
-
分片能不用就不用,优先常规优化
-
分片键是一切的核心,设计错了全盘重构
-
哈希分片保均衡、范围分片保扩容,各有取舍
-
绑定表解JOIN、广播表解字典、精准算法解路由
-
开发最大禁忌:无分片键查询、跨分片JOIN、随意改分片键