分库分表深度解析

前言

随着业务规模的快速增长,单库单表的架构模式逐渐暴露出性能瓶颈、存储限制和维护困难等问题。分库分表作为解决数据库扩展性的核心方案,已成为大型互联网系统架构设计中的重要组成部分。本文将系统性地介绍分库分表的核心概念、实施准则、拆分规则和最佳实践,帮助读者在实际项目中做出正确的架构决策。


一、基础概念解析

1.1 分库分表的核心定义

分库分表是指将原本存储在单个数据库实例中的数据,按照一定的规则分散存储到多个数据库实例或多个数据表中,从而达到提升系统性能、扩展存储容量、降低单点压力的目的。

核心解决的扩展性问题

问题类型 具体表现 分库分表解决方案
性能瓶颈 单表数据量超过千万级,查询响应时间显著增加 通过分表减少单表数据量,提升查询效率
存储限制 单实例磁盘空间不足,无法继续扩容 通过分库将数据分散到多个实例,突破存储上限
连接数瓶颈 数据库连接数达到上限,影响并发处理能力 通过分库分摊连接压力,提升整体并发能力
IO压力 单实例IO负载过高,导致读写性能下降 通过分库分表将IO分散到多个物理设备
维护困难 大表备份、恢复、索引重建耗时过长 小表维护成本更低,可并行操作

1.2 垂直拆分 vs 水平拆分

垂直拆分(Vertical Splitting)

垂直拆分是基于业务逻辑或数据表结构进行的拆分,分为垂直分库垂直分表两种形式。

垂直分库

  • 按照业务模块将不同的表分配到不同的数据库中
  • 示例:用户库、订单库、商品库、支付库等

垂直分表

  • 将大表中不常用的字段拆分到新表中
  • 示例:将用户基础信息和用户扩展信息分离
sql 复制代码
-- 垂直分表示例
-- 原表:user
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(100),
    email VARCHAR(100),
    phone VARCHAR(20),
    address TEXT,
    profile TEXT,
    create_time DATETIME
);

-- 拆分后:基础信息表
CREATE TABLE user_basic (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(100),
    email VARCHAR(100),
    phone VARCHAR(20),
    create_time DATETIME
);

-- 拆分后:扩展信息表
CREATE TABLE user_profile (
    user_id BIGINT PRIMARY KEY,
    address TEXT,
    profile TEXT,
    update_time DATETIME
);

优点

  • 拆分规则清晰,实施相对简单
  • 能够解决单表字段过多的问题
  • 便于业务模块的独立扩展

缺点

  • 部分业务场景下存在跨库事务问题
  • 无法解决单表数据量过大的问题

适用场景

  • 业务模块边界清晰,耦合度低
  • 单表字段过多(超过20-30个字段)
  • 不同模块的访问频率差异较大

水平拆分(Horizontal Splitting)

水平拆分是根据数据的行进行拆分,将数据按照某种规则分散到多个数据库或表中。

水平分库

  • 将同一表的数据分散到多个数据库中
  • 示例:将订单表按用户ID哈希分散到8个数据库中

水平分表

  • 将同一库中的表数据分散到多个表中
  • 示例:将用户表按ID范围分散到user_0、user_1、user_2...中
sql 复制代码
-- 水平分表示例
-- 原表:order
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50),
    amount DECIMAL(10,2),
    status INT,
    create_time DATETIME
);

-- 拆分后:按user_id哈希分表
CREATE TABLE order_0 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50),
    amount DECIMAL(10,2),
    status INT,
    create_time DATETIME
);

CREATE TABLE order_1 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50),
    amount DECIMAL(10,2),
    status INT,
    create_time DATETIME
);

CREATE TABLE order_2 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50),
    amount DECIMAL(10,2),
    status INT,
    create_time DATETIME
);

优点

  • 能够有效解决单表数据量过大的问题
  • 提升查询性能,减少单表数据扫描
  • 便于数据迁移和扩容

缺点

  • 跨表分页查询复杂度增加
  • 需要引入额外的路由层
  • 数据一致性维护难度增加

适用场景

  • 单表数据量超过千万级
  • 写入量达到单实例瓶颈
  • 需要横向扩展系统容量

1.3 分库与分表的关联性与独立性

关联性

  • 分库通常伴随着分表,两者经常同时使用
  • 分库是物理层面的拆分,分表是逻辑层面的拆分
  • 分库分表的设计需要综合考虑,形成统一的策略

独立性

  • 分库可以独立于分表实施,仅解决实例层面的问题
  • 分表可以在单一数据库中独立实施,解决单表问题
  • 在某些场景下,可以只分库不分表,或只分表不分库
java 复制代码
// 分库分表策略组合示例
public class ShardingStrategy {
    
    // 只分库不分表:按用户ID哈希分到8个库
    public int getDatabaseShard(Long userId) {
        return (int) (userId % 8);
    }
    
    // 只分表不分库:按时间范围分表
    public String getTableShard(Date createTime) {
        String month = new SimpleDateFormat("yyyyMM").format(createTime);
        return "order_" + month;
    }
    
    // 分库分表:先分库再分表
    public String getFullTableName(Long userId, Date createTime) {
        int dbShard = getDatabaseShard(userId);
        String tableShard = getTableShard(createTime);
        return "db" + dbShard + ".order_" + tableShard;
    }
}

