分库分表实战:Sharding-JDBC 快速落地

作者:洛水石
> 标签:分库分表、Sharding-JDBC、MySQL、数据库优化


一、为什么需要分库分表

1.1 单机数据库的性能瓶颈

|------------|------|-------------|
| 数据量级 | 表记录数 | 常见问题 |
| < 500万 | 小表 | 无明显问题 |
| 500万-5000万 | 中表 | 查询变慢,需要索引优化 |
| 5000万-2亿 | 大表 | 索引失效,DDL变慢 |

单表性能瓶颈表现

  • 查询响应时间 QUOTE_2

1.2 分库分表的驱动力

``_INLINE

┌─────────────────────────────────────────────────────────────┐

│ 单机 MySQL 性能极限 │

├─────────────────────────────────────────────────────────────┤

│ 连接数:默认 151,最大约 10000 │

│ 存储:单实例建议 < 500GB │

│ QPS:纯内存查询约 10万,磁盘查询约 1万 │

│ TPS:写操作约 2000-5000 │

└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐

│ 分库分表解决方案 │

├─────────────────────────────────────────────────────────────┤

│ 分库:解决连接数瓶颈、存储瓶颈、单点故障 │

│ 分表:解决单表数据量过大、索引效率下降、并发写入瓶颈 │

└─────────────────────────────────────────────────────────────┘

__`_INLINE

1.3 什么场景需要分库分表

必须分表的场景

  • 日增数据 QUOTE_3

必须分库的场景

  • 单库连接数打满

  • 单库存储空间 QUOTE_4


二、分库分表核心概念

2.1 分片策略

__`_INLINE

┌────────────────────────────────────────────────────────────┐

│ 分片维度选择 │

├────────────────┬────────────────┬──────────────────────────┤

│ 分片键 │ 示例 │ 适用场景 │

├────────────────┼────────────────┼──────────────────────────┤

│ 用户ID │ user_id │ 用户相关数据(订单、日志) │

│ 订单ID │ order_id │ 订单系统 │

│ 时间 │ create_time │ 日志、流水类数据 │

│ 地区 │ region_code │ 地域性业务(配送、仓储) │

│ 哈希 │ hash(id) │ 数据分布均匀的业务 │

└────────────────┴────────────────┴──────────────────────────┘

__`_INLINE

2.2 分片算法

|------------|---------------|-----------|--------|
| 算法 | 原理 | 优点 | 缺点 |
| **取模** | user_id % 8 | 分布均匀,扩容简单 | 数据迁移量大 |
| **范围** | 按时间范围 | 便于归档,查询高效 | 热点问题 |
| **哈希** | hash(user_id) | 分布均匀 | 范围查询困难 |
| **混合** | 范围+哈希 | 兼顾两者优点 | 复杂度较高 |

推荐策略

  • 用户数据:_user_id % 16__INLINE 分16张表

  • 订单数据:按 _create_time 年月分表 + user_id__INLINE 取模

  • 日志数据:按天/月分表

2.3 分库分表架构

__`_INLINE

┌─────────────┐

│ 应用层 │

│ SpringBoot │

└──────┬──────┘

┌──────▼──────┐

│ Sharding-JDBC│

│ 中间件 │

└──────┬──────┘

┌──────────────────┼──────────────────┐

│ │ │

┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐

│ ds_0 │ │ ds_1 │ │ ds_2 │

│ 主从库 │ │ 主从库 │ │ 主从库 │

└────┬────┘ └─────┬─────┘ └─────┬─────┘

│ │ │

┌────┴────┐ ┌────┴────┐ ┌────┴────┐

│t_order_0│ │t_order_0│ │t_order_0│

│t_order_1│ │t_order_1│ │t_order_1│

│ ... │ │ ... │ │ ... │

│t_order_7│ │t_order_7│ │t_order_7│

└─────────┘ └─────────┘ └─────────┘

__`_INLINE


三、Sharding-JDBC 快速入门

3.1 引入依赖

__`__INLINE_xml

<dependencies>

<!-- Sharding-JDBC -->

<dependency>

<groupId>org.apache.shardingsphere</groupId>

<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>

<version>5.4.0</version>

</dependency>

<!-- MySQL 驱动 -->

<dependency>

<groupId>mysql</groupId>

<artifactId>mysql-connector-java</artifactId>

