Bối cảnh & Tại sao cần
Hệ thống sau vài tháng vận hành thường bắt đầu lộ vấn đề ở các bảng tích lũy nhanh như audit_logs, user_sessions hay sms_otps. Khi một bảng phình từ vài chục nghìn lên hơn 10 triệu bản ghi, dung lượng đĩa cứng bị chiếm dụng nhanh chóng. Tệ hơn, câu lệnh SELECT phân trang hay tìm kiếm thường ngày từ 50ms có thể vọt lên 3-4 giây.
Thói quen phổ biến của nhiều team là viết script Python hoặc Node.js rồi giao cho Linux crontab chạy đêm. Cách này giải quyết được việc trước mắt. Dù vậy, nó tạo thêm điểm phụ thuộc bên ngoài ứng dụng. Crontab chết không ai hay. Script ngoài đòi hỏi quản lý thông tin đăng nhập riêng, đồng thời tốn thêm network round-trip khi truyền tải hàng triệu dòng dữ liệu qua lại.
MySQL có sẵn giải pháp nội bộ: MySQL Event Scheduler. Tính năng này đóng vai trò như một cron job nằm ngay trong database engine. Máy chủ tự động lập lịch và thực thi các câu lệnh SQL trực tiếp ở tầng dữ liệu mà không cần kết nối trung gian.
Trong thực tế với database MySQL 8.0 dung lượng 60GB, việc cắt tỉa tự động 100.000 dòng log hết hạn mỗi đêm giúp bảng chính thu gọn dung lượng từ 18GB xuống còn 3.5GB. Kích thước chỉ mục (B-Tree index) nhỏ hơn rõ rệt. Nhờ đó, buffer pool giữ được nhiều dữ liệu nóng hơn, giảm tải I/O đĩa tới 40%.
Cài đặt
Trước khi lên lịch cho bất kỳ tác vụ nào, bạn cần đảm bảo tiến trình điều phối (scheduler thread) trên máy chủ MySQL đang hoạt động.
Bước 1: Kiểm tra trạng thái Event Scheduler
Truy cập vào MySQL CLI qua terminal và chạy truy vấn:
SHOW VARIABLES LIKE 'event_scheduler';
Nếu cột Value hiển thị OFF hoặc DISABLED, tính năng này đang tắt và hệ thống sẽ bỏ qua mọi event đã lên lịch.
Bước 2: Bật Event Scheduler
Kích hoạt tạm thời ngay trong phiên làm việc mà không cần khởi động lại MySQL:
SET GLOBAL event_scheduler = ON;
Lưu ý rằng lệnh SET GLOBAL sẽ bị reset về mặc định nếu server khởi động lại. Từ MySQL 8.0, bạn có thể lưu cấu hình vĩnh viễn trực tiếp bằng lệnh SQL mà không cần sửa file bằng tay:
SET PERSIST event_scheduler = ON;
Nếu dùng MySQL 5.7 hoặc muốn quản lý qua file, hãy thêm dòng sau vào khối [mysqld] trong file /etc/mysql/mysql.conf.d/mysqld.cnf (hoặc /etc/my.cnf):
[mysqld]
event_scheduler=ON
Khởi động lại tiến trình nếu bạn sửa file cấu hình vật lý:
sudo systemctl restart mysql
Bước 3: Phân quyền cho User thực thi
Tài khoản chạy tác vụ cần quyền EVENT trên database mục tiêu. Hãy đăng nhập bằng quyền root và thực thi lệnh cấp quyền:
GRANT EVENT ON my_project_db.* TO 'app_user'@'%';
FLUSH PRIVILEGES;
Cấu hình chi tiết
Cấu trúc chuẩn của một Event trong MySQL bao gồm các thành phần sau:
CREATE EVENT [IF NOT EXISTS] event_name
ON SCHEDULE schedule_definition
[ON COMPLETION [NOT] PRESERVE]
[ENABLE | DISABLE]
DO
event_body;
1. Tác vụ đơn giản: Dọn dẹp token đăng nhập hết hạn
Xét trường hợp bảng user_tokens lưu refresh token. Token hết hạn sau 30 ngày cần được xóa định kỳ để dọn dẹp dung lượng bảng. Event dưới đây sẽ tự động kích hoạt vào lúc 03:00 sáng mỗi ngày:
CREATE EVENT IF NOT EXISTS ev_clean_expired_tokens
ON SCHEDULE EVERY 1 DAY
STARTS (CURRENT_DATE + INTERVAL 3 HOUR)
ON COMPLETION PRESERVE
ENABLE
DO
DELETE FROM user_tokens
WHERE expires_at < NOW();
Ý nghĩa các tham số thiết lập:
EVERY 1 DAY: Chu kỳ lặp lại. Bạn có thể linh hoạt đổi sang1 HOUR,30 MINUTEhoặc1 WEEKtùy nhu cầu.ON COMPLETION PRESERVE: Giữ lại event trong từ điển dữ liệu sau khi thực thi, thay vì bị MySQL tự xóa bỏ.ENABLE: Kích hoạt lịch chạy ngay khi tạo xong.
2. Tác vụ nâng cao: Khối lệnh BEGIN … END dọn log và ghi vết
Đối với kịch bản phức tạp gồm nhiều bước — lưu trữ log cũ sang bảng archive, xóa sạch ở bảng chính, rồi ghi nhật ký hệ thống — bạn cần gom các lệnh vào khối BEGIN ... END kèm việc đổi DELIMITER tạm thời:
DELIMITER $$
CREATE EVENT IF NOT EXISTS ev_archive_and_purge_logs
ON SCHEDULE EVERY 1 WEEK
STARTS (CURRENT_DATE + INTERVAL 2 HOUR)
ON COMPLETION PRESERVE
DO
BEGIN
-- 1. Sao chép dữ liệu quá 90 ngày sang bảng lưu trữ archive
INSERT INTO audit_logs_archive
SELECT * FROM audit_logs
WHERE created_at < NOW() - INTERVAL 90 DAY;
-- 2. Xóa các dòng đã sao chép để giải phóng dung lượng và index
DELETE FROM audit_logs
WHERE created_at < NOW() - INTERVAL 90 DAY;
-- 3. Ghi vết bảo trì vào bảng quản trị
INSERT INTO maintenance_history (task_name, executed_at, status)
VALUES ('ev_archive_and_purge_logs', NOW(), 'SUCCESS');
END $$
DELIMITER ;
3. Tạm dừng và kích hoạt lại Event khi deploy
Trong quá trình migrate schema hoặc cập nhật cấu trúc bảng lớn, các event chạy ngầm có thể gây xung đột khóa (lock conflict). Tốt nhất là tạm tắt event trước khi deploy:
-- Tạm dừng chạy event
ALTER EVENT ev_clean_expired_tokens DISABLE;
-- Mở lại lịch chạy bình thường sau khi deploy hoàn tất
ALTER EVENT ev_clean_expired_tokens ENABLE;
Kiểm tra & Monitoring
Event chạy hoàn toàn ở chế độ nền (background thread). Vì không có màn hình hiển thị trực tiếp, bạn cần chủ động nắm các công cụ giám sát để phát hiện sự cố.
1. Xem danh sách và thời điểm chạy tiếp theo
Kiểm tra nhanh danh sách event đang tồn tại trong database:
SHOW EVENTS FROM my_project_db\G
Để theo dõi cụ thể lần chạy gần nhất (LAST_EXECUTED) và thời điểm kích hoạt tiếp theo (NEXT_RUN), hãy truy vấn bảng hệ thống:
SELECT
EVENT_NAME,
STATUS,
INTERVAL_VALUE,
INTERVAL_FIELD,
LAST_EXECUTED,
NEXT_RUN
FROM information_schema.EVENTS
WHERE EVENT_SCHEMA = 'my_project_db';
2. Kiểm tra log lỗi hệ thống
Khi event vấp lỗi cú pháp hoặc đụng độ deadlock, MySQL không thể báo về client. Thay vào đó, toàn bộ chi tiết được ghi thẳng vào error.log của server. Bạn kiểm tra nhanh qua terminal:
sudo tail -n 50 /var/log/mysql/error.log
Log liên quan đến scheduler luôn đi kèm tiền tố nhận diện dạng [Warning] hoặc [ERROR] Event Scheduler: [user][db.event_name] ....
3. Kinh nghiệm tránh nghẽn Database khi chạy Event
- Tránh DELETE nguyên khối hàng triệu bản ghi: Một câu lệnh
DELETExóa cùng lúc 500.000 dòng sẽ khóa hàng loạt bản ghi (row/gap lock), gây nghẽn buffer pool và tạo độ trễ replication rất lớn trên slave server. Hãy chia nhỏ thành các batch 5.000 dòng kèm mệnh đềLIMIT, hoặc đặt trong một vòng lặp Stored Procedure có quãng nghỉ ngắn. - Bắt buộc đánh Index cho điều kiện WHERE: Mọi cột lọc mốc thời gian như
expires_athaycreated_atcần có index. Thiếu index, event sẽ quét toàn bộ bảng (full table scan), đẩy CPU lên 100% và làm nghẽn các query nghiệp vụ khác. - Né khung giờ cao điểm: Hãy chọn khung giờ có lưu lượng thấp nhất, thường rơi vào khoảng 02:00 – 04:30 sáng, để chạy các tác vụ dọn dẹp hoặc aggregate số liệu nặng.