二、实施准则与依据

2.1 分库分表的决策依据

数据量阈值参考

指标 阈值 说明
单表数据量 1000万-2000万行 超过此范围,查询性能明显下降
单表存储容量 10GB-50GB 根据数据库类型和硬件配置调整
单库连接数 接近数据库最大连接数的70% 需要考虑分库分表连接池压力
QPS/TPS 单实例QPS > 5000 或 TPS > 1000 根据业务类型和数据库性能评估

性能指标参考

sql 复制代码
-- 查询性能分析示例
EXPLAIN SELECT * FROM user WHERE id = 123456;
-- 关注:type是否为const或eq_ref、rows扫描行数、Extra是否包含Using index

-- 慢查询分析
SELECT 
    sql_text,
    exec_count,
    avg_time,
    max_time
FROM mysql.slow_log
WHERE avg_time > 1000  -- 超过1秒的查询
ORDER BY avg_time DESC
LIMIT 10;

业务增长预期

短期预期(3-6个月)

  • 评估当前数据增长速度
  • 预测峰值压力和存储需求
  • 制定分库分表实施时间表

中长期预期(1-2年)

  • 考虑业务爆发性增长的可能性
  • 设计具有弹性的分片策略
  • 规划多级扩容方案

2.2 实施前提条件与准备工作

技术前提条件

  1. 完善的监控体系

    • 数据库性能监控(QPS、TPS、慢查询、连接数)
    • 应用层监控(响应时间、错误率、调用链)
    • 资源监控(CPU、内存、磁盘IO、网络)
  2. 代码层面的改造能力

    • 访问层需要支持分库分表的路由逻辑
    • ORM框架需要支持多数据源配置
    • 需要引入分布式事务解决方案
  3. 运维能力准备

    • 数据库备份与恢复方案
    • 数据迁移与验证工具
    • 故障快速切换机制

准备工作清单

复制代码
□ 业务场景分析:梳理核心业务的访问模式
□ 数据特征分析:统计数据量、增长趋势、热点数据
□ 技术选型:选择合适的分库分表中间件(ShardingSphere、MyCAT等)
□ 环境准备:部署分库分表测试环境
□ 测试验证:功能测试、性能测试、压力测试
□ 应急预案:制定回滚方案和应急处理流程
□ 文档完善:更新架构文档、操作手册、应急预案

2.3 分库分表的挑战与规避策略

主要挑战

挑战类型 具体表现 影响
分布式事务 跨库事务一致性难以保证 数据一致性问题
跨库关联查询 JOIN操作性能急剧下降 查询效率降低
分页查询 跨表分页结果不准确 用户体验差
全局唯一ID 分布式环境下的ID生成 业务逻辑复杂
数据迁移 在线数据迁移困难 业务中断风险
扩容复杂 重新分片涉及大量数据移动 运维成本高

规避策略

1. 分布式事务处理

java 复制代码
// 基于最终一致性的方案
public class OrderService {
    
    @Autowired
    private MessageProducer messageProducer;
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountMapper accountMapper;
    
    public void createOrder(Order order) {
        // 1. 创建订单(主库操作)
        orderMapper.insert(order);
        
        // 2. 发送消息到MQ
        OrderMessage message = new OrderMessage();
        message.setOrderId(order.getId());
        message.setUserId(order.getUserId());
        message.setAmount(order.getAmount());
        messageProducer.send(message);
        
        // 3. 账户服务消费消息处理(异步)
        // accountService.decreaseAccount(message);
    }
}

2. 跨库联表查询优化

  • 数据冗余:在相关表中冗余必要字段
  • 应用层JOIN:在应用层进行数据组装
  • 宽表设计:将关联数据设计到同一分片

3. 分页查询优化

java 复制代码
// 使用ID范围分页
public List<Order> getOrdersByPage(Long lastId, int pageSize) {
    return orderMapper.selectPage(
        "WHERE id > #{lastId} ORDER BY id LIMIT #{pageSize}",
        lastId, pageSize
    );
}

// 使用缓存+游标方式
public CursorResult<Order> getOrdersWithCursor(String cursor, int pageSize) {
    // 使用Redis存储游标状态
    String cursorKey = "order_cursor:" + cursor;
    // 分片查询并组装结果
    return cursorResult;
}

三、核心拆分规则

3.1 分片策略详解

1. 范围分片(Range Sharding)

根据数据的某个连续范围字段进行分片,常用于时间、数值型字段。

实现方式

sql 复制代码
-- 按用户ID范围分片
-- user_0: 1-1000000
-- user_1: 1000001-2000000
-- user_2: 2000001-3000000

-- 按时间范围分片
CREATE TABLE order_202401 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    create_time DATETIME,
    -- 其他字段
    KEY idx_create_time (create_time)
) PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

Java路由实现

java 复制代码
public class RangeShardingStrategy {
    
    private final List<RangeConfig> rangeConfigs;
    
    public RangeShardingStrategy() {
        this.rangeConfigs = Arrays.asList(
            new RangeConfig(1L, 1000000L, "user_0"),
            new RangeConfig(1000001L, 2000000L, "user_1"),
            new RangeConfig(2000001L, 3000000L, "user_2")
        );
    }
    
    public String getTableName(Long userId) {
        for (RangeConfig config : rangeConfigs) {
            if (userId >= config.getMin() && userId <= config.getMax()) {
                return config.getTableName();
            }
        }
        throw new IllegalArgumentException("User ID out of range");
    }
    
