1. 项目背景与核心需求
最近在维护一个用户行为日志系统时遇到了存储空间告急的问题。这个系统每天产生约200万条日志记录,按照这个增长速度,我们的SSD存储将在3个月内耗尽。更关键的是,业务部门反馈最近查询响应时间从原来的200ms飙升到了1.5s以上。
经过分析发现,这个日志表已经积累了超过2亿条历史数据,其中90%都是6个月前的旧数据。这些数据虽然偶尔需要用于审计回溯,但日常业务完全用不到。这就是典型的"数据热温冷分层"问题——我们需要将热数据保留在性能最好的存储中,而冷数据应该归档或清理。
2. 技术方案选型
2.1 为什么选择事件调度器
MySQL 5.1开始引入的事件调度器(Event Scheduler)在8.0版本已经非常成熟。相比外部crontab方案,它有三大优势:
- 执行上下文在数据库内部,避免网络开销和连接建立消耗
- 权限体系与数据库用户体系一致,安全性更好
- 执行日志可以直接在performance_schema中查看
2.2 删除策略设计
我们设计了阶梯式删除策略:
sql复制-- 保留最近7天的完整数据
DELETE FROM user_logs WHERE create_time < DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 保留30天内的每日快照(每天00:00的一条样本)
INSERT INTO log_archive
SELECT * FROM user_logs
WHERE create_time BETWEEN DATE_SUB(NOW(), INTERVAL 30 DAY) AND DATE_SUB(NOW(), INTERVAL 7 DAY)
AND HOUR(create_time) = 0
ORDER BY create_time DESC
LIMIT 1 PER DAY;
-- 超过30天的数据直接删除
DELETE FROM user_logs WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);
3. 完整实现步骤
3.1 启用事件调度器
首先确认MySQL配置:
sql复制SHOW VARIABLES LIKE 'event_scheduler';
如果未启用,需要在my.cnf中添加:
ini复制[mysqld]
event_scheduler=ON
3.2 创建定时事件
sql复制DELIMITER //
CREATE EVENT daily_log_cleanup
ON SCHEDULE EVERY 1 DAY
STARTS CURRENT_DATE + INTERVAL 1 DAY + INTERVAL 2 HOUR
DO
BEGIN
-- 为防止锁表,每次最多删除50万条
DELETE FROM user_logs
WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 500000;
-- 记录操作日志
INSERT INTO event_logs VALUES (NOW(), 'daily_log_cleanup', ROW_COUNT());
END //
DELIMITER ;
3.3 优化删除操作
大表删除的注意事项:
- 分批删除避免长事务
- 在业务低峰期执行
- 删除后及时optimize table
改进后的分批删除逻辑:
sql复制CREATE PROCEDURE batch_delete(IN cutoff_date DATETIME)
BEGIN
DECLARE affected_rows INT DEFAULT 1;
WHILE affected_rows > 0 DO
DELETE FROM user_logs
WHERE create_time < cutoff_date
LIMIT 10000;
SET affected_rows = ROW_COUNT();
COMMIT;
DO SLEEP(1); -- 给其他查询留出机会
END WHILE;
END
4. 性能优化实践
4.1 索引策略调整
发现原来的索引只有主键id,我们增加了复合索引:
sql复制ALTER TABLE user_logs
ADD INDEX idx_cleanup (create_time, is_archived);
4.2 存储引擎优化
将表从InnoDB转换为MyRocks引擎(适合写多读少场景):
sql复制ALTER TABLE user_logs ENGINE=ROCKSDB;
4.3 监控指标配置
在Prometheus中添加监控:
yaml复制- name: mysql_deleted_rows
metrics_path: /metrics
static_configs:
- targets: ['mysql-exporter:9104']
params:
query: ["sum(mysql_global_status_wsrep_replicated_bytes_total) by (instance)"]
5. 常见问题处理
5.1 事件不执行排查
检查步骤:
- 确认event_scheduler状态
- 查看错误日志:
sql复制SELECT * FROM performance_schema.events_errors_summary_global_by_error; - 检查事件定义:
sql复制SHOW EVENTS LIKE 'daily_log_cleanup'\G
5.2 锁等待超时
解决方案:
sql复制SET GLOBAL innodb_lock_wait_timeout = 120;
5.3 磁盘空间不释放
执行OPTIMIZE TABLE前需要确保:
- 有足够的临时空间(建议1.5倍表大小)
- 在业务低峰期操作
- 有从库可以切换
6. 进阶优化方案
6.1 分区表方案
将表按月份分区:
sql复制ALTER TABLE user_logs
PARTITION BY RANGE (TO_DAYS(create_time)) (
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
);
删除数据时直接truncate分区:
sql复制ALTER TABLE user_logs TRUNCATE PARTITION p202301;
6.2 使用pt-archiver工具
Percona工具链的pt-archiver更安全:
bash复制pt-archiver \
--source h=localhost,D=test,t=user_logs \
--where "create_time < DATE_SUB(NOW(), INTERVAL 30 DAY)" \
--purge \
--limit 10000 \
--sleep 1
7. 注意事项
- 大表删除前务必先备份
- 监控复制延迟(如果是从库)
- 避免在事务中执行大批量删除
- 定期检查binlog增长情况
- 考虑使用软删除+物理删除组合方案
经过上述优化后,我们的日志表稳定维持在3000万条数据左右,查询响应时间回落至300ms以内,磁盘空间使用率从95%降到了65%。这个方案特别适合需要长期运行但只需保留近期数据的业务场景。