16. 数据库连接池、读写分离与分库分表

深入 HikariCP 连接池原理、ShardingSphere 分库分表、读写分离架构与分布式主键设计

随着业务数据量突破千万级、亿级,单一数据库实例在连接数、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 替代 ArrayListget() 不检查范围,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 类型说明替代方案
跨分片 JOINt_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

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析