    static class RangeConfig {
        private Long min;
        private Long max;
        private String tableName;
        // constructor and getters
    }
}

优点

  • 查询范围数据效率高
  • 扩容相对简单,只需新增范围
  • 数据分布可预测

缺点

  • 可能存在数据分布不均
  • 范围边界可能出现热点
  • 不适合随机访问场景

适用场景

  • 数据有明显的时间或数值分布特征
  • 经常需要进行范围查询
  • 数据量相对均匀分布

2. 哈希分片(Hash Sharding)

通过对分片键进行哈希计算,将数据均匀分散到各个分片。

实现方式

java 复制代码
public class HashShardingStrategy {
    
    private final int shardCount;
    
    public HashShardingStrategy(int shardCount) {
        this.shardCount = shardCount;
    }
    
    // 简单哈希分片
    public int getShard(Long shardKey) {
        return (int) (shardKey % shardCount);
    }
    
    // 更均匀的哈希分片
    public int getConsistentHashShard(Long shardKey) {
        int hash = hashFunction(shardKey.toString());
        return Math.abs(hash) % shardCount;
    }
    
    private int hashFunction(String key) {
        // FNV哈希算法
        final int FNV_32_PRIME = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < key.length(); i++) {
            hash = (hash ^ key.charAt(i)) * FNV_32_PRIME;
        }
        return hash;
    }
    
    // 获取完整表名
    public String getTableName(Long userId) {
        int shard = getConsistentHashShard(userId);
        return "user_" + shard;
    }
}

分片键选择原则

  • 选择查询频率高的字段作为分片键
  • 分片键的值应该具有高基数(重复率低)
  • 避免选择变化频繁的字段

优点

  • 数据分布均匀
  • 查询性能稳定
  • 适合高并发写入场景

缺点

  • 范围查询性能较差
  • 扩容时需要重新哈希,数据迁移复杂
  • 分片键选择不当可能导致热点问题

适用场景

  • 数据分布均匀,无明显的访问模式
  • 高并发写入场景
  • 需要负载均衡的读多写少场景

3. 一致性哈希(Consistent Hashing)

解决传统哈希分片在扩容时需要大量数据迁移的问题。

实现方式

java 复制代码
public class ConsistentHashing {
    
    private final TreeMap<Long, String> virtualNodes = new TreeMap<>();
    private final int virtualNodeCount = 150; // 每个物理节点对应的虚拟节点数
    
    public void addNode(String node) {
        for (int i = 0; i < virtualNodeCount; i++) {
            String virtualNode = node + "#" + i;
            int hash = getHash(virtualNode);
            virtualNodes.put((long) hash, node);
        }
    }
    
    public void removeNode(String node) {
        for (int i = 0; i < virtualNodeCount; i++) {
            String virtualNode = node + "#" + i;
            int hash = getHash(virtualNode);
            virtualNodes.remove((long) hash);
        }
    }
    
    public String getNode(String key) {
        if (virtualNodes.isEmpty()) {
            return null;
        }
        
        int hash = getHash(key);
        
        // 顺时针查找第一个大于等于hash的虚拟节点
        Map.Entry<Long, String> entry = virtualNodes.ceilingEntry((long) hash);
        
        if (entry == null) {
            // 如果没找到,返回第一个节点(环形结构)
            entry = virtualNodes.firstEntry();
        }
        
        return entry.getValue();
    }
    
    private int getHash(String key) {
        // 使用MurmurHash算法
        return MurmurHash.hash32(key);
    }
}

优点

  • 扩容时只影响部分数据迁移
  • 节点故障时自动路由到其他节点
  • 具有较好的负载均衡性

缺点

  • 实现复杂度较高
  • 虚拟节点数量选择影响性能
  • 需要维护环状结构

适用场景

  • 需要频繁扩缩容的系统
  • 分布式缓存系统
  • 对数据迁移敏感的业务

4. 按业务模块分片

根据业务逻辑将相关的数据表分配到同一个数据库中。

架构示例

复制代码
数据库集群
├── 用户库 (db_user)
│   ├── user_basic          # 用户基础信息
│   ├── user_profile        # 用户扩展信息
│   ├── user_address        # 用户地址信息
│   └── user_social         # 用户社交信息
│
├── 订单库 (db_order)
│   ├── order_main          # 订单主表
│   ├── order_detail        # 订单详情
│   ├── order_payment       # 支付信息
│   └── order_log           # 订单日志
│
├── 商品库 (db_product)
│   ├── product_info        # 商品信息
│   ├── product_category    # 商品分类
│   ├── product_inventory   # 库存信息
│   └── product_sku         # SKU信息
│
└── 支付库 (db_payment)
    ├── payment_record      # 支付记录
    ├── payment_refund      # 退款记录
    └── payment_channel     # 支付渠道

配置示例

yaml 复制代码
# ShardingSphere 配置
sharding:
  tables:
    user_basic:
      actualDataNodes: db_user.user_basic
    user_profile:
      actualDataNodes: db_user.user_profile
    order_main:
      actualDataNodes: db_order.order_main
    order_detail:
      actualDataNodes: db_order.order_detail
    product_info:
      actualDataNodes: db_product.product_info
    payment_record:
      actualDataNodes: db_payment.payment_record

