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

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),会导致全库扫描

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

相关推荐
绝无仅有3 小时前
前端开发环境搭建:从安装 Node 到成功运行代码
后端·面试·github
绝无仅有4 小时前
某个互联网大厂的Elasticsearch基础面试题与答案
后端·面试·github
倔强青铜三4 小时前
最强Python Web框架到底是谁?
人工智能·python·面试
UrbanJazzerati4 小时前
可拖拽的进度条组件实战:实现思路与Demo
前端·面试
倔强青铜三4 小时前
苦练Python第45天:使用open函数读取文件内容
人工智能·python·面试
GHOME4 小时前
原型链的原貌
前端·javascript·面试
倔强青铜三4 小时前
苦练Python第43天:datetime和calendar模块的使用
人工智能·python·面试
倔强青铜三4 小时前
苦练Python第44天:math、random、statistics三剑客,带你秒杀数学计算与数据分析
人工智能·python·面试
bug_kada4 小时前
Flex布局/弹性布局(面试篇)
前端·面试
种子q_q4 小时前
Redis的三种典型的 “缓存失效” 问题
后端·面试