MySQL按時(shí)間維度對(duì)億級(jí)數(shù)據(jù)表進(jìn)行平滑分表
引言
在互聯(lián)網(wǎng)應(yīng)用快速發(fā)展的今天,數(shù)據(jù)量呈現(xiàn)爆炸式增長(zhǎng)。作為后端開發(fā)者,我們常常會(huì)遇到單表數(shù)據(jù)量過億導(dǎo)致的性能瓶頸問題。本文將以一個(gè)真實(shí)的4億數(shù)據(jù)表分表案例為基礎(chǔ),詳細(xì)介紹如何在不影響線上業(yè)務(wù)的情況下,完成按時(shí)間維度分表的完整過程,包含架構(gòu)設(shè)計(jì)、具體實(shí)施方案、Java代碼適配以及注意事項(xiàng)等全方位內(nèi)容。
一、為什么我們需要分表
1.1 單表數(shù)據(jù)量過大的問題
當(dāng)MySQL單表數(shù)據(jù)量達(dá)到4億級(jí)別時(shí),會(huì)面臨諸多挑戰(zhàn):
- 索引膨脹,B+樹層級(jí)加深,查詢效率下降
- 備份恢復(fù)時(shí)間呈指數(shù)級(jí)增長(zhǎng)
- DDL操作(如加字段、改索引)鎖表時(shí)間不可接受
- 高頻寫入導(dǎo)致鎖競(jìng)爭(zhēng)加劇
1.2 分表方案選型
常見的分表策略有:
- 水平分表 :按行拆分,如按ID范圍、哈希、時(shí)間等
- 垂直分表 :按列拆分,將不常用字段分離
- 分區(qū)表 :MySQL內(nèi)置分區(qū)功能
本文選擇 按時(shí)間水平分表 ,因?yàn)椋?/p>
- 業(yè)務(wù)查詢大多帶有時(shí)間條件
- 天然符合數(shù)據(jù)冷熱特征
- 便于歷史數(shù)據(jù)歸檔
二、分表前的準(zhǔn)備工作
2.1 數(shù)據(jù)評(píng)估分析
-- 分析數(shù)據(jù)時(shí)間分布
SELECT
DATE_FORMAT(create_time, '%Y-%m') AS month,
COUNT(*) AS count
FROM original_table
GROUP BY month
ORDER BY month;
2.2 分表命名規(guī)范設(shè)計(jì)
制定明確的分表命名規(guī)則:
- 主表:
original_table - 月度分表:
original_table_202301 - 年度分表:
original_table_2023 - 歸檔表:
archive_table_2022
2.3 應(yīng)用影響評(píng)估
檢查所有涉及該表的SQL:
- 是否都有時(shí)間條件
- 是否存在跨時(shí)間段的復(fù)雜查詢
- 事務(wù)是否涉及多表關(guān)聯(lián)
三、分表實(shí)施方案詳解
3.1 方案一:平滑遷移方案(推薦)
第一步:創(chuàng)建分表結(jié)構(gòu)
-- 創(chuàng)建2023年1月的分表(結(jié)構(gòu)完全相同) CREATE TABLE original_table_202301 LIKE original_table; -- 為分表添加同樣的索引 ALTER TABLE original_table_202301 ADD INDEX idx_user_id(user_id);
第二步:分批遷移數(shù)據(jù)
使用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); // 控制遷移速度
}
}
}
}
第三步:建立聯(lián)合視圖
CREATE VIEW original_table_unified AS SELECT * FROM original_table_202301 UNION ALL SELECT * FROM original_table_202302 UNION ALL ... SELECT * FROM original_table; -- 當(dāng)前表作為最新數(shù)據(jù)
3.2 方案二:觸發(fā)器過渡方案
對(duì)于不能停機(jī)的關(guān)鍵業(yè)務(wù)表:
-- 創(chuàng)建分表
CREATE TABLE original_table_new LIKE original_table;
-- 創(chuàng)建觸發(fā)器
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應(yīng)用層適配
4.1 動(dòng)態(tài)表名路由
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的表名路由器:
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分表適配
方案一:動(dòng)態(tài)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());
}
方案二:插件攔截(高級(jí))
實(shí)現(xiàn)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();
}
}
五、分表后的運(yùn)維管理
5.1 自動(dòng)建表策略
使用Spring Scheduler實(shí)現(xiàn)每月自動(dòng)建表:
@Scheduled(cron = "0 0 0 1 * ?") // 每月1號(hào)執(zhí)行
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 數(shù)據(jù)歸檔策略
public void archiveOldData(int keepMonths) {
LocalDate archivePoint = LocalDate.now().minusMonths(keepMonths);
String archiveTable = "archive_table_" + archivePoint.getYear();
// 創(chuàng)建歸檔表
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS " + archiveTable +
" LIKE original_table_template");
// 遷移數(shù)據(jù)
jdbcTemplate.update("INSERT INTO " + archiveTable +
" SELECT * FROM original_table WHERE create_time < ?",
archivePoint.atStartOfDay());
// 刪除原數(shù)據(jù)
jdbcTemplate.update("DELETE FROM original_table WHERE create_time < ?",
archivePoint.atStartOfDay());
}
六、踩坑與經(jīng)驗(yàn)總結(jié)
6.1 遇到的典型問題
1.跨分頁(yè)查詢問題 :
解決方案:使用Elasticsearch等中間件預(yù)聚合
2.分布式事務(wù)問題 :
解決方案:避免跨分表事務(wù),或引入Seata等框架
3.全局唯一ID問題 :
解決方案:使用雪花算法(Snowflake)生成ID
6.2 性能對(duì)比數(shù)據(jù)
| 指標(biāo) | 分表前 | 分表后 |
|---|---|---|
| 單條查詢平均耗時(shí) | 320ms | 45ms |
| 批量寫入QPS | 1,200 | 3,500 |
| 備份時(shí)間 | 6小時(shí) | 30分鐘 |
七、未來演進(jìn)方向
- 分庫(kù)分表 :當(dāng)單機(jī)容量達(dá)到瓶頸時(shí)考慮
- TiDB遷移 :對(duì)于超大規(guī)模數(shù)據(jù)考慮NewSQL方案
- 數(shù)據(jù)湖架構(gòu) :將冷數(shù)據(jù)遷移到HDFS等存儲(chǔ)
結(jié)語(yǔ)
MySQL分表是一個(gè)系統(tǒng)工程,需要結(jié)合業(yè)務(wù)特點(diǎn)選擇合適的分片策略。本文介紹的按時(shí)間分表方案,在保證業(yè)務(wù)連續(xù)性的前提下,成功將4億數(shù)據(jù)表的查詢性能提升了7倍。
以上就是MySQL按時(shí)間維度對(duì)億級(jí)數(shù)據(jù)表進(jìn)行平滑分表的詳細(xì)內(nèi)容,更多關(guān)于MySQL分表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- MySQL大表數(shù)據(jù)的分區(qū)與分庫(kù)分表的實(shí)現(xiàn)
- MySQL數(shù)據(jù)庫(kù)分庫(kù)分表的方案
- MySQL數(shù)據(jù)庫(kù)優(yōu)化之分表分庫(kù)操作實(shí)例詳解
- MyBatis實(shí)現(xiàn)Mysql數(shù)據(jù)庫(kù)分庫(kù)分表操作和總結(jié)(推薦)
- MYSQL數(shù)據(jù)庫(kù)數(shù)據(jù)拆分之分庫(kù)分表總結(jié)
- Mysql數(shù)據(jù)庫(kù)分庫(kù)和分表方式(常用)
- 1億條數(shù)據(jù)如何分表100張到Mysql數(shù)據(jù)庫(kù)中(PHP)
相關(guān)文章
為什么說MySQL單表數(shù)據(jù)不要超過500萬行
在本篇文章里小編給大家整理了一篇關(guān)于為什么說MySQL單表數(shù)據(jù)不要超過500萬行的相關(guān)內(nèi)容,有興趣的朋友們閱讀下吧。2019-06-06
MySQL按月自動(dòng)設(shè)置表分區(qū)的實(shí)現(xiàn)
本文主要介紹了MySQL按月自動(dòng)設(shè)置表分區(qū)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
MySql中的IFNULL、NULLIF和ISNULL用法詳解
在做項(xiàng)目中發(fā)現(xiàn)MySql里的isnull和mssql里的有點(diǎn)不同。接下來小編通過本文給大家介紹MySql中的IFNULL、NULLIF和ISNULL用法詳解的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
SQLyog錯(cuò)誤號(hào)碼2058最新解決辦法
這篇文章主要給大家介紹了關(guān)于SQLyog錯(cuò)誤號(hào)碼2058的最新解決辦法,使用sqlyog連接數(shù)據(jù)庫(kù)過程中可能會(huì)出現(xiàn)2058錯(cuò)誤,出現(xiàn)的原因是因?yàn)镸YSQL8.0對(duì)密碼的加密方式進(jìn)行了改變,需要的朋友可以參考下2023-08-08
mysql binlog如何恢復(fù)數(shù)據(jù)到某一時(shí)刻
這篇文章主要介紹了mysql binlog如何恢復(fù)數(shù)據(jù)到某一時(shí)刻問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06
MySQL?數(shù)據(jù)庫(kù)范式化設(shè)計(jì)理論總結(jié)
這篇文章主要介紹了MySQL?數(shù)據(jù)庫(kù)范式設(shè)計(jì)理論總結(jié),數(shù)據(jù)庫(kù)的規(guī)劃化范式設(shè)計(jì),在邏輯結(jié)構(gòu)上可以讓結(jié)構(gòu)更加細(xì)粒度,容易理解,下文我們就來了解具體的內(nèi)容介紹吧2022-04-04