优点

  • 业务边界清晰,便于维护
  • 跨库关联查询需求减少
  • 不同业务可以独立扩容

缺点

  • 可能造成负载不均
  • 某些业务模块可能成为瓶颈
  • 需要准确的业务边界识别

适用场景

  • 微服务架构
  • 业务模块耦合度低
  • 不同业务访问模式差异大

3.2 路由规则设计与实现

路由规则设计原则

  1. 单一分片键原则:尽量避免复杂的多键分片
  2. 可预测性原则:相同的分片键必须路由到相同的目标
  3. 扩展性原则:路由规则应支持动态调整
  4. 性能优先原则:路由计算必须高效

ShardingSphere路由配置

yaml 复制代码
# ShardingSphere 分片配置示例
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/db0
        username: root
        password: password
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/db1
        username: root
        password: password
      ds2:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/db2
        username: root
        password: password
      ds3:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/db3
        username: root
        password: password
    
    sharding:
      tables:
        t_order:
          actualDataNodes: ds$->{0..3}.t_order_$->{0..3}
          databaseStrategy:
            standard:
              shardingColumn: user_id
              preciseAlgorithmClassName: com.example.DbShardingAlgorithm
          tableStrategy:
            standard:
              shardingColumn: order_id
              preciseAlgorithmClassName: com.example.TableShardingAlgorithm
          keyGenerator:
            type: SNOWFLAKE
            column: order_id

自定义分片算法实现

java 复制代码
public class DbShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    
    @Override
    public String doSharding(Collection<String> availableTargetNames, 
                           PreciseShardingValue<Long> shardingValue) {
        
        Long userId = shardingValue.getValue();
        // 数据库分片:userId % 4
        int dbIndex = (int) (userId % 4);
        
        for (String targetName : availableTargetNames) {
            if (targetName.endsWith(String.valueOf(dbIndex))) {
                return targetName;
            }
        }
        
        throw new IllegalArgumentException("No database found for userId: " + userId);
    }
}

public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    
    @Override
    public String doSharding(Collection<String> availableTargetNames, 
                           PreciseShardingValue<Long> shardingValue) {
        
        Long orderId = shardingValue.getValue();
        // 表分片:orderId % 4
        int tableIndex = (int) (orderId % 4);
        
        for (String targetName : availableTargetNames) {
            if (targetName.endsWith(String.valueOf(tableIndex))) {
                return targetName;
            }
        }
        
        throw new IllegalArgumentException("No table found for orderId: " + orderId);
    }
}

复合分片策略

java 复制代码
public class ComplexShardingStrategy {
    
    /**
     * 复合分片:先按业务模块分库,再按哈希分表
     */
    public String getShardingResult(String businessModule, Long shardKey) {
        // 第一步:按业务模块确定数据库
        String database = getDatabaseByModule(businessModule);
        
        // 第二步:按哈希确定表
        String table = getTableByHash(shardKey);
        
        return database + "." + table;
    }
    
    private String getDatabaseByModule(String businessModule) {
        Map<String, String> moduleToDb = Map.of(
            "user", "db_user",
            "order", "db_order",
            "product", "db_product"
        );
        return moduleToDb.getOrDefault(businessModule, "db_default");
    }
    
    private String getTableByHash(Long shardKey) {
        int tableIndex = Math.abs(shardKey.hashCode()) % 8;
        return "table_" + tableIndex;
    }
}

四、最佳实践建议

4.1 分库分表后的事务处理策略

1. 本地事务 + 消息队列(最终一致性)

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private MessageProducer messageProducer;
    
    @Autowired
    private TransactionTemplate transactionTemplate;
    
    /**
     * 订单创建流程:本地事务 + 异步消息
     */
    @Override
    public void createOrder(OrderDTO orderDTO) {
        transactionTemplate.execute(status -> {
            // 1. 本地事务:创建订单
            Order order = new Order();
            BeanUtils.copyProperties(orderDTO, order);
            orderMapper.insert(order);
            
            // 2. 发送订单创建消息
            OrderEvent event = new OrderEvent();
            event.setOrderId(order.getId());
            event.setUserId(order.getUserId());
            event.setAmount(order.getAmount());
            event.setType("ORDER_CREATED");
            
            messageProducer.send(event);
            
            return order.getId();
        });
    }
}

// 库存服务消费消息
@KafkaListener(topics = "order-topic")
public void handleOrderEvent(OrderEvent event) {
    if ("ORDER_CREATED".equals(event.getType())) {
        // 扣减库存
        inventoryService.deductInventory(event);
    }
}

2. TCC事务(Try-Confirm-Cancel)

java 复制代码
@Service
public class TccOrderService {
    
    /**
     * Try阶段:预留资源
     */
    @Transactional
    public void tryCreateOrder(Order order) {
        // 1. 冻结订单金额
        accountService.freezeAmount(order.getUserId(), order.getAmount());
        
        // 2. 预扣库存
        productService.reserveInventory(order.getProductId(), order.getQuantity());
        
        // 3. 记录TCC事务日志
        tccTransactionLog.recordTry(order.getId());
    }
    
