随着业务数据量突破千万级、亿级,单一数据库实例在连接数、CPU、磁盘 I/O、内存等方面都会达到瓶颈。Java 企业级应用需要系统性地解决三个问题:连接高效复用、读流量分散、数据水平拆分。本文从连接池原理出发,逐步深入到 ShardingSphere 分库分表实战。
1. 连接池原理:为什么 HikariCP 是最快的
1.1 连接池存在的意义
无连接池:
客户端 ──→ 创建 TCP 连接 (3次握手) ──→ 认证鉴权 ──→ SQL执行 ──→ 关闭连接 (4次挥手)
每次请求 ~30-100ms 的连接开销
有连接池:
客户端 ──→ 从池中获取连接(纳秒级) ──→ SQL执行 ──→ 归还连接
连接预先创建并保持
1.2 HikariCP 设计哲学
HikariCP 以 极简设计 + 极致性能 著称,其核心设计决策:
| 设计点 | HikariCP 做法 | 其他池常见做法 |
|---|---|---|
ConcurrentBag 存储连接 | CAS + SynchronousQueue,无锁获取 | LinkedBlockingQueue 加锁 |
FastList 替代 ArrayList | get() 不检查范围,remove() 从尾部扫描 | 标准 JDK 集合 |
| 字节码优化 | 生成 ProxyFactory 内联方法 | JDK Proxy 反射 |
| 连接生命周期 | 简化状态机,减少 CAS 操作 | 复杂状态管理 |
| HouseKeeper | 单线程管理超时回收 | 多线程竞争 |
1.3 HikariCP 核心配置
spring:
datasource:
hikari:
# ── 尺寸配置 ──
minimum-idle: 10 # 最小空闲连接(建议 = maximum-pool-size,避免频繁创建)
maximum-pool-size: 20 # 最大连接数,经验公式: (CPU核数 * 2) + 硬盘数
connection-timeout: 30000 # 获取连接超时(ms),默认30s
idle-timeout: 600000 # 空闲连接超时回收(ms),仅当 min-idle < max 时生效
max-lifetime: 1800000 # 连接最大存活时间(ms),30分钟,小于数据库 wait_timeout
leak-detection-threshold: 60000 # 连接泄漏检测,60s未归还则记录日志
# ── 连接验证 ──
connection-test-query: SELECT 1 # JDBC4驱动不需要,用 isValid() 自动检测
validation-timeout: 5000
# ── 优化配置 ──
data-source-properties:
cachePrepStmts: true
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
useServerPrepStmts: true
useLocalSessionState: true
useLocalTransactionState: true
连接数计算公式:
推荐最大连接数 = (核心数 * 2) + 有效磁盘数
实际值根据监控调整:活跃连接数 / 总连接数 ≈ 60-80% 为合理区间
1.4 HikariCP 监控指标
@Autowired
private HikariDataSource dataSource;
public void printMetrics() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
HikariConfigMXBean configMXBean = dataSource.getHikariConfigMXBean();
System.out.println("活跃连接: " + poolMXBean.getActiveConnections());
System.out.println("空闲连接: " + poolMXBean.getIdleConnections());
System.out.println("等待线程: " + poolMXBean.getThreadsAwaitingConnection());
System.out.println("总连接数: " + poolMXBean.getTotalConnections());
}
Micrometer 指标采集:
@Configuration
public class HikariMetricsConfig {
@Bean
public MeterBinder hikariMetrics(HikariDataSource dataSource) {
return registry -> {
Gauge.builder("hikaricp.connections.active",
dataSource, ds -> ds.getHikariPoolMXBean().getActiveConnections())
.tag("pool", dataSource.getPoolName())
.register(registry);
};
}
}
2. 读写分离架构
2.1 主从复制原理
写请求 异步复制 读请求
客户端 ──→ Master ──→ binlog ──→ I/O Thread ──→ Relay Log ──→ SQL Thread ──→ Slave
│ │
└─ 数据变更 └─ 只读查询
复制延迟: Master commit → Slave 应用,通常 < 1s(同城)或 10-100ms(本地)
复制模式: 异步(默认)/ 半同步 / GTID
2.2 Spring + ShardingSphere 读写分离
spring:
shardingsphere:
datasource:
names: master, slave0, slave1
master:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://master:3306/db
username: rw_user
slave0:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://slave0:3306/db
username: ro_user
slave1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://slave1:3306/db
username: ro_user
rules:
readwrite-splitting:
data-sources:
ds_rw:
static-strategy:
write-data-source-name: master
read-data-source-names: slave0, slave1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
props:
sql-show: true
负载均衡算法:
| 算法 | 说明 | 场景 |
|---|---|---|
ROUND_ROBIN | 轮询 | 从库配置相同 |
RANDOM | 随机 | 从库配置相同 |
WEIGHT | 按权重 | 从库配置不同 |
强制走主库:
// 使用 HintManager 强制路由到主库(如读取刚写入的数据)
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly();
// 这段 SQL 强制走 Master
orderMapper.selectById(orderId);
}
2.3 复制延迟应对策略
@Configuration
public class ReadWriteConfig {
/**
* 方案 1: 关键读走主库(业务层判断)
*/
@Transactional(readOnly = false) // 强制主库
public Order getOrderImmediately(Long orderId) {
return orderMapper.selectById(orderId);
}
/**
* 方案 2: 缓存补偿
* 写主库时同步写缓存,读从库 miss 时读缓存
*/
public Order getOrderWithCache(Long orderId) {
Order order = cache.get(orderId);
if (order != null) return order;
order = orderMapper.selectById(orderId); // 从库
if (order != null) cache.put(orderId, order, 5, TimeUnit.MINUTES);
return order;
}
/**
* 方案 3: 延迟阈值检测
*/
public boolean isReplicationLagAcceptable() {
Long slaveSeconds = jdbcTemplate.queryForObject(
"SHOW SLAVE STATUS", (rs, rowNum) -> rs.getLong("Seconds_Behind_Master"));
return slaveSeconds != null && slaveSeconds < 1;
}
}
3. 分库分表
3.1 何时需要分库分表
| 指标 | 分表 | 分库 |
|---|---|---|
| 单表数据量 | > 500万 → 考虑分表 | — |
| 单表容量 | > 10GB | — |
| 单库连接数 | — | > 2000 或 CPU > 80% |
| TPS/QPS | — | 写 TPS > 5000 |
分片键选择:
- 高频查询条件字段(如 user_id、tenant_id)
- 避免热点(如时间字段易导致"尾部热点")
- 尽量保证数据均匀分布
3.2 ShardingSphere 水平分片
spring:
shardingsphere:
rules:
sharding:
tables:
t_order:
# 实际数据节点: ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order-table-inline
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-db-inline
key-generate-strategy:
column: order_id
key-generator-name: snowflake
sharding-algorithms:
order-table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
order-db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 0
3.3 分片算法详解
| 算法 | 类型 | 说明 |
|---|---|---|
INLINE | 标准 | Groovy 表达式,如 user_id % 2 |
HASH_MOD | 标准 | 哈希取模,数据分布更均匀 |
RANGE | 标准 | 范围分片,如时间按月分表 |
COMPLEX | 复合 | 多字段组合分片 |
CLASS_BASED | 自定义 | 实现 ShardingAlgorithm 接口 |
// 自定义分片算法:按用户 ID 的哈希值取模
public class CustomHashShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long id = shardingValue.getValue();
int hash = Math.abs(id.hashCode());
String suffix = String.valueOf(hash % availableTargetNames.size());
return availableTargetNames.stream()
.filter(name -> name.endsWith(suffix))
.findFirst()
.orElseThrow();
}
}
3.4 绑定表与广播表
spring:
shardingsphere:
rules:
sharding:
# 绑定表:关联查询时避免笛卡尔积
binding-tables:
- t_order, t_order_item # 相同的分片策略,join 时路由到同一节点
# 广播表:每个库都有一份完整数据(如字典表、配置表)
broadcast-tables:
- t_dict
- t_region
绑定表原理:
用户查询: SELECT o.*, i.* FROM t_order o JOIN t_order_item i ON o.order_id = i.order_id
无绑定表 → 4 次查询(2 库 × 2 表)= 笛卡尔积
有绑定表 → 2 次查询(order_id 相同,路由到同一库同一分片)
3.5 分布式主键
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自增 ID + 步长 | 简单 | 扩容困难,ID 不连续 |
| UUID | 全局唯一 | 无序,占用大(36字符),索引性能差 |
| Snowflake | 趋势递增,高性能 | 依赖时钟,存在回溯风险 |
| Leaf(美团) | 高可用,可线性扩展 | 需额外部署 |
| 数据库号段 | 简单,ID 相对连续 | 有单点瓶颈 |
Snowflake 结构:
64 bits:
1 位符号(0) | 41 位时间戳(ms) | 10 位工作节点 | 12 位序列号
└─ 5位数据中心 + 5位机器
可用 69 年,每秒每节点可生成 4096 个 ID
// ShardingSphere Snowflake 配置
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: ${WORKER_ID:0} # 0-1023,需不同节点不同值
max-vibration-offset: 1 # 抖动上限,解决时钟回拨
4. 分库分表后的 SQL 限制
4.1 不再支持的 SQL 类型
| SQL 类型 | 说明 | 替代方案 |
|---|---|---|
| 跨分片 JOIN | t_order 和 t_user 分片键不同 | 应用层聚合或冗余字段 |
| 跨分片 ORDER BY + LIMIT | 需全局排序,性能差 | 按分片键排序或应用层归并 |
| 聚合函数(SUM、COUNT等) | 需全分片扫描 | ShardingSphere 支持简单聚合 |
| 子查询跨分片 | 不支持 | 拆分为多次查询 |
| 存储过程 | 不支持 | 应用层实现业务逻辑 |
4.2 分页查询优化
// 问题: LIMIT 1000000, 10 需要每个分片查询 1000010 条,然后归并
// 方案 1: 流式分页(游标)
SELECT * FROM t_order WHERE create_time > '2024-01-01'
AND order_id > #{lastOrderId} // 上一页最后一条的 ID
ORDER BY order_id
LIMIT 10;
// 方案 2: ES + 分页(最终一致性)
// 数据双写 MySQL + Elasticsearch,分页走 ES
// 详情查询走数据库
// 方案 3: ShardingSphere 分页归并(小数据量适用)
PageHelper.startPage(pageNum, pageSize);
orderMapper.selectAll(); // 自动做内存归并排序
5. 实战:电商订单分库分表设计
5.1 需求分析
预估数据量: 10 亿订单,5 年
分库: 8 库(按 user_id % 8)
分表: 每库 16 表(按 order_id % 16)
总计: 128 张物理表
单表预估: 780 万条
5.2 表结构
-- t_order_0 ~ t_order_127
CREATE TABLE t_order_0 (
order_id BIGINT PRIMARY KEY, -- Snowflake ID
user_id BIGINT NOT NULL, -- 分库键
merchant_id INT NOT NULL,
total_amount DECIMAL(16,2) NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, create_time),
INDEX idx_merchant (merchant_id, create_time)
) ENGINE=InnoDB;
-- t_order_item_0 ~ t_order_item_127(绑定表,相同分片策略)
CREATE TABLE t_order_item_0 (
item_id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, -- 冗余分库键,避免跨分片
sku_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(16,2) NOT NULL,
INDEX idx_order (order_id)
) ENGINE=InnoDB;
5.3 扩容方案
方案 A: 影子库迁移(推荐)
阶段 1: 新库新表创建,双写开始(旧库 + 新库)
阶段 2: 历史数据迁移(按 id 范围分批)
阶段 3: 数据校验(一致性对比)
阶段 4: 切读流量到新库
阶段 5: 停止旧库写入,完成
方案 B: 一致性哈希
// 使用虚拟节点,扩容时只迁移部分数据
// ShardingSphere 5.x 支持自动弹性伸缩(Scaling)
6. 监控与告警
| 指标 | 类型 | 告警阈值 |
|---|---|---|
| 连接池使用率 | 百分比 | > 80% |
| 活跃连接数 | 数值 | > maximum-pool-size * 0.9 |
| 等待连接线程数 | 数值 | > 0(持续 30s) |
| 慢查询数量 | QPS | > 10/min |
| 主从延迟 | 秒数 | > 1s |
| 分片路由错误 | QPS | > 0 |
延伸阅读
- ORM 框架:JPA/Hibernate 与 MyBatis — 分库分表下的 ORM 适配
- Java 缓存策略与 Redis 集成 — 缓存与数据库的配合
- 分布式锁与协调服务 — 分片环境下的全局一致性
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。