Khi xử lý logic nghiệp vụ, đôi khi ứng dụng cần phải thực thi lặp đi lặp lại một số tác vụ liên tục trong khoảng thời gian ngắn. Việc này không chỉ đảm bảo tính nhất quán của hệ thống mà còn có tác dụng nâng cao trải nghiệm người dùng. Trong bài viết sau đây, Vietnix sẽ hướng dẫn các bạn cách chạy job PHP nhiều lần một phút với Crontab trên Ubuntu 20.04.
Giới thiệu về Crontab
Crontab là một công cụ hỗ trợ các tác vụ nền theo thời gian của Linux. Dù thực hiện tốt việc liên tục lặp lại các tác vụ, tuy nhiên hạn chế của nó là bạn chỉ có thể thực hiện các tác vụ trong khoảng thời gian tối thiểu là 1 phút.
Trong nhiều trường hợp, việc này có thể ảnh hưởng đến trải nghiệm người dùng của ứng dụng. Chẳng hạn như một website cần lập lịch một tác vụ xử lý file bằng job-queue model (mô hình hàng đợi job), thời gian đợi lâu sẽ ảnh hưởng tiêu cực đến người dùng cuối.
Việc này có thể được khắc phục bằng cách sử dụng một vòng lặp với PHP script. Vòng lặp này sẽ liên tục xử lý các tác vụ cần thiết trong khoảng thời gian 60 giây đợi daemon gọi thực thi lần tiếp theo. Nhờ đó các tác vụ của ứng dụng có thể thực thi theo đúng thời gian và logic nghiệp vụ mà không khiến người dùng chờ đợi.
Bài viết này sẽ hướng dẫn các bạn cách tạo mẫu một cơ sở dữ liệu cron_jobs
với bảng task
trên server Ubuntu 20.04, sau đó tạo script thực thi vòng lặp PHP while(…){…}
và hàm sleep()
để xử lý các tác vụ trong bảng mỗi 5 giây.
Chuẩn bị để chạy job PHP nhiều lần một phút với Crontab trên Ubuntu 20.04
Các bạn cần chuẩn bị một số việc sau trước khi bắt đầu thực hiện:
- Một server Ubuntu 20.04 và một tài khoản người dùng non-root.
- Cài đặt LAMP stack (Linux, Apache, MySQL, PHP) trên server.
Bước 1 – Cấu hình cơ sở dữ liệu
Đầu tiên, bạn đăng nhập vào server và kết nối MySQL bằng tài khoản người dùng root để tạo mẫu một cơ sở dữ liệu.
$ sudo mysql -u root -p
Nhập mật khẩu người dùng MySQL server, ENTER
và tạo cơ sở dữ liệu cron_jobs
bằng lệnh dưới đây.
mysql> CREATE DATABASE cron_jobs;
Tiếp tục tạo một người dùng non-root. Người dùng này sẽ cung cấp các thông tin xác thực để PHP có thể kết nối cron_jobs
. Bạn nên chọn một mật khẩu mạnh cho EXAMPLE_PASSWORD
.
mysql> CREATE USER 'cron_jobs_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'EXAMPLE_PASSWORD';
mysql> GRANT ALL PRIVILEGES ON cron_jobs.* TO 'cron_jobs_user'@'localhost';
mysql> FLUSH PRIVILEGES;
Truy cập cơ sở dữ liệu cron_jobs
bằng lệnh sau.
mysql> USE cron_jobs;
Output
Database changed
Các bạn tạo bảng task
và thêm một số tác vụ mà cron job thực thi tự động. Những tác vụ này sẽ được thực thi cách 5 giây thay vì 1 phút sau khi bạn sử dụng một PHP script để ghi đè các thiết lập mặc định.
Tạo bảng task
bằng lệnh truy vấn sau.
mysql> CREATE TABLE tasks (
mysql> task_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
mysql> task_name VARCHAR(50),
mysql> queued_at DATETIME,
mysql> completed_at DATETIME,
mysql> is_processed CHAR(1)
mysql> ) ENGINE = InnoDB;
Các bạn thêm 3 record (bản ghi) mới vào bảng. Hàm NOW()
ở cột queued_at
sẽ ghi lại thời gian tác vụ được thực thi. Hàm CURDATE()
ở cột completed_at
có giá trị mặc định là 00:00:00
và sẽ được script cập nhật sau khi tác vụ thực thi hoàn tất.
mysql> INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 1', NOW(), CURDATE(), 'N');
mysql> INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 2', NOW(), CURDATE(), 'N');
mysql> INSERT INTO tasks (task_name, queued_at, completed_at, is_processed) VALUES ('TASK 3', NOW(), CURDATE(), 'N');
Kiểm tra output sau mỗi lệnh INSERT
.
Output
Query OK, 1 row affected (0.00 sec)
...
Để chắc chắn dữ liệu đã thêm thành công vào tasks
, các bạn gọi lệnh truy vấn SELECT
sau.
mysql> SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
Danh sách các tác vụ sẽ được hiển thị.
Output
+-----------+---------------+-----------------------------+----------------------------+------------------+
| task_id | task_name | queued_at | completed_at | is_processed |
+-----------+---------------+-----------------------------+----------------------------+------------------+
| 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 00:00:00 | N |
| 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 00:00:00 | N |
| 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 00:00:00 | N |
+-----------+---------------+-----------------------------+----------------------------+------------------+
3 rows in set (0.00 sec)
Thoát MySQL CLI (command-line interface).
mysql> QUIT;
Output
Bye
Sau khi tạo bảng tasks
và cơ sở dữ liệu cron_jobs
, bước tiếp theo các bạn sẽ tạo một script PHP để xử lý tác vụ.
Nếu bạn đang tìm kiếm một dịch vụ VPS chất lượng để chạy job PHP nhiều lần một phút với Crontab, bạn có thể tham khảo lựa chọn VPS của Vietnix. Với các gói VPS đa dạng và phù hợp với nhu cầu của bạn như VPS Giá Rẻ, Cloud Server, VPS Cao Cấp, VPS NVMe, VPS GPU, Vietnix đảm bảo cung cấp cho bạn một môi trường ổn định và hiệu suất cao để thực hiện các công việc PHP của bạn một cách hiệu quả.
Bước 2 – Tạo và cấu hình script PHP chạy mỗi 5 giây
Các bạn sẽ kết hợp vòng lặp while(…){…}
và hàm sleep
của PHP để xử lý tác vụ sau một khoảng thời gian nhất định.
Dùng nano
để tạo mới file /var/www/html/tasks.php
trong thư mục root của web server.
$ sudo nano /var/www/html/tasks.php
Mở thẻ <?php
và thêm block try {
để khai báo các biến được tạo trong cơ sở dữ liệu ở bước 1. Đừng quên thay EXAMPLE_PASSWORD
bằng mật khẩu cơ sở dữ liệu của người dùng.
<?php
try {
$db_name = 'cron_jobs';
$db_user = 'cron_jobs_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
Tạo đối tượng PDO (PHP Data Object) và thiết lập thuộc tính ERRMODE_EXCEPTION
để bắt các lỗi phát sinh của PDO, đồng thời gán giá trị false
cho thuộc tính ATTR_EMULATE_PREPARES
để engine của cơ sở dữ liệu MySQL xử lý việc giả lập. Các lệnh truy vấn SQL và dữ liệu nên được tách biệt để tăng cường bảo mật cũng như giảm thiểu nguy cơ bị lỗ hổng SQL injection.
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
Biến $loop_expiry_time
được tạo tiếp theo có giá trị là một phút sau khi khởi tạo. Biến được sử dụng để dừng vòng lặp PHP while(time() < $loop_expiry_time) {
khi time()
khớp với giá trị của $loop_expiry_time
.
$loop_expiry_time = time() + 60;
while (time() < $loop_expiry_time) {
Khai báo một truy vấn SQL để lấy các job chưa chạy trong bảng tasks
.
$data = [];
$sql = "select
task_id
from tasks
where is_processed = :is_processed
";
Thực thi truy vấn và lấy về danh sách các dòng có giá trị cột is_processed
là N. Đây là những dòng tác vụ chưa được chạy.
$data['is_processed'] = 'N';
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
Duyệt qua danh sách trên bằng vòng lặp PHP while ($row = $stmt->fetch(PDO::FETCH_ASSOC))
{…}
. Tiếp tục gọi một truy vấn SQL để cập nhật giá trị cột is_processed
và complete_at
sau khi tác vụ được chạy. Việc này sẽ đảm bảo mỗi tác vụ chỉ thực thi một lần.
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$data_update = [];
$sql_update = "update tasks set
is_processed = :is_processed,
completed_at = :completed_at
where task_id = :task_id
";
$data_update = [
'is_processed' => 'Y',
'completed_at' => date("Y-m-d H:i:s"),
'task_id' => $row['task_id']
];
$stmt = $pdo->prepare($sql_update);
$stmt->execute($data_update);
}
Lưu ý: Nếu hàng đợi quá lớn (giả sử bạn cần thực thi 100,000 tác vụ một giây), các bạn có thể xem xét sử dụng Redis Server có tốc độ nhanh hơn MySQL để xử lý tác vụ. Bộ dữ liệu trong bài viết này có kích thước nhỏ hơn trường hợp nêu trên rất nhiều.
Bổ sung lời gọi hàm sleep(5)
trước khi kết thúc vòng lặp PHP while (time() < $loop_expiry_time) {
đầu tiên để tạm ngưng thực thi tác vụ trong 5 giây và giải phóng tài nguyên server.
Tùy theo logic nghiệp vụ cũng như tốc độ thực thi tác vụ nhanh đến đâu mà bạn có thể điều chỉnh khoảng thời gian 5 giây tạm ngưng. Giả như các tác vụ cần thực thi 3 lần một phút thì bạn có thể chọn 20 giây là khoảng thời gian nghỉ.
Thêm block } catch (PDOException $ex) { echo $ex->getMessage(); }
để bắt các thông điệp lỗi phát sinh của PDO trong quá trình chạy.
sleep(5);
}
} catch (PDOException $ex) {
echo $ex->getMessage();
}
Dưới đây là file tasks.php
hoàn chỉnh.
<?php
try {
$db_name = 'cron_jobs';
$db_user = 'cron_jobs_user';
$db_password = 'EXAMPLE_PASSWORD';
$db_host = 'localhost';
$pdo = new PDO('mysql:host=' . $db_host . '; dbname=' . $db_name, $db_user, $db_password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$loop_expiry_time = time() + 60;
while (time() < $loop_expiry_time) {
$data = [];
$sql = "select
task_id
from tasks
where is_processed = :is_processed
";
$data['is_processed'] = 'N';
$stmt = $pdo->prepare($sql);
$stmt->execute($data);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$data_update = [];
$sql_update = "update tasks set
is_processed = :is_processed,
completed_at = :completed_at
where task_id = :task_id
";
$data_update = [
'is_processed' => 'Y',
'completed_at' => date("Y-m-d H:i:s"),
'task_id' => $row['task_id']
];
$stmt = $pdo->prepare($sql_update);
$stmt->execute($data_update);
}
sleep(5);
}
} catch (PDOException $ex) {
echo $ex->getMessage();
}
Lưu file với tổ hợp phím CTRL
+ X
, Y
rồi ENTER
.
Đến đây, bạn đã hoàn tất việc thiết lập logic trong file /var/www/html/tasks.php
. Bước tiếp theo, các bạn sẽ lập lịch để file được gọi mỗi 1 phút bởi daemon crontab.
Bước 3 – Lên lịch chạy script PHP mỗi phút
Chỉ cần thêm một dòng lệnh vào file crontab, Linux có thể lập lịch chạy tự động các job sau một khoảng thời gian nhất định. Ở bước này, các bạn sẽ được hướng dẫn cách thiết lập crontab daemon chạy tự động script /var/www/html/tasks.php
mỗi 60 giây. Mở file /etc/crontab
bằng trình biên tập văn bản nano
.
$ sudo nano /etc/crontab
Thêm vào cuối file dòng lệnh dưới đây để daemon gọi thực thi http://localhost/tasks.php
sau mỗi phút.
...
* * * * * root /usr/bin/wget -O - http://localhost/tasks.php
Đóng và lưu file.
Như đã đề cập, dù script task.php
chỉ được gọi một lần bởi daemon, các vòng lặp trong script sẽ tiếp tục thực thi các tác vụ chưa được xử lý trong vòng 60 giây kế tiếp. Một khi vòng lặp ngừng, daemon sẽ gọi lại script và tiếp tục lặp lại quy trình trên.
Sau khi lưu thiết lập, crontab sẽ lập tức thực thi các tác vụ trong bảng tasks
trong cơ sở dữ liệu. Để đảm bảo mọi thứ hoạt động ổn định, các bạn đến bước cuối cùng là kiểm tra cơ sở dữ liệu cron_jobs
.
Bước 4 – Xác nhận các job thực thi thành công
Mở lại cơ sở dữ liệu với tài khoản người dùng root để kiểm tra việc crontab tự động thực thi job bằng script tasks.php
.
$ sudo mysql -u root -p
Sau khi nhập mật khẩu, các bạn kết nối cơ sở dữ liệu.
mysql> USE cron_jobs;
Output
Database changed
Gọi truy vấn SELECT
sau cho bảng tasks
.
SELECT task_id, task_name, queued_at, completed_at, is_processed FROM tasks;
Nhìn vào cột completed_at
của output, bạn sẽ thấy cứ mỗi 5 giây có một tác vụ được chạy. Những tác vụ này cũng được đánh dấu đã thực thi bằng ký tự Y
nghĩa là YES
ở cột is_processed
.
Output
+-----------+---------------+----------------------------+-----------------------------+-------------------+
| task_id | task_name | queued_at | completed_at | is_processed |
+-----------+---------------+----------------------------+-----------------------------+-------------------+
| 1 | TASK 1 | 2021-03-06 06:27:19 | 2021-03-06 06:30:01 | Y |
| 2 | TASK 2 | 2021-03-06 06:27:28 | 2021-03-06 06:30:06 | Y |
| 3 | TASK 3 | 2021-03-06 06:27:36 | 2021-03-06 06:30:11 | Y |
+-----------+---------------+-----------------------------+----------------------------+-------------------+
3 rows in set (0.00 sec)
Đến đây, script PHP của bạn đã hoạt động suôn sẻ. Các bạn đã có thể xử lý các tác vụ cần thiết với khoảng thời gian nghỉ ngắn hơn 1 phút sau khi ghi đè các thiết lập mặc định của crontab daemon.
Lời kết
Hy vọng các bạn đã nắm được cách chạy job PHP nhiều lần một phút với Crontab trên Ubuntu 20.04 sau bài hướng dẫn trên. Nếu có bất kỳ thắc mắc hay phản hồi nào, các bạn hãy để lại bình luận bên dưới để được Vietnix giải đáp.