1. 为什么我们需要分库分表?

第一次遇到数据库性能瓶颈的场景至今记忆犹新。那是一个电商促销日,我们的订单表数据量突破了3000万条,简单的查询都要花费数秒。更可怕的是,当DBA尝试添加索引时,整个数据库直接锁死。这就是典型的数据量超出单机数据库承载能力的案例。

分库分表本质上是一种水平拆分策略,将原本存储在单一数据库中的数据,按照某种规则分散到多个数据库或数据表中。这种架构演进不是银弹,但能有效解决以下三类问题:

  1. 容量瓶颈 :单机数据库的存储空间有限,当数据量达到TB级别时,备份恢复都成问题
  2. 性能瓶颈 :大数据量表上的索引效率下降,全表扫描成本剧增
  3. 可用性风险 :所有鸡蛋放在一个篮子里,单点故障影响面大

重要提示:分库分表是最后的解决方案。在考虑拆分前,应该先尝试优化SQL、增加缓存、升级硬件等常规手段。只有当这些方法都无法满足需求时,才需要考虑架构层面的拆分。

2. ShardingSphere-JDBC核心架构解析

作为Apache顶级项目,ShardingSphere提供了完整的分布式数据库解决方案生态。其中ShardingSphere-JDBC定位为轻量级Java框架,在JDBC层提供额外服务,具有以下显著特点:

ShardingSphere-JDBC架构图

2.1 核心组件工作流

  1. SQL解析引擎 :通过ANTLR解析SQL语句,提取表名、条件等上下文信息
  2. 路由引擎 :根据分片规则确定SQL应该发往哪些真实数据节点
  3. 改写引擎 :将逻辑SQL改写为可在真实节点上执行的物理SQL
  4. 执行引擎 :多线程并发访问不同数据节点
  5. 归并引擎 :将多个数据节点的结果集合并为单一结果

2.2 与同类方案对比

特性 ShardingSphere-JDBC MyCat TDDL
架构层级 JDBC驱动层 代理层 JDBC驱动层
性能损耗 低(直接连接DB) 中(网络跳转)
功能完整性 完善 完善 基础
运维复杂度
跨语言支持 Java专属 多语言 Java专属

从实际使用经验看,ShardingSphere-JDBC特别适合Java技术栈的互联网应用,它的无中心化架构避免了代理层带来的性能损耗和单点故障。

3. 实战:电商订单分库分表示例

假设我们有一个订单系统,需要将订单表order按用户ID分库分表。具体需求如下:

  • 分2个库(ds0, ds1)
  • 每个库分4张表(order_0到order_3)
  • 分片键为用户ID(user_id)

3.1 环境准备

首先引入Maven依赖(以5.1.1版本为例):

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.1.1</version>
</dependency>

3.2 分片规则配置

创建Java配置类定义分片逻辑:

public class ShardingConfig {
    
    @Bean
    public DataSource dataSource() throws SQLException {
        // 定义两个真实数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put("ds0", createDataSource("jdbc:mysql://localhost:3306/ds0"));
        dataSourceMap.put("ds1", createDataSource("jdbc:mysql://localhost:3306/ds1"));
        
        // 分表规则
        ShardingTableRuleConfiguration orderTableRule = new ShardingTableRuleConfiguration(
                "order", 
                "ds${0..1}.order_${0..3}");
        
        // 分库策略(用户ID最后一位模2)
        orderTableRule.setDatabaseShardingStrategy(new StandardShardingStrategyConfiguration(
                "user_id", 
                new InlineShardingAlgorithm() {
                    @Override
                    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
                        int hash = shardingValue.getValue().hashCode();
                        return "ds" + (hash % 2);
                    }
                }));
        
