1. 项目背景与核心需求
最近在维护一个用户行为日志系统时遇到了存储空间告急的问题。这个每天产生近200万条记录的MySQL 8.0数据库,三个月就积累了近2亿条数据,查询性能开始明显下降。经过分析发现,90%的业务查询都只涉及最近30天的数据,那些早期的历史数据就像衣柜里多年不穿的衣服——既占空间又影响使用效率。
定时删除过期数据的需求应运而生。不同于简单的DELETE语句,我们需要一个可靠的自动化方案来解决几个关键问题:如何在不影响线上业务的情况下高效删除数据?删除操作如何避免产生过多的undo日志?被删除的空间如何真正回收?这就是今天要分享的MySQL 8.0定时删除数据实战方案。
2. 技术方案选型与对比
2.1 常见删除方案对比
在MySQL中删除历史数据主要有三种技术路线:
-
直接DELETE语句
- 优点:语法简单直观
- 缺点:产生大量undo日志,删除大表时会长时间锁表
-
分区表+DROP PARTITION
- 优点:元数据操作速度快
- 缺点:需要预先规划分区策略
-
临时表+数据迁移
- 优点:对业务影响最小
- 缺点:实现复杂度高
我们最终选择了分区表+事件调度的组合方案,这是综合考虑了实现难度、执行效率和业务影响后的最优解。特别是MySQL 8.0对分区表的优化(如直方图统计信息、并行扫描等)让这个方案更具吸引力。
2.2 为什么选择分区表?
分区表的核心优势在于删除数据时实际上是删除整个分区(物理文件级别的删除),这种操作:
- 几乎不产生undo日志
- 是DDL操作而非DML,执行速度快
- 可以精确控制每个分区的数据时间范围
实测显示,对一个包含5000万记录的分区执行DROP PARTITION只需要0.3秒,而用DELETE删除同样数据量需要近20分钟。
3. 详细实现步骤
3.1 分区表设计与创建
假设我们的日志表原始结构如下:
sql复制CREATE TABLE user_behavior_log (
id BIGINT AUTO_INCREMENT,
user_id INT NOT NULL,
action VARCHAR(50) NOT NULL,
device_info JSON,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, created_at)
) ENGINE=InnoDB;
改造为按周分区的分区表:
sql复制CREATE TABLE user_behavior_log_partitioned (
id BIGINT AUTO_INCREMENT,
user_id INT NOT NULL,
action VARCHAR(50) NOT NULL,
device_info JSON,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, created_at)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
关键点:主键必须包含分区字段(这里是created_at),否则会报错"A PRIMARY KEY must include all columns in the table's partitioning function"
3.2 自动分区维护策略
通过事件调度实现自动分区管理:
sql复制DELIMITER //
CREATE EVENT auto_manage_partitions
ON SCHEDULE EVERY 1 WEEK
STARTS CURRENT_TIMESTAMP
DO
BEGIN
-- 添加新分区
SET @next_week = DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 8 DAY), '%Y%m%d');
SET @sql = CONCAT('ALTER TABLE user_behavior_log_partitioned ADD PARTITION (PARTITION p',
DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 WEEK), '%Y%m%d'),
' VALUES LESS THAN (TO_DAYS(''', @next_week, ''')))');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 删除过期分区(保留最近8周数据)
SET @old_partition = (SELECT PARTITION_NAME
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME = 'user_behavior_log_partitioned'
AND PARTITION_NAME != 'pmax'
ORDER BY PARTITION_ORDINAL_POSITION
LIMIT 1);
SET @sql = CONCAT('ALTER TABLE user_behavior_log_partitioned DROP PARTITION ', @old_partition);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
3.3 数据迁移方案
对于已有数据的表,推荐使用pt-online-schema-change工具进行在线迁移:
bash复制pt-online-schema-change \
--alter "ENGINE=InnoDB PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),
PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),
PARTITION pmax VALUES LESS THAN MAXVALUE
)" \
D=database,t=user_behavior_log \
--execute
4. 性能优化关键点
4.1 分区粒度选择
分区粒度的选择需要平衡管理开销和查询效率:
- 按天分区:适合超大规模数据(日增千万级),但分区数量过多会影响元数据管理
- 按周分区:适合日增百万级数据,是大多数场景的最佳选择
- 按月分区:适合数据增长较慢的场景
我们选择按周分区是因为:
- 每天约200万数据,一周约1400万,每个分区大小适中
- 业务查询通常按周统计,符合查询模式
- 保持分区数量在100个以内(保留近2年数据)
4.2 分区维护时间窗口
分区维护操作应避开业务高峰时段。通过修改事件调度:
sql复制ALTER EVENT auto_manage_partitions
ON SCHEDULE EVERY 1 WEEK
STARTS '2023-08-01 02:00:00'
...
5. 监控与异常处理
5.1 分区状态监控
创建监控视图检查分区状态:
sql复制CREATE VIEW partition_monitor AS
SELECT
TABLE_NAME,
PARTITION_NAME,
PARTITION_DESCRIPTION,
TABLE_ROWS,
DATA_LENGTH/1024/1024 AS size_mb
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_NAME = 'user_behavior_log_partitioned';
5.2 常见问题处理
问题1:事件调度未执行
- 检查事件调度器是否开启:
SHOW VARIABLES LIKE 'event_scheduler'; - 开启调度器:
SET GLOBAL event_scheduler = ON;
问题2:分区删除后空间未释放
- InnoDB需要执行OPTIMIZE TABLE回收空间(注意会锁表)
- 建议在低峰期执行:
OPTIMIZE TABLE user_behavior_log_partitioned;
问题3:新增分区失败
- 检查MAXVALUE分区是否存在
- 确保没有分区范围重叠
6. 进阶优化技巧
6.1 冷热数据分离
对于需要长期保留的归档数据:
sql复制-- 创建归档表(使用压缩表)
CREATE TABLE user_behavior_log_archive (
id BIGINT,
user_id INT,
action VARCHAR(50),
device_info JSON,
created_at DATETIME,
KEY idx_created_at (created_at)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
-- 定期将待删除分区数据迁移到归档表
INSERT INTO user_behavior_log_archive
SELECT * FROM user_behavior_log_partitioned PARTITION(p202301);
6.2 并行查询优化
MySQL 8.0支持分区表的并行扫描:
sql复制-- 启用并行查询
SET SESSION innodb_parallel_read_threads = 8;
-- 跨分区查询会自动并行化
SELECT user_id, COUNT(*)
FROM user_behavior_log_partitioned
WHERE created_at BETWEEN '2023-01-01' AND '2023-03-01'
GROUP BY user_id;
7. 实测性能对比
在相同硬件环境下(16核CPU,64GB内存,NVMe SSD)测试:
| 操作类型 | 数据量 | 传统DELETE耗时 | 分区删除耗时 |
|---|---|---|---|
| 删除1周数据 | 1400万 | 18分32秒 | 0.35秒 |
| 全表扫描查询 | 2亿 | 4分12秒 | 47秒(并行) |
| 备份单个分区 | 1400万 | - | 28秒 |
从实际效果看,分区表方案在数据删除效率上有数量级的提升,同时查询性能也有显著改善。特别是结合MySQL 8.0的并行查询特性,大数据量查询响应时间缩短了近80%。