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

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

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

相关推荐
wxweven2 小时前
校招面试官揭秘:我们到底在寻找什么样的技术人才?
java·面试·校招
聪明的笨猪猪4 小时前
Java Redis “缓存设计”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
聪明的笨猪猪5 小时前
Java Redis “运维”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
苏打水com6 小时前
JavaScript 面试题标准答案模板(对应前文核心考点)
javascript·面试
南北是北北10 小时前
JetPack WorkManager
面试
uhakadotcom10 小时前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
想想就想想10 小时前
线程池执行流程详解
面试
程序员清风11 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
南北是北北12 小时前
JetPack ViewBinding
面试
南北是北北12 小时前
jetpack ViewModel
面试