    /**
     * Confirm阶段:确认执行
     */
    @Transactional
    public void confirmCreateOrder(Long orderId) {
        // 1. 扣除冻结金额
        accountService.deductFrozenAmount(orderId);
        
        // 2. 扣减实际库存
        productService.deductReservedInventory(orderId);
        
        // 3. 更新TCC事务状态
        tccTransactionLog.updateStatus(orderId, "CONFIRMED");
    }
    
    /**
     * Cancel阶段:取消执行
     */
    @Transactional
    public void cancelCreateOrder(Long orderId) {
        // 1. 释放冻结金额
        accountService.releaseFrozenAmount(orderId);
        
        // 2. 释放预留库存
        productService.releaseReservedInventory(orderId);
        
        // 3. 更新TCC事务状态
        tccTransactionLog.updateStatus(orderId, "CANCELLED");
    }
}

3. Saga事务(补偿机制)

java 复制代码
@Service
public class SagaOrderService {
    
    private List<SagaStep> sagaSteps = new ArrayList<>();
    
    public void executeSaga(Order order) {
        List<SagaStep> executedSteps = new ArrayList<>();
        
        try {
            // 按顺序执行各个步骤
            for (SagaStep step : sagaSteps) {
                step.execute(order);
                executedSteps.add(step);
            }
            
        } catch (Exception e) {
            // 发生异常,按相反顺序执行补偿
            Collections.reverse(executedSteps);
            for (SagaStep step : executedSteps) {
                try {
                    step.compensate(order);
                } catch (Exception compensateException) {
                    log.error("补偿失败", compensateException);
                }
            }
            throw e;
        }
    }
}

interface SagaStep {
    void execute(Order order);
    void compensate(Order order);
}

4.2 分布式ID生成方案对比

常用方案对比

方案 优点 缺点 适用场景
UUID 生成简单,无单点问题 占用空间大,无序 小规模系统,非数值型ID
数据库自增 递增有序,实现简单 单点问题,性能瓶颈 单库场景,小规模系统
Redis生成 性能较好,支持递增 依赖Redis,集群复杂 中小规模系统,需要有序ID
Snowflake 高性能,分布式,有序 依赖机器时钟,ID较长 大规模分布式系统
号段模式 性能极佳,降低DB压力 ID不连续,浪费空间 高并发写入场景

4.3 跨库联表查询解决方案

1. 数据冗余(字段冗余)

sql 复制代码
-- 原始设计
-- 用户表:user
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(100)
);

-- 订单表:order
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    order_no VARCHAR(50),
    amount DECIMAL(10,2)
);

-- 冗余优化:在订单表中冗余用户名
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    username VARCHAR(50),  -- 冗余字段
    order_no VARCHAR(50),
    amount DECIMAL(10,2)
);

优点

  • 查询性能提升,避免JOIN
  • 实现简单,代码改动小

缺点

  • 数据冗余,占用额外空间
  • 需要保证数据一致性

一致性维护

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private MessageProducer messageProducer;
    
    /**
     * 更新用户信息时,同步更新订单表中的冗余字段
     */
    @Transactional
    public void updateUsername(Long userId, String newUsername) {
        // 1. 更新用户表
        userMapper.updateUsername(userId, newUsername);
        
        // 2. 发送消息通知更新相关表
        UserUpdateEvent event = new UserUpdateEvent();
        event.setUserId(userId);
        event.setNewUsername(newUsername);
        messageProducer.send(event);
    }
}

// 订单服务消费消息
@KafkaListener(topics = "user-update-topic")
public void handleUserUpdate(UserUpdateEvent event) {
    orderMapper.updateUsernameByUserId(event.getUserId(), event.getNewUsername());
}

2. 应用层JOIN

java 复制代码
@Service
public class OrderQueryService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 应用层JOIN:先查订单,再批量关联查询
     */
    public OrderDetailVO getOrderDetail(Long orderId) {
        // 1. 查询订单基本信息
        Order order = orderMapper.selectById(orderId);
        if (order == null) {
            return null;
        }
        
        // 2. 根据userId查询用户信息
        User user = userMapper.selectById(order.getUserId());
        
        // 3. 根据productId查询商品信息
        Product product = productMapper.selectById(order.getProductId());
        
        // 4. 组装VO对象
        OrderDetailVO vo = new OrderDetailVO();
        vo.setOrderId(order.getId());
        vo.setOrderNo(order.getOrderNo());
        vo.setAmount(order.getAmount());
        
        if (user != null) {
            vo.setUsername(user.getUsername());
            vo.setUserPhone(user.getPhone());
        }
        
        if (product != null) {
            vo.setProductName(product.getName());
            vo.setProductPrice(product.getPrice());
        }
        
        return vo;
    }
    
    /**
     * 批量查询优化:减少数据库查询次数
     */
    public List<OrderDetailVO> batchGetOrderDetails(List<Long> orderIds) {
        if (orderIds.isEmpty()) {
            return Collections.emptyList();
        }
        
        // 1. 批量查询订单
        List<Order> orders = orderMapper.batchSelectByIds(orderIds);
        
        // 2. 提取所有userId和productId
        Set<Long> userIds = orders.stream()
            .map(Order::getUserId)
            .collect(Collectors.toSet());
        Set<Long> productIds = orders.stream()
            .map(Order::getProductId)
            .collect(Collectors.toSet());
        
        // 3. 批量查询用户和商品
        Map<Long, User> userMap = userMapper.batchSelectByIds(userIds).stream()
            .collect(Collectors.toMap(User::getId, Function.identity()));
        Map<Long, Product> productMap = productMapper.batchSelectByIds(productIds).stream()
            .collect(Collectors.toMap(Product::getId, Function.identity()));
        
        // 4. 组装结果
        return orders.stream().map(order -> {
            OrderDetailVO vo = new OrderDetailVO();
            vo.setOrderId(order.getId());
            vo.setOrderNo(order.getOrderNo());
            vo.setAmount(order.getAmount());
            
            User user = userMap.get(order.getUserId());
            if (user != null) {
                vo.setUsername(user.getUsername());
            }
            
            Product product = productMap.get(order.getProductId());
            if (product != null) {
                vo.setProductName(product.getName());
            }
            
            return vo;
        }).collect(Collectors.toList());
    }
}

