面试官:谈谈为什么要拆分数据库?有哪些方法?

"单表5000万数据,查询从0.1秒到10秒------我们为什么要拆数据库?"

一次数据库架构演进的完整复盘

面试官:谈谈 为什么要拆分数据库?

单体项目在构建之初,数据库的负载和数据量都不大,所以不需要对数据库做拆分,小型财务系统、文书系统、ERP系统、OA系统,用一个MySQL数据库实例基本就够用了。

就像《淘宝技术这十年》里面说到的,电商业务的数据量增长飞快,所以最开始的PHP+MySQL的架构已经不能满足实际要求了,于是淘宝想到的第一个办法就是把MySQL替换成Oracle。但是没过了多久,在08年前后,单节点的Oracle数据库也不好用了,于是淘宝终于告别了单节点数据库,开始拆分数据库。从一个节点,变成多个节点。

拆分数据库是有讲究的,比如说拆分方法有两种:垂直切分和水平切分。那你是先水平切分还是垂直切分呢?顺序无所谓?不,顺序有所谓,次序绝对不能错:先水平切分,然后垂直切分。

为什么要拆分数据库?五大核心原因

1. 性能瓶颈:单机性能到达天花板

真实数据

  • 单表数据量超过5000万,索引效率急剧下降
  • 数据库连接数达到上限(默认151个)
  • 磁盘IO成为瓶颈,查询延迟飙升

sql

sql 复制代码
-- 拆分前:单表6000万数据
SELECT * FROM orders WHERE user_id = 123456 ORDER BY create_time DESC LIMIT 20;
-- 执行时间:8.2秒

-- 拆分后:单表50万数据  
SELECT * FROM orders_01 WHERE user_id = 123456 ORDER BY create_time DESC LIMIT 20;
-- 执行时间:0.05秒

2. 可用性风险:单点故障的致命威胁

事故场景

  • 数据库服务器硬件故障 → 全站不可用
  • MySQL bug导致crash → 所有业务停摆
  • 维护操作(如ALTER TABLE)→ 长时间锁表

3. 团队协作:数据库成为开发瓶颈

团队痛点

  • 多个团队共用同一个数据库
  • 表结构变更需要多方协调
  • 一个团队的慢SQL影响所有业务

4. 业务复杂度:不同业务的不同需求

需求差异

  • 订单业务:高并发写入,强一致性
  • 报表业务:复杂查询,弱一致性
  • 用户业务:读多写少,高可用性

5. 成本控制:硬件成本的指数增长

成本对比

  • 单机高端服务器:50万/台
  • 多台普通服务器:10万/台 × 5台 = 50万
  • 但性能提升:5倍以上,且具备横向扩展能力

数据库拆分的六大方法

方法一:垂直分库(按业务拆分)

核心思想:不同业务使用不同数据库

拆分前架构

text 复制代码
单一数据库:shop_db
├── 用户表:users
├── 商品表:products  
├── 订单表:orders
├── 支付表:payments
└── 日志表:logs

拆分后架构

java 复制代码
// 用户库:user_db
@Component
@DataSource("userDataSource")
public class UserMapper {
    // 用户相关表:users, user_profile, user_address
}

// 商品库:product_db  
@Component
@DataSource("productDataSource")
public class ProductMapper {
    // 商品相关表:products, categories, inventory
}

// 订单库:order_db
@Component
@DataSource("orderDataSource") 
public class OrderMapper {
    // 订单相关表:orders, order_items, order_logs
}

配置示例

yaml 复制代码
spring:
  datasource:
    user:
      url: jdbc:mysql://user-db:3306/user_db
    product:
      url: jdbc:mysql://product-db:3306/product_db  
    order:
      url: jdbc:mysql://order-db:3306/order_db

方法二:水平分库(按数据分片)

核心思想:相同业务,不同数据分布在不同数据库

分片策略

java 复制代码
public class DatabaseShardingStrategy {
    
    // 按用户ID分库:user_id % 4
    public String getDataSourceName(Long userId) {
        int dbIndex = userId % 4;  // 分4个库
        return "order_db_" + dbIndex;
    }
    
    // 按时间分库:每月一个库
    public String getDataSourceName(Date createTime) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM");
        return "order_db_" + sdf.format(createTime);
    }
}

方法三:垂直分表(按字段拆分)

拆分场景:表字段过多,冷热数据分离

拆分前

sql 复制代码
-- 用户表(40+字段)
CREATE TABLE users (
    id BIGINT,
    username VARCHAR(50),
    password VARCHAR(100),
    -- ... 20个常用字段
    last_login_ip VARCHAR(45),
    last_login_time DATETIME,
    -- ... 20个不常用字段
    personal_signature TEXT,
    background_image VARCHAR(200)
);

拆分后

sql 复制代码
-- 热表:频繁查询的字段
CREATE TABLE users_hot (
    id BIGINT,
    username VARCHAR(50),
    password VARCHAR(100),
    email VARCHAR(100),
    phone VARCHAR(20),
    -- ... 其他常用字段
    PRIMARY KEY (id)
);

-- 冷表:不常查询的字段  
CREATE TABLE users_cold (
    user_id BIGINT,
    personal_signature TEXT,
    background_image VARCHAR(200),
    register_source VARCHAR(50),
    -- ... 其他不常用字段
    PRIMARY KEY (user_id)
);

方法四:水平分表(按数据行拆分)

核心思想:大表拆小表,减少单表数据量

分表策略

java 复制代码
public class TableShardingStrategy {
    
    // 按用户ID分表:user_id % 16
    public String getTableName(Long userId, String logicTable) {
        int tableIndex = userId % 16;  // 分16张表
        return logicTable + "_" + tableIndex;
    }
    