<version>8.0.33</version>

</dependency>

<!-- MyBatis-Plus -->

<dependency>

<groupId>com.baomidou</groupId>

<artifactId>mybatis-plus-boot-starter</artifactId>

<version>3.5.4.1</version>

</dependency>

</dependencies>

__`_INLINE

3.2 基础配置

__`__INLINE_yaml

数据源配置

spring:

shardingsphere:

datasource:

names: ds0, ds1, ds2

ds0:

type: com.zaxxer.hikari.HikariDataSource

driver-class-name: com.mysql.cj.jdbc.Driver

jdbc-url: jdbc:mysql://192.168.1.10:3306/db_order_0?useUnicode=true&characterEncoding=utf8

username: root

password: 123456

ds1:

type: com.zaxxer.hikari.HikariDataSource

driver-class-name: com.mysql.cj.jdbc.Driver

jdbc-url: jdbc:mysql://192.168.1.11:3306/db_order_1?useUnicode=true&characterEncoding=utf8

username: root

password: 123456

ds2:

type: com.zaxxer.hikari.HikariDataSource

driver-class-name: com.mysql.cj.jdbc.Driver

jdbc-url: jdbc:mysql://192.168.1.12:3306/db_order_2?useUnicode=true&characterEncoding=utf8

username: root

password: 123456

分片规则

rules:

sharding:

tables:

订单表分片配置

t_order:

actual-data-nodes: ds{0..2}.t_order_{0..7}

库分片策略

database-strategy:

standard:

sharding-column: user_id

sharding-algorithm-name: db-inline

表分片策略

table-strategy:

standard:

sharding-column: user_id

sharding-algorithm-name: table-inline

主键生成策略

key-generate-strategy:

column: order_id

key-generator-name: snowflake

分片算法定义

sharding-algorithms:

db-inline:

type: INLINE

props:

algorithm-expression: ds${user_id % 3}

table-inline:

type: INLINE

props:

algorithm-expression: t_order_${user_id % 8}

主键生成器

key-generators:

snowflake:

type: SNOWFLAKE

props:

worker-id: ${WORKER_ID:0}

属性配置

props:

sql-show: true

__`_INLINE

3.3 实体类定义

__`__INLINE_java

@Data

@TableName("t_order")

public class Order {

@TableId(type = IdType.INPUT)

private Long orderId;

private Long userId;

private String orderNo;

private BigDecimal totalAmount;

private Integer status;

private LocalDateTime createTime;

private LocalDateTime updateTime;

}

__`_INLINE

3.4 数据访问层

__`__INLINE_java

@Mapper

public interface OrderMapper extends BaseMapper<Order__QUOTE_5__


四、高级分片策略

4.1 复合分片策略(标准分片)

__`__INLINE_java

/**

* 自定义分片算法:按用户ID取模分库,按订单ID取模分表

*/

@Component

public class CustomShardingAlgorithm implements

StandardShardingAlgorithm<Long>, ComplexKeysShardingAlgorithm<Long__QUOTE_6__

4.2 时间范围分片(日志场景)

__`__INLINE_yaml

按月分表配置

spring:

shardingsphere:

rules:

sharding:

tables:

t_operation_log:

actual-data-nodes: ds0.t_operation_log_{2024..2026}{(01..12)}

table-strategy:

standard:

sharding-column: create_time

sharding-algorithm-name: log-month-sharding

sharding-algorithms:

log-month-sharding:

type: CLASS_BASED

props:

strategy: STANDARD

algorithmClassName: com.example.sharding.algorithm.MonthShardingAlgorithm

__`_INLINE

__`__INLINE_java

/**

* 按月分片算法

*/

public class MonthShardingAlgorithm implements StandardShardingAlgorithm<Date__QUOTE_7__

4.3 读写分离配置

__`__INLINE_yaml

spring:

shardingsphere:

rules:

readwrite-splitting:

data-sources:

ds_master:

type: Static

props:

write-data-source-name: ds_master

read-data-source-names: ds_slave_0, ds_slave_1

load-balancer-name: round-robin

load-balancers:

round-robin:

type: ROUND_ROBIN

sharding:

tables:

t_order:

actual-data-nodes: rw_ds_{0..2}.t_order_{0..7}

__`_INLINE


五、分布式主键生成

5.1 Snowflake 算法