3. 数据聚合中间件

java 复制代码
@Service
public class DataAggregationService {
    
    @Autowired
    private ShardingDataSource shardingDataSource;
    
    /**
     * 并行查询多个分片,聚合结果
     */
    public <T> List<T> aggregateQuery(String sql, ResultSetExtractor<T> extractor) {
        List<DataSource> dataSources = getAllDataSources();
        
        // 并行查询
        List<CompletableFuture<List<T>>> futures = dataSources.stream()
            .map(dataSource -> CompletableFuture.supplyAsync(() -> {
                JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
                return jdbcTemplate.query(sql, extractor);
            }, asyncExecutor))
            .collect(Collectors.toList());
        
        // 等待所有查询完成并合并结果
        return futures.stream()
            .flatMap(future -> future.join().stream())
            .collect(Collectors.toList());
    }
    
    /**
     * 跨库分页查询
     */
    public PageResult<Order> crossDbPageQuery(int pageNum, int pageSize) {
        int offset = (pageNum - 1) * pageSize;
        
        // 1. 从每个分片查询指定数量的数据
        List<List<Order>> shardResults = new ArrayList<>();
        for (DataSource dataSource : getAllDataSources()) {
            String sql = "SELECT * FROM `order` ORDER BY id DESC LIMIT ? OFFSET ?";
            JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
            List<Order> orders = jdbcTemplate.query(sql, 
                new Object[]{pageSize * 2, offset}, orderRowMapper);
            shardResults.add(orders);
        }
        
        // 2. 合并所有结果
        List<Order> allOrders = shardResults.stream()
            .flatMap(List::stream)
            .sorted(Comparator.comparing(Order::getId).reversed())
            .collect(Collectors.toList());
        
        // 3. 分页截取
        int total = allOrders.size();
        List<Order> pageData = allOrders.stream()
            .skip(offset)
            .limit(pageSize)
            .collect(Collectors.toList());
        
        return new PageResult<>(pageData, total, pageNum, pageSize);
    }
}

4.4 扩容与数据迁移策略

1. 在线数据迁移方案

java 复制代码
@Service
public class DataMigrationService {
    
    @Autowired
    private ShardingDataSource sourceDataSource;
    
    @Autowired
    private ShardingDataSource targetDataSource;
    
    /**
     * 在线数据迁移主流程
     */
    public void migrateData(String tableName, int sourceShardCount, int targetShardCount) {
        // 1. 数据同步:开启双写
        enableDualWrite(tableName, sourceShardCount, targetShardCount);
        
        // 2. 历史数据迁移
        migrateHistoryData(tableName, sourceShardCount, targetShardCount);
        
        // 3. 数据校验
        validateDataConsistency(tableName, sourceShardCount, targetShardCount);
        
        // 4. 切换流量
        switchTraffic(tableName, sourceShardCount, targetShardCount);
        
        // 5. 清理旧数据
        cleanupOldData(tableName, sourceShardCount);
    }
    
    /**
     * 开启双写模式
     */
    private void enableDualWrite(String tableName, int sourceShardCount, int targetShardCount) {
        log.info("开启双写模式: {}", tableName);
        // 在应用层配置中添加目标分片,同时写入新旧分片
        shardingConfig.addShard(tableName, targetShardCount);
        shardingConfig.enableDualWriteMode(tableName, true);
    }
    
    /**
     * 历史数据迁移
     */
    private void migrateHistoryData(String tableName, int sourceShardCount, int targetShardCount) {
        log.info("开始迁移历史数据: {}", tableName);
        
        int batchSize = 1000;
        for (int i = 0; i < sourceShardCount; i++) {
            long lastId = 0;
            while (true) {
                String sql = String.format(
                    "SELECT * FROM %s_%d WHERE id > ? ORDER BY id LIMIT %d", 
                    tableName, i, batchSize
                );
                
                List<Map<String, Object>> records = querySourceData(sql, lastId);
                
                if (records.isEmpty()) {
                    break;
                }
                
                // 批量写入目标分片
                batchInsertTargetData(records, tableName, targetShardCount);
                
                lastId = (Long) records.get(records.size() - 1).get("id");
                log.info("迁移进度: shard={}, lastId={}", i, lastId);
            }
        }
    }
    
