分库分表:数据爆炸时的计划生育政策

6.3 分库分表:数据爆炸时的计划生育政策

背景故事

当你的数据库变成"超生游击队"(单表数据超过千万级),查询慢得像挤地铁,这时候就需要"分库分表"------用"计划生育政策"给数据人口做精准管控!

6.3.1 什么是分库分表?

  • 分库:把用户表、订单表等拆到不同数据库,就像把"超大城市"拆成"省-市-区"三级行政区
  • 分表 :把单表拆成多张物理表,例如用户表按user_id拆成user_0~user_9,就像把"万人学校"拆成10个"千人分校"
graph LR A[原始数据库] -->|分库| B[用户库] --> C[用户_0表] A --> D[订单库] --> E[订单_0表] A --> F[日志库] --> G[日志_0表] subgraph 分库后 B D F end

6.3.2 实现方式 & 代码示例

场景 :电商系统用户表数据爆炸,需按user_id分库分表

方案1:数据库分库(按用户ID哈希)
java 复制代码
// 配置多个数据源(假设分3个库)
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource userDataSource() {
        return new HikariDataSource(
            Map.of("jdbcUrl", "jdbc:h2:mem:user_db",
                   "username", "sa",
                   "password", ""));
    }

    @Bean
    public DataSource orderDataSource() {
        return new HikariDataSource(
            Map.of("jdbcUrl", "jdbc:h2:mem:order_db",
                   "username", "sa",
                   "password", ""));
    }
}

// 分库策略:根据user_id选择数据源
public class DynamicDataSource {
    private static final Map<Object, Object> DATA_SOURCES = 
        new HashMap<>();
    static {
        DATA_SOURCES.put("user", userDataSource());
        DATA_SOURCES.put("order", orderDataSource());
    }

    public DataSource determineTargetDataSource() {
        // 假设当前操作是用户表,返回对应库
        return DATA_SOURCES.get("user");
    }
}
方案2:分表(按时间范围)
java 复制代码
// MyBatis分表插件示例
public class TimeBasedShardingAlgorithm 
        implements ShardingAlgorithm<Date> {
    @Override
    public String doSharding(
        Collection<String> availableTargetNames, 
        ShardingValue<Date> shardingValue) {
        Date date = shardingValue.getValue();
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        int month = cal.get(Calendar.MONTH);
        return String.format("order_%d", month);
    }
}
方案3:混合策略(用户ID哈希+时间分片)
java 复制代码
// 用户表按ID哈希分库,订单表按时间分表
public class HybridShardingAlgorithm 
        implements ShardingAlgorithm<Long> {
    @Override
    public String doSharding(
        Collection<String> availableTargetNames, 
        ShardingValue<Long> shardingValue) {
        long userId = shardingValue.getValue();
        int shard = (int) (userId % 3); // 分3个库
        return String.format("user_%d", shard);
    }
}

6.3.3 常见面试题 & 答案

Q1:分库分表的优缺点是什么?

  • 优点
    • 解决单库单表性能瓶颈(类比"分流交通")
    • 数据分布均匀,避免热点(比如用户ID按哈希分库)
  • 缺点
    • 跨库JOIN操作困难(需要应用层处理)
    • 分布式事务复杂度上升(需用TCC/Seata等方案)

Q2:常见的分表策略有哪些?如何选择?

  • 哈希分表用户表按userId % N,适合数据均匀分布
  • 范围分表订单表按createTime分表,适合时间序列数据
  • 字段取模商品表按商品类别分表,业务场景明确时使用

Q3:分库分表后如何生成唯一主键?

  • 方案对比

    | 方案 | 适用场景 | 示例代码 |
    |-----------|---------|-----------------------------------------------------------------------------|-------------------|-------------|
    | UUID | 无序但足够唯一 | String uuid = UUID.randomUUID().toString(); |
    | Snowflake | 高并发有序ID | java long id = IdWorker.nextId(); |
    | 数据库自增ID | 单表场景 | @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; |
    | 分库分表ID混合 | 复杂分布式场景 | `long dbId = 1; long tableId = 2; long sequence = 100; id = (dbId << 22) | (tableId << 12) | sequence;` |

Q4:分库分表后如何处理跨表JOIN查询?

  • 解决方案
    1. 应用层合并:分步查询后在代码中关联(适合小数据量)
    2. 冗余字段:在订单表中冗余用户名称字段(避免JOIN)
    3. ES搜索引擎:通过ES实现跨库检索(适合复杂查询)

Q5:分库分表后如何保证事务一致性?

  • 方案选择
    • 最终一致性:消息队列+补偿机制(适合低频关键操作)

    • 分布式事务框架

      java 复制代码
      // 使用Seata实现分布式事务
      @GlobalTransactional
      public void placeOrder() {
          // 操作用户库
          userMapper.updateBalance(userId);
          // 操作订单库
          orderMapper.insert(order);
      }

Q6:分库分表后如何处理数据倾斜问题?

  • 解决办法
    1. 哈希算法优化 :避免简单取模导致的热点(比如用userId.hashCode() % N
    2. 动态扩容:支持水平扩展新分片(比如从3个分库扩容到5个)
    3. 数据迁移:定期将热点数据迁移到新分片

6.3.4 分库分表的致命伤 & 解决方案

问题:跨库统计用户总消费金额

sql 复制代码
-- 原SQL(无法直接执行)
SELECT SUM(amount) 
FROM orders 
JOIN users ON orders.user_id = users.id 
WHERE users.age > 25;

解决方案

  1. 应用层聚合
java 复制代码
// 分别查询各分库数据再汇总
List<BigDecimal> amounts = new ArrayList<>();
for (DataSource ds : allDataSources) {
    String sql = "SELECT SUM(amount) FROM orders WHERE ...";
    amounts.add(ds.query(sql));
}
BigDecimal total = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
  1. ES索引辅助
json 复制代码
// 在ES中建立用户+订单联合索引
{
  "mappings": {
    "properties": {
      "user_id": {"type": "keyword"},
      "total_amount": {"type": "double"}
    }
  }
}

课程彩蛋

  • 分库分表工具推荐:ShardingSphere(阿里开源方案)
  • 避坑指南 :分库后不要在WHERE子句中使用user_id IN (1,2,3),会导致全库扫描

通过以上内容,面试官可能会问:"如果让你设计一个亿级用户系统的分库分表方案,你会怎么设计?"------现在你已经准备好了!

相关推荐
Anlici24 分钟前
面试官:想把你问趴下 => 面题整理[3] 😮‍💨初心未变🚀
javascript·面试·前端框架
独行soc2 小时前
2025年渗透测试面试题总结-长某亭科技-安全服务工程师(一面)(题目+回答)
科技·安全·面试·职场和发展·红蓝攻防·护网·2025
少年姜太公5 小时前
让你快速拿捏大厂面试中关于eventloop执行顺序问题
前端·javascript·面试
六个点12 小时前
面试中的网络协议
前端·网络协议·面试
周八营业的代码人15 小时前
Go常见面试题整理
面试·golang
蛇皮划水怪16 小时前
代码随想录-图论
后端·算法·面试
勤劳打代码16 小时前
烽火连营——爆杀 Jank 闪烁卡顿
flutter·面试·性能优化
低飞的蜜蜂16 小时前
场景题: 使用 redis 实现某抽奖活动人维度抽样逻辑
算法·面试
蛇皮划水怪16 小时前
代码随想录-动态规划-子序列
后端·面试
怒放吧德德16 小时前
JUC从实战到源码:原子类全解析-从基础到应用
java·后端·面试