    // 按时间分表:每月一张表
    public String getTableName(Date createTime, String logicTable) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MM");
        return logicTable + "_" + sdf.format(createTime);
    }
}

SQL改写

sql 复制代码
-- 拆分前:
SELECT * FROM orders WHERE user_id = 123456;

-- 拆分后(需要应用层改写):
SELECT * FROM orders_8 WHERE user_id = 123456;  -- 123456 % 16 = 8

方法五:读写分离

核心思想:写操作走主库,读操作走从库

架构设计

java 复制代码
@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource routingDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave1", slave1DataSource());
        targetDataSources.put("slave2", slave2DataSource());
        
        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        return routingDataSource;
    }
}

// 通过注解控制数据源
@Service
public class OrderService {
    
    @ReadOnly  // 读从库
    public Order getOrder(Long orderId) {
        return orderMapper.selectById(orderId);
    }
    
    // 默认写主库
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
}

方法六:混合拆分策略

真实案例:大型电商平台架构

java 复制代码
public class HybridShardingStrategy {
    
    public ShardingResult resolve(String logicTable, Long userId, Date createTime) {
        // 1. 先按业务垂直分库
        String dbName = "order_db";
        
        // 2. 再按用户ID水平分库
        int dbShard = userId % 8;  // 分8个库
        dbName = dbName + "_" + dbShard;
        
        // 3. 最后按时间水平分表
        String tableName = logicTable + "_" + getMonthSuffix(createTime);
        
        return new ShardingResult(dbName, tableName);
    }
}

拆分带来的挑战与解决方案

挑战一:分布式事务

解决方案

java 复制代码
// 使用Seata分布式事务框架
@GlobalTransactional
public void createOrder(Order order) {
    // 1. 扣减库存(库存库)
    inventoryService.deduct(order);
    
    // 2. 创建订单(订单库)
    orderService.create(order);
    
    // 3. 扣减余额(用户库)
    accountService.deductBalance(order);
}

挑战二:跨库查询

解决方案

java 复制代码
// 1. 字段冗余
public class OrderVO {
    private Long orderId;
    private Long userId;
    private String userName;  // 冗余用户名字段
    private String productName; // 冗余商品名字段
}

// 2. 使用Elasticsearch构建查询视图
@Component
public class OrderSearchService {
    public List<OrderVO> searchOrders(OrderQuery query) {
        // 从ES查询,避免跨库JOIN
        return elasticsearchTemplate.query(query);
    }
}

挑战三:全局唯一ID

解决方案

java 复制代码
// 雪花算法生成分布式ID
@Component
public class SnowflakeIdGenerator {
    
    public Long nextId() {
        // 时间戳 + 机器ID + 序列号
        return snowflake.nextId();
    }
}

// 数据库号段模式
@Service
public class SegmentIdGenerator {
    
    public Long nextId(String businessType) {
        // 从数据库获取号段,内存中分配
        return idSegmentService.getNextSegment(businessType);
    }
}

拆分实施路线图

阶段一:准备期(1个月)

  1. 数据评估:分析数据量、增长趋势、访问模式
  2. 架构设计:确定拆分方案、分片键、路由规则
  3. 技术选型:选择中间件(ShardingSphere、MyCat等)

阶段二:实施期(2个月)

  1. 数据迁移:双写方案,逐步迁移
  2. 应用改造:DAO层重构,SQL改写
  3. 测试验证:功能测试、性能测试、数据一致性验证

阶段三:优化期(持续)

  1. 监控告警:慢查询、数据分布、连接数监控
  2. 弹性扩容:动态添加分片,在线数据迁移
  3. 故障演练:模拟节点故障,验证高可用性

什么时候应该考虑拆分?

立即拆分(红色警报):

  • 单表数据量 > 5000万,查询性能明显下降
  • 数据库服务器CPU持续 > 80%
  • 频繁出现锁等待超时

规划拆分(黄色预警):

  • 单表数据量 > 2000万,且每月增长 > 100万
  • 业务快速发展,预计半年内达到性能瓶颈
  • 团队规模扩大,需要解耦协作

暂不拆分(绿色安全):

  • 单表数据量 < 1000万
  • 业务稳定,无显著增长
  • 团队规模小,协作顺畅

经验总结

成功关键:

  1. 循序渐进:从读写分离开始,再到垂直拆分,最后水平拆分
  2. 数据驱动:基于真实数据做决策,不要过早优化
  3. 工具支撑:使用成熟的中间件,不要重复造轮子
  4. 监控先行:建立完善的监控体系,提前发现问题

失败教训:

  1. 分片键选择不当:导致数据倾斜,热点问题
  2. 过度拆分:增加系统复杂度,维护成本高
  3. 准备不足:数据迁移过程中出现数据不一致

思考题:在你的项目中,如果订单表需要同时支持按用户查询和按商家查询,分片键应该怎么设计?欢迎在评论区分享你的架构方案!

相关推荐
用户298698530142 小时前
C# 中 Excel 工作表打印前页面边距的设置方法
后端·.net
自由的疯2 小时前
java DWG文件转图片
java·后端·架构
小兔崽子去哪了2 小时前
EasyExcel 使用
java·excel
青云交2 小时前
Java 大视界 -- Java 大数据机器学习模型的对抗攻击与防御技术研究
java·机器学习模型·对抗攻击·java 大数据·防御技术·对抗训练·i - fgsm
ServBay3 小时前
Rust 1.89更新,有哪些值得关注的新功能
后端·rust
程序员小假3 小时前
请介绍类加载过程,什么是双亲委派模型?
java·后端
汤姆yu3 小时前
基于springboot的家具商城销售系统
java·spring boot·后端
Rexi3 小时前
Go的代码组织结构
后端