__`_INLINE

┌─────────────────────────────────────────────────────────────────┐

│ Snowflake 结构 │

├──────────┬────────────┬──────────────┬──────────────┬───────────┤

│ 1 bit │ 41 bits │ 10 bits │ 12 bits │ 共64bit │

│ 符号位 │ 时间戳 │ 机器ID │ 序列号 │ │

│ (保留) │ (毫秒级) │ (5位机房+5位) │ (每毫秒4096) │ │

└──────────┴────────────┴──────────────┴──────────────┴───────────┘

__`_INLINE

特点

  • 趋势递增,性能高

  • 不依赖数据库

  • 支持分布式部署

5.2 自定义主键生成器

__`__INLINE_java

@Component

public class OrderIdGenerator implements KeyGenerateAlgorithm {

private final Snowflake snowflake;

public OrderIdGenerator() {

// workerId 从配置中心获取

long workerId = getWorkerIdFromConfigCenter();

this.snowflake = new Snowflake(workerId, 0);

}

@Override

public Comparable<?QUOTE_8

5.3 业务 ID 生成(带业务含义)

__`__INLINE_java

@Service

public class OrderNoGenerator {

private final StringRedisTemplate redisTemplate;

/**

* 生成订单号:时间戳(14位) + 用户ID后4位 + 随机数(4位) + 序号(2位)

* 示例:20240505143000123456789012

*/

public String generateOrderNo(Long userId) {

String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

String userSuffix = String.format("%04d", userId % 10000);

String random = RandomStringUtils.randomNumeric(4);

String seq = getSequence();

return date + userSuffix + random + seq;

}

private String getSequence() {

String key = "order:seq:" + LocalDate.now();

Long seq = redisTemplate.opsForValue().increment(key);

redisTemplate.expire(key, 1, TimeUnit.DAYS);

return String.format("%02d", seq % 100);

}

}

__`_INLINE


六、分页与排序

6.1 分页查询的挑战

__`__INLINE_sql

-- 跨分片分页查询需要合并多个数据源结果

SELECT * FROM t_order

WHERE create_time QUOTE_9

Sharding-JDBC 分页策略

  • 改写 SQL:去掉 LIMIT,查询所有分片

  • 内存归并:合并结果后排序、分页

  • 流式归并:大数据量时避免OOM

6.2 分页优化方案

__`__INLINE_java

@Service

public class OrderService {

/**

* 优化分页:基于游标/上次查询位置

*/

public List<Order__QUOTE_10__

6.3 全局排序方案

__`__INLINE_java

/**

* 全局排序:基于时间范围限定

*/

public List<Order__QUOTE_11__


七、数据迁移与扩容

7.1 平滑扩容方案

__`_INLINE

阶段1:双写阶段(数据同步)

应用 ─────→ 旧库(4片)

└──────→ 新库(8片)← 同步工具(Canal)

阶段2:切读阶段(验证数据)

应用 ─────→ 旧库(写)

└──────→ 新库(读+写)

阶段3:切写阶段(完全切换)

应用 ─────→ 新库(8片)

旧库停止写入

阶段4:清理阶段

停用旧库,释放资源

__`_INLINE

7.2 双写实现

__`__INLINE_java

@Component

public class DualWriteInterceptor implements Interceptor {

@Autowired

private DataSource oldDataSource;

@Autowired

private DataSource newDataSource;

private volatile boolean writeToNew = false;

@Override

public Object intercept(Invocation invocation) throws Throwable {

Object result = invocation.proceed();

// 异步写入新库

if (writeToNew && isWriteOperation(invocation)) {

asyncWriteToNew(invocation);

}

return result;

}

private void asyncWriteToNew(Invocation invocation) {

executor.submit(() -QUOTE_12

7.3 数据校验与补偿

__`__INLINE_java

@Service