    /**
     * 数据一致性校验
     */
    private void validateDataConsistency(String tableName, int sourceShardCount, int targetShardCount) {
        log.info("开始数据校验: {}", tableName);
        
        for (int i = 0; i < sourceShardCount; i++) {
            // 比较源分片和目标分片的数据量
            long sourceCount = getRecordCount(tableName + "_" + i);
            long targetCount = getTargetRecordCount(tableName, i, targetShardCount);
            
            if (sourceCount != targetCount) {
                throw new RuntimeException(String.format(
                    "数据校验失败: shard=%d, source=%d, target=%d", 
                    i, sourceCount, targetCount
                ));
            }
        }
        
        log.info("数据校验通过: {}", tableName);
    }
    
    /**
     * 流量切换
     */
    private void switchTraffic(String tableName, int sourceShardCount, int targetShardCount) {
        log.info("开始切换流量: {}", tableName);
        
        // 1. 灰度切换:先切换10%的流量
        shardingConfig.updateShardWeight(tableName, sourceShardCount, 0.9);
        shardingConfig.updateShardWeight(tableName, targetShardCount, 0.1);
        
        // 2. 观察一段时间,确认无异常
        monitorMetrics(tableName);
        
        // 3. 逐步增加目标分片的权重
        for (int weight = 20; weight <= 100; weight += 20) {
            shardingConfig.updateShardWeight(tableName, targetShardCount, weight / 100.0);
            shardingConfig.updateShardWeight(tableName, sourceShardCount, (100 - weight) / 100.0);
            monitorMetrics(tableName);
        }
        
        // 4. 完全切换到新分片
        shardingConfig.removeShard(tableName, sourceShardCount);
        log.info("流量切换完成: {}", tableName);
    }
}

2. 双写方案实现

java 复制代码
@Component
public class DualWriteInterceptor implements Interceptor {
    
    @Autowired
    private ShardingConfig shardingConfig;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        
        // 判断是否需要双写
        if (needDualWrite(ms)) {
            return dualWrite(invocation, ms, parameter);
        }
        
        return invocation.proceed();
    }
    
    /**
     * 双写实现
     */
    private Object dualWrite(Invocation invocation, MappedStatement ms, Object parameter) {
        String tableName = extractTableName(ms.getId());
        
        try {
            // 1. 执行原写入操作
            Object result = invocation.proceed();
            
            // 2. 异步写入目标分片
            asyncExecutor.execute(() -> {
                try {
                    writeToTargetShard(ms, parameter, tableName);
                } catch (Exception e) {
                    log.error("双写失败,记录到重试队列", e);
                    retryQueue.offer(new RetryTask(ms, parameter, tableName));
                }
            });
            
            return result;
        } catch (Exception e) {
            log.error("双写主操作失败", e);
            throw e;
        }
    }
    
    /**
     * 写入目标分片
     */
    private void writeToTargetShard(MappedStatement ms, Object parameter, String tableName) {
        List<String> targetShards = shardingConfig.getTargetShards(tableName);
        
        for (String shard : targetShards) {
            try {
                // 获取目标分片的数据源
                DataSource targetDataSource = shardingConfig.getDataSource(shard);
                
                // 执行写入
                executeWrite(ms, parameter, targetDataSource);
                
            } catch (Exception e) {
                log.error("写入目标分片失败: {}", shard, e);
                throw e;
            }
        }
    }
}

3. 扩容决策矩阵

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    分库分表扩容决策矩阵                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  当前容量 │ 数据增长 │ 业务重要性 │ 扩容方案                 │
│────────────────────────────────────────────────────────────│
│    <70%   │   低    │   低      │ 监控观察,暂不扩容         │
│    <70%   │   高    │   低      │ 规划扩容,3-6个月实施      │
│    <70%   │   高    │   高      │ 提前扩容,1-3个月实施      │
│   70-90%  │   低    │   低      │ 制定扩容计划,准备实施      │
│   70-90%  │   低    │   高      │ 立即启动扩容               │
│   70-90%  │   高    │   高      │ 紧急扩容,7×24小时监控      │
│    >90%   │   -     │   -      │ 立即执行紧急扩容           │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  扩容方案选择:                                               │
│                                                             │
│  1. 水平扩容(推荐优先):                                     │
│     - 增加分片数量                                           │
│     - 适用:数据量大,访问模式均匀                             │
│                                                             │
│  2. 垂直扩容:                                               │
│     - 升级硬件配置                                           │
│     - 适用:暂时性瓶颈,成本可接受                             │
│                                                             │
│  3. 架构优化:                                               │
│     - 引入缓存层                                             │
│     - 读写分离                                               │
│     - 异步处理                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

五、分库分表方案选择决策流程

复制代码
┌──────────────────────────────────────────────────────────────┐
│                    分库分表决策流程图                          │
└──────────────────────────────────────────────────────────────┘

┌───────────────────┐
│ 开始评估           │
└────────┬──────────┘
         │
         ▼
┌───────────────────┐     是     ┌───────────────────┐
│ 是否存在性能瓶颈? │──────────►│ 性能瓶颈类型      │
└────────┬──────────┘           │ - 查询慢          │
         │                      │ - 连接数满        │
         │ 否                   │ - IO压力大        │
         │                      │ - 存储空间不足    │
         ▼                      └────────┬──────────┘
