Lan man
Một hôm nọ mình nhận làm một cái side-project nho nhỏ cho một ngôi trường ở Hà Nội. Trường học này sử dụng X máy chấm công dạng quẹt thẻ, nhằm để điểm danh cho học sinh ra vào trường. Các máy chấm công này đều dùng chung một phần mềm quản lý chuyên trách với cơ sở dữ liệu (CSDL) riêng, có tác dụng đọc dữ liệu từ các máy chấm công lắp đặt ở cồng trường. Ngoài ra nhà trường cũng có sẵn một hệ thống (khá là lâu đời, nhưng hoạt động vẫn ổn định) gửi tin nhắn về cho phụ huynh xem con đã đến trường hay là chưa.
Đối với side-project này, mình cần viết ra một phần mềm kết nối đọc dữ liệu từ CSDL của máy chấm công, rồi gửi đống dữ liệu ấy lên cho hệ thống gửi tin nhắn. Phần mềm của mình sẽ được cài trực tiếp vào máy tính chứa phần mềm máy chấm công, tức là mình sẽ được động tay thẳng vào CSDL chấm công luôn mà không cần viết thêm API gửi dữ liệu qua lại nữa.
Tuy nhiên, nhà trường không chi nhiều tiền để vận hành, nên máy tính chạy project của mình cùng với máy chấm công tương đối yếu. Mình bắt buộc phải tối ưu hoá cho nó để làm sao việc đọc và gửi dữ liệu không bị chậm trễ. Thực sự chẳng ai muốn thấy tin nhắn đến chậm cả.
Đặt vấn đề
Từ thông tin mà nhà trường cung cấp thì vào thời điểm mình bắt tay xây dựng phần mềm này, nhà trường hiện có 2.500 học sinh đều được phát thẻ điểm danh. Tức là mỗi ngày sẽ có ít nhất 5.000 lượt quẹt thẻ tương ứng với lượt ra/vào của học sinh. Học sinh phải đi học 6 ngày 1 tuần, vậy là sẽ có 24 ngày hàng tháng có 5000 lượt quẹt thẻ, tính tổng rơi vào khoảng hơn 120.000 lượt quẹt thẻ trên tháng.
Hệ thống mà nhà trường cung cấp chạy hệ điều hành Windows. Phần mềm và CSDL của máy chấm công cũng chạy trên Windows và sử dụng MSSQL. Vậy là phần mềm của mình sẽ phải là một Console Project sử dụng SQL-Server để tiện truy vấn rồi. Vả lại với lượng dữ liệu khổng lồ như vậy thì việc có thêm một bảng dữ liệu lưu lại thao tác cũng là điều bắt buộc để có phần đối chiếu khi cần thiết. Vả lại CSDL cho phần mềm của mình cũng hoạt động như một Message-Queue, tạo ra các job và để gửi dần dần lên hệ thống gửi tin nhắn.
Vì hệ thống gửi tin nhắn có rate-limit nên mình không thể spam server liên tục được, cần có thời gian (30s - 60s) giữa các lần gọi API để tránh gây nghẽn hệ thống.
Thiết kế hệ thống
Đối với phần mềm, cần có 2 phần tách rời chạy song song:
- Read Bot: đọc dữ liệu từ CSDL máy chấm công và đẩy vào hàng chờ
- Send Bot: đọc dữ liệu từ hàng chờ và gửi yêu cầu lên API. Nếu API không trả về lỗi, hoặc lời gọi không bị gián đoạn thì cập nhật thành công vào hàng chờ
Đối với Message Queue, cần lưu một số thông tin như là:
- Mã thẻ
- Thời gian được tạo
- Trạng thái
- Thời gian cập nhật
Thực hiện
Mình nhanh chóng thiết kế xong CSDL trong SQL Server với PK để là tự tăng (auto-increment). Cũng như viết xong các đoạn lệnh cho Read Bot
với Send Bot
sao cho chúng có thể hoạt động liên tục và vẫn có thời gian nghỉ, để không gây ảnh hưởng vào RAM/CPU trong lúc hoạt động.
Sau đó, mình bắt đầu kiểm thử phần mềm. Với 2.500 bản ghi đầu tiên tương đối đơn giản, hệ thống vẫn truy vấn với tốc độ chấp nhận được. Nhưng khi lên 5.000 bản thì bắt đầu chậm đi đáng kể. Dường như các câu truy vấn đang tiến hành quét toàn bộ bảng nên thời gian truy vấn tương đối lâu với lượng bản ghi lớn. Ngoài ra, còn gặp trường hợp nho nhỏ khi học sinh quẹt thẻ liên tục trong vòng 5s - 10s, một phần do học sinh luống cuống hoặc máy chấm công không đủ nhạy; việc này sinh ra một lượng lớn bản ghi thừa thãi làm trễ các phần khác.
Để truy vấn hiệu quả hơn, mình nghĩ tới việc đánh Index bảng với các trường mà mình hay dùng, bao gồm Mã thẻ, Thời gian được tạo và Trạng thái. Tuy nhiên việc đánh index này sẽ tăng chi phí cho câu truy vấn CREATE
và UPDATE
. Còn với trường hợp học sinh quẹt nhiều lần, mình thêm một trường nữa trong bảng là Khoảng thời gian thay đổi tính theo phút, nếu lần cuối học sinh quẹt có thời gian chênh lệch không tới một khoảng thời gian nhất định thì mình sẽ không thêm bản ghi vào Message-Queue của phần mềm.
May mắn sao lúc đó trong team mình có một lead khá rành về CSDL, mình mới đem khúc mắc của mình về đoạn đánh Index với anh ấy. Sau chừng 10 phút, anh ấy chỉ ra một thứ mình chưa nghĩ tới, đó là Primary Key (PK). PK hoạt động như một Index đặc biệt được CSDL tối ưu cho hoạt động truy vấn. Nếu mình tạo ra PK từ lúc thực hiện lệnh CREATE
thì coi như bản ghi đã được đánh Index ngay lúc đó, và mọi truy vấn sau này nếu lọc qua PK sẽ cho thời gian trả về nhanh nhất cũng như sẽ không phải tốn chi phí như cách ban đầu mình nghĩ.
PK lúc này sẽ không còn là tự tăng nữa, mà trở thành kiểu dữ liệu văn bản (varchar) và được tạo ra bằng cách trộn các dữ liệu mình cần dùng với nhau. PK của mình lúc này sẽ công thức tạo như sau:
PK = Ngày tạo (lấy từ thời gian được tạo) + Mã thẻ + Lần quẹt thứ x
Vì đặc điểm của quét mã là chạy theo từng ngày, nên mình sẽ chỉ cần lưu lại năm/tháng/ngày vào PK, như vậy thì một ngày mình sẽ chỉ cần lọc theo 8 ký tự đầu tiên của PK là ra được ngày hiện tại, gần như không phải quét lại bảng nữa. Tiếp tới là mã thẻ, trong trường hợp của mình, là 5 ký tự tiếp theo sau đó, mình sẽ biết được học sinh nào đó đã quẹt thẻ hay chưa. Và cuối cùng là lần quẹt, mình giới hạn 2 ký tự, là mình biết học sinh này có ra vào trường bao nhiêu lần. Vậy là với PK dài 15 ký tự, mình đã nắm được phần lớn thông tin điểm danh của học sinh rồi. Ngoài ra, với cách tối ưu này, phần mềm của mình sẽ không dính vào những trường hợp bị chậm do phải quét toàn bộ bảng nữa.
Đánh giá
Việc thêm lần quẹt thực tế là để tạo ra một UNIQUE PK, để đảm bảo không có bản ghi nào bị trùng nhau cả. Như vậy thì sẽ đạt dược best practice mà CSDL quan hệ đề ra để tối ưu truy vấn tốt nhất.
Từ lúc phần mềm mình được cài đặt tới giờ là gần 1 năm và chưa có vấn đề gì liên quan đến chuyện chậm trễ của phần mềm cả. Mọi thứ đều chạy ổn định, tất nhiên là không nhắc tới những lúc mạng thiếu ổn định hay mất điện.
Qua lần làm side-project này thì mình học được cách tối ưu truy vấn thông qua Index mà cụ thể là Primary Key của CSDL quan hệ. Tuy nhiên nếu đúng thì mình phải xoá dữ liệu cũ (hoặc chuyển nó tới một bảng backup), như vậy thì truy vấn mới đạt hiệu suất cao nhất.
Tóm gọn
- Đừng dùng PK tự tăng nếu bạn cần truy vấn nhiều
- Hãy tạo ra PK theo những thứ mà bạn cần truy vấn nhiều, và đảm bảo tính UNIQUE của PK
- Vẫn nên suy nghĩ đến việc phải đánh Index nếu bạn dùng trong một CSDL với nhiều bảng hơn là chỉ trên 1 bảng duy nhất như bài viết