public class DataMigrationService {

/**

* 数据一致性校验

*/

public void verifyDataConsistency(Date startTime, Date endTime) {

List<Order__QUOTE_13__


八、常见问题与解决方案

8.1 跨分片 JOIN 问题

问题 :Sharding-JDBC 不支持跨分片 JOIN

解决方案

__`__INLINE_java

/**

* 方案1:应用层组装(推荐)

*/

public OrderDetail getOrderDetail(Long orderId, Long userId) {

// 1. 查询订单(路由到具体分片)

Order order = orderMapper.selectById(orderId, userId);

// 2. 查询用户(另一张表,同样路由)

User user = userMapper.selectById(order.getUserId());

// 3. 组装结果

OrderDetail detail = new OrderDetail();

detail.setOrder(order);

detail.setUser(user);

return detail;

}

/**

* 方案2:冗余字段(宽表设计)

*/

@Data

@TableName("t_order")

public class Order {

// 基础字段

private Long orderId;

private Long userId;

// 冗余用户字段,避免JOIN

private String userName;

private String userPhone;

private String userAddress;

}

/**

* 方案3:绑定表(关联表同分片)

*/

// YAML配置

spring:

shardingsphere:

rules:

sharding:

binding-tables:

  • t_order,t_order_item

订单和订单项使用相同的分片策略,确保在同一分片

__`_INLINE

8.2 分布式事务

__`__INLINE_java

@Service

public class OrderService {

@ShardingSphereTransactionType(TransactionType.BASE)

@Transactional

public void createOrder(Order order, List<OrderItem__QUOTE_14__

8.3 热点数据问题

__`__INLINE_java

/**

* 热点用户数据缓存

*/

@Service

public class HotDataService {

@Cacheable(value = "hot:user:order", key = "#userId")

public List<Order__QUOTE_15__


九、监控与运维

9.1 Sharding-JDBC 指标监控

__`__INLINE_java

@Component

public class ShardingMetricsCollector {

@Autowired

private MeterRegistry meterRegistry;

@PostConstruct

public void init() {

// 注册分片路由指标

meterRegistry.gauge("sharding.route.time",

Tags.of("type", "routing"),

this,

ShardingMetricsCollector::getAvgRouteTime

);

// 注册SQL执行指标

meterRegistry.counter("sharding.sql.execute",

Tags.of("type", "execute")

);

}

}

__`_INLINE

9.2 慢查询监控

__`__INLINE_yaml

慢查询日志配置

spring:

shardingsphere:

props:

sql-show: true

sql-simple: false

__`_INLINE

__``java

@Component

@Slf4j

public class SlowSqlInterceptor implements QueryInterceptor {

private static final long SLOW_SQL_THRESHOLD = 100; // ms

@Override

public Object intercept(QueryInvocation invocation) throws Throwable {

long start = System.currentTimeMillis();

try {

return invocation.proceed();

} finally {

long cost = System.currentTimeMillis() - start;

if (cost QUOTE_16


总结

|---------------|----------------|-------------|
| 核心概念 | 实现要点 | 注意事项 |
| **分片键选择** | 业务维度、查询频率 | 避免热点、便于扩展 |
| **分片算法** | 取模/范围/哈希 | 考虑数据分布、扩容迁移 |
| **主键生成** | Snowflake/业务ID | 趋势递增、分布式唯一 |
| **分页查询** | 游标/限定分片键 | 避免深分页、内存归并 |
| **分布式事务** | BASE/Seata | 最终一致性 |
| **数据迁移** | 双写/校验/补偿 | 平滑切换、数据一致 |

作者:洛水石 | 数据库优化 | 分库分表实战

作者:洛水石 | 数据库优化 | 分库分表实战

相关推荐
冬天vs不冷2 小时前
面试必知必会(13):MySQL锁机制
mysql·面试·职场和发展
冬天vs不冷2 小时前
面试必知必会(14):MySQL执行计划与SQL优化
sql·mysql·面试
萧曵 丶2 小时前
MySQL 高频面试题(由浅到深 完整版,面试必背)
数据库·mysql·面试
czlczl200209252 小时前
MySQL 执行引擎:排序与临时表机制深度解析
数据库·mysql
木井巳4 小时前
【MySQL数据库】数据库操作及数据类型
数据库·mysql·adb
爱莉希雅&&&4 小时前
MySQL MGR + MySQL Router 高可用集群完整笔记(含手动配置 + Shell 接管双路线)
linux·数据库·笔记·mysql·mysqlrouter·mysqlshell
战南诚5 小时前
深分页问题
数据库·mysql
ByteX6 小时前
MySQL 联合索引创建效果评估
数据库·mysql
渔民小镇7 小时前
4 行代码接入 Spring —— ionet 的生态融合之道
java·服务器·分布式·游戏