┌───────────────────┐                    │
│ 监控观察,暂不实施 │                    ▼
└───────────────────┘          ┌───────────────────┐
                              │ 是否可垂直拆分?   │
                              └────────┬──────────┘
                                       │
                    ┌──────────────────┴──────────────────┐
                    │ 是                                  │ 否
                    ▼                                     ▼
         ┌───────────────────┐              ┌───────────────────┐
         │ 垂直拆分           │              │ 数据量评估         │
         │ - 按业务模块拆分   │              │ - 单表数据量       │
         │ - 按冷热数据拆分   │              │ - 单库数据量       │
         └────────┬──────────┘              └────────┬──────────┘
                  │                                   │
                  ▼                                   │ 否
         ┌───────────────────┐                        │
         │ 是否仍需分片?     │                        ▼
         └────────┬──────────┘              ┌───────────────────┐
                  │                           │ 监控观察,暂不实施 │
                  │ 是                        └───────────────────┘
                  ▼
         ┌───────────────────┐
         │ 选择水平拆分       │
         └────────┬──────────┘
                  │
         ┌────────┴────────┐
         │                 │
         ▼                 ▼
┌───────────────────┐ ┌───────────────────┐
│ 访问模式分析       │ │ 数据特征分析       │
│ - 读多写少         │ │ - 均匀分布         │
│ - 写多读少         │ │ - 热点数据         │
│ - 读写均衡         │ │ - 时间有序         │
└────────┬──────────┘ └────────┬──────────┘
         │                       │
         │                       │
         ▼                       ▼
┌───────────────────┐ ┌───────────────────┐
│ 选择分片策略       │ │ 选择分片策略       │
│ - 范围分片         │ │ - 哈希分片         │
│ - 一致性哈希       │ │ - 复合分片         │
└────────┬──────────┘ └────────┬──────────┘
         │                       │
         └───────────┬───────────┘
                     │
                     ▼
         ┌───────────────────┐
         │ 确定分片数量       │
         │ - 初始分片数       │
         │ - 预留扩容空间     │
         └────────┬──────────┘
                  │
                  ▼
         ┌───────────────────┐
         │ 技术方案设计       │
         │ - 中间件选型       │
         │ - 路由规则设计     │
         │ - 分布式事务方案   │
         └────────┬──────────┘
                  │
                  ▼
         ┌───────────────────┐
         │ 实施计划制定       │
         │ - 迁移策略         │
         │ - 验证方案         │
         │ - 回滚方案         │
         └────────┬──────────┘
                  │
                  ▼
         ┌───────────────────┐
         │ 执行实施           │
         │ - 测试环境验证     │
         │ - 灰度发布         │
         │ - 全量切换         │
         └────────┬──────────┘
                  │
                  ▼
         ┌───────────────────┐
         │ 监控优化           │
         │ - 性能监控         │
         │ - 问题定位         │
         │ - 持续优化         │
         └────────┬──────────┘
                  │
                  ▼
         ┌───────────────────┐
         │ 完成               │
         └───────────────────┘

六、总结与建议

6.1 核心要点回顾

  1. 分库分表是解决数据库扩展性的关键方案,但不是万能药,需要根据实际业务场景选择。

  2. 垂直拆分适用于业务边界清晰的场景,水平拆分适用于数据量大的场景,两者可以结合使用。

  3. 分片策略的选择至关重要,需要综合考虑访问模式、数据特征、扩展需求等因素。

  4. 分布式事务和跨库查询是主要挑战,需要通过最终一致性、数据冗余、应用层处理等方案解决。

  5. 在线扩容需要谨慎规划,建议采用双写+渐进式迁移的方案,确保业务不受影响。

6.2 实施建议

  1. 不要过早优化:只有在真正遇到性能瓶颈时才考虑分库分表,避免过度设计。

  2. 渐进式实施:可以先进行垂直拆分,再考虑水平拆分,逐步演进。

  3. 充分的测试验证:在测试环境中充分验证分库分表方案,确保功能正确性和性能达标。

  4. 完善的监控体系:建立完善的监控体系,及时发现问题并优化。

  5. 团队技能提升:确保团队具备分库分表相关的技术能力和运维经验。

6.3 常见误区规避

  1. 避免过度分片:分片数量不宜过多,增加管理复杂度。

  2. 不要忽视成本:分库分表会增加硬件成本、开发成本、运维成本。

  3. 避免频繁变更分片策略:一旦确定分片策略,尽量避免频繁变更。

  4. 不要忽视数据一致性:分布式环境下数据一致性更加复杂,需要重点关注。

  5. 避免盲目跟风:不是所有系统都需要分库分表,根据实际需求选择。

相关推荐
AIFQuant4 小时前
如何通过股票数据 API 计算 RSI、MACD 与移动平均线MA
大数据·后端·python·金融·restful
x70x804 小时前
Go中nil的使用
开发语言·后端·golang
REDcker4 小时前
libwebsockets库原理详解
c++·后端·websocket·libwebsockets
源代码•宸5 小时前
Leetcode—47. 全排列 II【中等】
经验分享·后端·算法·leetcode·面试·golang·深度优先
Wyy_9527*5 小时前
行为型设计模式——状态模式
java·spring boot·后端
梅梅绵绵冰5 小时前
springboot初步2
java·spring boot·后端
0和1的舞者7 小时前
公共类的注意事项详细讲解
经验分享·后端·开发·知识·总结
小北方城市网7 小时前
Spring Cloud Gateway 自定义过滤器深度实战:业务埋点、参数校验与响应改写
运维·jvm·数据库·spring boot·后端·mysql
jason.zeng@15022077 小时前
POM构造Spring boot多模块项目
java·spring boot·后端