开发者

MySQL按时间维度对亿级数据表进行平滑分表

目录
  • 引言
  • 一、为什么我们需要分表
    • 1.1 单表数据量过大的问题
    • 1.2 分表方案选型
  • 二、分表前的准备工作
    • 2.1 数据评估分析
    • 2.2 分表命名规范设计
    • 2.3 应用影响评估
  • 三、分表实施方案android详解
    • 3.1 方案一:平滑迁移方案(推荐)
    • 3.2 方案二:触发器过渡方案
  • 四、Java应用层适配
    • 4.1 动态表名路由
    • 4.2 MyBATis分表适配
  • 五、分表后的运维管理
    • 5.1 自动建表策略
    • 5.2 数据归档策略
  • 六、踩坑与经验总结
    • 6.1 遇到的典型问题
    • 6.2 性能对比数据
  • 七、未来演进方向
    • 结语

      引言

      在互联网应用快速发展的今天,数据量呈现爆炸式增长。作为后端开发者,我们常常会遇到单表数据量过亿导致的性能瓶颈问题。本文将以一个真实的4亿数据表分表案例为基础,详细介绍如何在不影响线上业务的情况下,完成按时间维度分表的完整过程,包含架构设计、具体实施方案、Java代码适配以及注意事项等全方位内容。

      一、为什么我们需要分表

      1.1 单表数据量过大的问题

      当mysql单表数据量达到4亿级别时,会面临诸多挑战:

      • 索引膨胀,B+树层级加深,查询效率下降
      • 备份恢复时间呈指数级增长
      • DDL操作(如加字段、改索引)锁表时间不可接受
      • 高频写入导致锁竞争加剧

      1.2 分表方案选型

      常见的分表策略有:

      1. 水平分表 :按行拆分,如按ID范围、哈希、时间等
      2. 垂直分表 :按列拆分,将不常用字段分离
      3. 分区表 :MySQL内置分区功能

      本文选择 按时间水平分表 ,因为:

      • 业务查询大多带有时间条件
      • 天然符合数据冷热特征
      • 便于历史数据归档

      二、分表前的准备工作

      2.1 数据评估分析

      -- 分析数据时间分布
      SELECT 
          DATE_FORMAT(create_time, '%Y-%m') AS month,
          COUNT(*) AS count
      FROM original_table
      GROUP BY month
      ORDER BY month;
      

      2.2 分表命名规范设计

      制定明确的分表命名规则:

      • 主表:original_table
      • 月度分表:original_table_202301
      • 年度分表:original_table_2023
      • 归档表:archive_table_2022

      2.3 应用影响评估

      检查所有涉及该表的SQL:

      • 是否都有时间条件
      • 是否存在跨时间段的复杂查询
      • 事务是否涉及多表vfbXeLIp关联

      三、分表实施方案详解

      3.1 方案一:平滑迁移方案(推荐)

      第一步:创建分表结构

      -- 创建2023年1月的分表(结构完全相同)
      CREATE TABLE original_table_202301 LIKE original_table;
      
      -- 为分表编程客栈添加同样的索引
      ALTER TABLE original_table_202301 ADD INDEX idx_user_id(user_id);
      

      第二步:分批迁移数据

      使用Java编写迁移工具:

      public class DataMigrator {
          private static final int BATCH_SIZE = 5000;
          
          public void migrateByMonth(String month) throws SQLException {
              String sourceTable = "original_table";
              String targetTable = "original_table_" + month;
              
              try (Connection conn = dataSource.getConnection()) {
                  long maxId = getMaxId(conn, sourceTable);
                  long currentId = 0;
                  
                  while (currentId < maxId) {
                      String sql = String.format(
                          "INSERT INTO %s SELECT * FROM %s " +
                          "WHERE create_time BETWEEN '%s-01' AND '%s-31' " +
                          "AND id > %d ORDER BY id LIMIT %d",
                          targetTable, sourceTable, month, month, currentId, BATCH_SIZE);
                      
                      try (Statement stmt = conn.createStatement()) {
                          stmt.executeUpdate(sql);
                          currentId = getLastInsertedId(conn, targetTable);
                      }
                      
                      Thread.sleep(100); // 控制迁移速度
                  }
              }
          }
      }
      

      第三步:建立联合视图

      CREATE VIEW original_table_unified AS
      SELECT * FROM original_table_202301 UNION ALL
      SELECT * FROM original_table_202302 UNION ALL
      ...
      SELECT * FROM original_table; -- 当前表作为最新数据
      

      3.2 方案二:触发器过渡方案

      对于不能停机的关键业务表:

      -- 创建分表
      CREATE TABLE original_table_new LIKE original_table;
      
      -- 创建触发器
      DELIMITER //
      CREATE TRIGGER tri_original_table_insert
      AFTER INSERT ON original_table
      FOR EACH ROW
      BEGIN
          IF NEW.create_time >= '2023-01-01' THEN
              INSERT INTO original_table_new VALUES (NEW.*);
          END IF;
      END//
      DELIMITER ;
      

      四、Java应用层适配

      4.1 动态表名路由

      实现http://www.devze.com一个简单的表名路由器:

      public class TableRouter {
          private static final DateTimeFormatter MONTH_FORMAT = 
              DateTimeFormatter.ofPattern("yyyyMM");
          
          public static String routeTable(LocalDateTime createTime) {
              String month = createTime.format(MONTH_FORMAT);
              return "original_table_" + month;
          }
      }
      

      4.2 MyBatis分表适配

      方案一:动态SQL

      <select id="queryByTime" resultType="com.example.Entity">
          SELECT * FROM ${tableName}
          WHERE user_id = #{userId}
          AND create_time BETWEEN #{start} AND #{end}
      </select>
      
      public List<Entity> queryByTime(Long userId, LocalDate start, LocalDate end) {
          List<String> tableNames = getTableNamesBetween(start, end);
          return tableNames.stream()
              .flatMap(table -> mapper.queryByTime(table, userId, start, end).stream())
              .collect(Collectors.toList());
      }
      

      方案二:插件拦截(高级)

      实现MyBatis的Interceptor接口:

      @Intercepts(@Signature(type= StatementHandler.class, 
              method="prepare", args={Connection.class, Integer.class}))
      public class TableShardInterceptor implements Interceptor {
          
          @Override
          public Object intercept(Invocation invocation) throws Throwable {
              BoundSql boundSql = ((StatementHandler)invocation.getTarget()).getBoundSql();
              String originalSql = boundSql.getSql();
              
              if (originalSql.contains("original_table")) {
                  Object param = boundSql.getParameterObject();
                  LocalDateTime createTime = getCreateTime(param);
                  String newSql = originalSql.replace("original_table", 
                      "original_table_" + createTime.format(MONTH_FORMAT));
                  
                  resetSql(invocation, newSql);
              }
              
              return invocation.proceed();
          }
      }
      

      五、分表后的运维管理

      5.1 自动建表策略

      使用Spring Scheduler实现每月自动建表:

      @Scheduled(cron = "0 0 0 1 * ?") // 每月1号执行
      public void autoCreateNextMonthtable() {
          LocalDate nextMonth = LocalDate.now().plusMonths(1);
          String tableName = "original_table_" + nextMonth.format(MONTH_FORMAT);
          
          jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS " + tableName + 
              " LIKE original_table_template");
      }
      

      5.2 数据归档策略

      public void archiveOldData(int keepMonths) {
          LocalDate archivePoint = LocalDate.now().minusMonths(keepMonths);
      php    String archiveTable = "archive_table_" + archivePoint.getYear();
          
          // 创建归档表
          jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS " + archiveTable + 
              " LIKE original_table_template");
          
          // 迁移数据
          jdbcTemplate.update("INSERT INTO " + archiveTable + 
              " SELECT * FROM original_table WHERE create_time < ?", 
              archivePoint.atStartOfDay());
          
          // 删除原数据
          jdbcTemplate.update("DELETE FROM original_table WHERE create_time < ?", 
              archivePoint.atStartOfDay());
      }
      

      六、踩坑与经验总结

      6.1 遇到的典型问题

      1.跨分页查询问题 :

      解决方案:使用Elasticsearch等中间件预聚合

      2.分布式事务问题 :

      解决方案:避免跨分表事务,或引入Seata等框架

      3.全局唯一ID问题 :

      解决方案:使用雪花算法(Snowflake)生成ID

      6.2 性能对比数据

      指标分表前分表后
      单条查询平均耗时320ms45ms
      批量写入QPS1,2003,500
      备份时间6小时30分钟

      七、未来演进方向

      • 分库分表 :当单机容量达到瓶颈时考虑
      • TiDB迁移 :对于超大规模数据考虑NewSQL方案
      • 数据湖架构 :将冷数据迁移到HDFS等存储

      结语

      MySQL分表是一个系统工程,需要结合业务特点选择合适的分片策略。本文介绍的按时间分表方案,在保证业务连续性的前提下,成功将4亿数据表的查询性能提升了7倍。

      以上就是MySQL按时间维度对亿级数据表进行平滑分表的详细内容,更多关于MySQL分表的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新数据库

      数据库排行榜