        // 分表策略(用户ID最后两位模4)
        orderTableRule.setTableShardingStrategy(new StandardShardingStrategyConfiguration(
                "user_id",
                new InlineShardingAlgorithm() {
                    @Override
                    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
                        int hash = shardingValue.getValue().hashCode();
                        return "order_" + (Math.abs(hash) % 4);
                    }
                }));
        
        // 构建配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(orderTableRule);
        
        return ShardingSphereDataSourceFactory.createDataSource(
                dataSourceMap, 
                Collections.singleton(shardingRuleConfig),
                new Properties());
    }
    
    private DataSource createDataSource(String url) {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(url);
        ds.setUsername("root");
        ds.setPassword("password");
        return ds;
    }
}

3.3 分片算法详解

上述配置中使用了内联分片算法,实际生产环境更推荐使用标准分片算法:

public class UserIdShardingAlgorithm implements StandardShardingAlgorithm<String> {
    
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
        String userId = shardingValue.getValue();
        // 使用一致性哈希避免扩容时的数据迁移
        int hash = MurmurHash.hash32(userId);
        int index = Math.abs(hash) % availableTargetNames.size();
        return new ArrayList<>(availableTargetNames).get(index);
    }
    
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<String> shardingValue) {
        // 范围查询时路由到所有节点
        return availableTargetNames;
    }
    
    @Override
    public void init() {}
    
    @Override
    public String getType() {
        return "USER_ID_HASH";
    }
}

关键经验:分片算法应该考虑未来扩容需求。简单的取模运算在增加节点时会导致大规模数据迁移,建议使用一致性哈希等算法。

4. 生产环境关键问题与解决方案

4.1 分布式ID生成

分库分表后,自增主键会重复,需要引入分布式ID方案。以下是Snowflake实现示例:

public class SnowflakeIdGenerator {
    private final long datacenterId;
    private final long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨异常");
        }
        
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        return ((timestamp - 1288834974657L) << 22) 
                | (datacenterId << 17) 
                | (workerId << 12) 
                | sequence;
    }
}

4.2 跨库关联查询

分库后,JOIN操作变得困难。解决方案包括:

  1. 冗余字段 :在关联表中冗余需要查询的字段
  2. 内存计算 :先查询主表,再批量查询关联表,在内存中关联
  3. 广播表 :将小表在所有库中冗余存储

4.3 分布式事务

对于订单创建这类需要跨库事务的场景,可采用Seata集成:

# application.yml
seata:
  enabled: true
  application-id: order-service
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default

5. 性能优化实战技巧

5.1 索引设计原则

  1. 每个分片表都需要单独建立索引
  2. 分片键必须包含在联合索引中
  3. 避免在非分片键上使用范围查询

5.2 查询优化建议

  • 精准查询 :确保条件包含分片键
-- 好:包含user_id
SELECT * FROM order WHERE user_id = 123 AND order_no = 'ABC';

-- 差:没有分片键
SELECT * FROM order WHERE order_no = 'ABC';
  • 批量查询 :使用IN而不是多次单条查询
-- 好
SELECT * FROM order WHERE user_id IN (123, 456);

-- 差
SELECT * FROM order WHERE user_id = 123;
SELECT * FROM order WHERE user_id = 456;

5.3 监控指标

建议监控以下关键指标:

指标名称 监控目标 告警阈值
路由到多个节点的查询比例 避免全库扫描 > 20%
最大分片查询耗时 识别慢查询 > 500ms
分布式事务成功率 确保事务可靠性 < 99.9%

6. 扩容与数据迁移方案

当现有分片数量不足时,需要考虑扩容。以下是推荐步骤:

  1. 准备新库新表 :部署新的数据库实例,创建相同结构的表
  2. 双写模式 :修改应用配置,同时写入新旧分片
  3. 历史数据迁移 :使用DataX等工具迁移存量数据
  4. 校验数据一致性 :使用校验工具比对数据
  5. 切换读流量 :逐步将读请求导向新分片
  6. 停用旧分片 :确认无误后下线旧节点

血泪教训:一定要在低峰期执行迁移,并准备好回滚方案。我们曾经因为迁移过程中没有关闭写流量,导致数据不一致花了三天时间修复。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