> 技术文档 > 《MySQL 亿级数据表平滑分表实践:基于时间分片的架构演进》

《MySQL 亿级数据表平滑分表实践:基于时间分片的架构演进》


个人名片
在这里插入图片描述
🎓作者简介:java领域优质创作者
🌐个人主页:码农阿豪
📞工作室:新空间代码工作室(提供各种软件服务)
💌个人邮箱:[2435024119@qq.com]
📱个人微信:15279484656
🌐个人导航网站:www.forff.top
💡座右铭:总有人要赢。为什么不能是我呢?

  • 专栏导航:

码农阿豪系列专栏导航
面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️
Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻
Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡
全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀

目录

  • 《MySQL 亿级数据表平滑分表实践:基于时间分片的架构演进》
    • 引言
    • 一、为什么我们需要分表?
      • 1.1 单表数据量过大的问题
      • 1.2 分表方案选型
    • 二、分表前的准备工作
      • 2.1 数据评估分析
      • 2.2 分表命名规范设计
      • 2.3 应用影响评估
    • 三、分表实施方案详解
      • 3.1 方案一:平滑迁移方案(推荐)
        • 第一步:创建分表结构
        • 第二步:分批迁移数据
        • 第三步:建立联合视图
      • 3.2 方案二:触发器过渡方案
    • 四、Java应用层适配
      • 4.1 动态表名路由
      • 4.2 MyBatis分表适配
        • 方案一:动态SQL
        • 方案二:插件拦截(高级)
    • 五、分表后的运维管理
      • 5.1 自动建表策略
      • 5.2 数据归档策略
    • 六、踩坑与经验总结
      • 6.1 遇到的典型问题
      • 6.2 性能对比数据
    • 七、未来演进方向
    • 结语

《MySQL 亿级数据表平滑分表实践:基于时间分片的架构演进》

引言

在互联网应用快速发展的今天,数据量呈现爆炸式增长。作为后端开发者,我们常常会遇到单表数据量过亿导致的性能瓶颈问题。本文将以一个真实的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 countFROM original_tableGROUP BY monthORDER BY month;

2.2 分表命名规范设计

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

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

2.3 应用影响评估

检查所有涉及该表的SQL:

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

三、分表实施方案详解

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 ASSELECT * FROM original_table_202301 UNION ALLSELECT * 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_insertAFTER INSERT ON original_tableFOR EACH ROWBEGIN IF NEW.create_time >= \'2023-01-01\' THEN INSERT INTO original_table_new VALUES (NEW.*); END IF;END//DELIMITER ;

四、Java应用层适配

4.1 动态表名路由

实现一个简单的表名路由器:

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); 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等中间件预聚合
  1. 分布式事务问题 :
  • 解决方案:避免跨分表事务,或引入Seata等框架
  1. 全局唯一ID问题 :
  • 解决方案:使用雪花算法(Snowflake)生成ID

6.2 性能对比数据

指标 分表前 分表后 单条查询平均耗时 320ms 45ms 批量写入QPS 1,200 3,500 备份时间 6小时 30分钟

七、未来演进方向

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

结语

MySQL分表是一个系统工程,需要结合业务特点选择合适的分片策略。本文介绍的按时间分表方案,在保证业务连续性的前提下,成功将4亿数据表的查询性能提升了7倍。希望这篇实践总结能为面临类似问题的开发者提供有价值的参考。

作者提示:任何架构改造都要先在测试环境充分验证,并准备好回滚方案!