Stored Procedure và Trigger trong PostgreSQL: Đừng để Logic Nghiệp vụ bị Phân tán

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Bối cảnh & Lý do: Đưa Logic lại gần Dữ liệu hơn

Gần đây, tôi đã dành sáu tháng để gỡ rối cho một ứng dụng fintech có lưu lượng truy cập cao đang ngập trong các vấn đề về nhất quán dữ liệu. Backend Node.js của chúng tôi xử lý mọi quy tắc nghiệp vụ—từ tính lãi kép đến cập nhật nhật ký kiểm tra (audit logs).

Mặc dù việc giữ logic ở tầng ứng dụng mang lại cảm giác linh hoạt, nhưng chúng tôi đã vấp phải rào cản khi năm microservices khác nhau bắt đầu truy cập vào cùng một cơ sở dữ liệu. Tình trạng tranh chấp (race conditions) trở thành thảm họa hàng tuần, và độ trễ mạng từ các vòng lặp truy vấn (round-trips) liên tục đã đẩy thời gian phản hồi của chúng tôi lên hơn 200ms.

Đó là lúc tôi quyết định chuyển những tác vụ nặng nề vào PostgreSQL. Bằng cách đưa logic nghiệp vụ cốt lõi trực tiếp vào cơ sở dữ liệu, tôi đảm bảo các quy tắc luôn đồng nhất bất kể dịch vụ nào tác động vào dữ liệu. Nó trở thành nguồn chân lý duy nhất (source of truth) của chúng tôi. Sau khi di chuyển, độ trễ API cốt lõi đã giảm gần 60% và những lỗi dữ liệu ảo (phantom bugs) khó chịu đã biến mất chỉ sau một đêm.

Hãy coi Stored Procedure là một cách để nhóm các câu lệnh SQL phức tạp thành một đơn vị duy nhất có thể gọi được. Trigger là những người hùng thầm lặng, tự động thực thi logic bất cứ khi nào một hàng được chèn hoặc thay đổi. Nếu bạn đã mệt mỏi với việc lặp lại logic xác thực trên ba ngôn ngữ lập trình khác nhau, phương pháp này chính là người bạn thân nhất của bạn.

Cài đặt: Chuẩn bị môi trường PostgreSQL

PostgreSQL đi kèm với ngôn ngữ thủ tục riêng gọi là PL/pgSQL. Trong hầu hết các bản cài đặt hiện đại, ngôn ngữ này đã sẵn sàng để sử dụng. Tuy nhiên, tôi luôn kiểm tra môi trường trước khi bắt đầu triển khai các logic phức tạp lên môi trường production.

Để kiểm tra xem ngôn ngữ này đã có sẵn chưa, hãy kết nối qua psql hoặc GUI bạn chọn và chạy truy vấn này:

SELECT * FROM pg_language;

Nếu bạn không thấy plpgsql trong danh sách, bạn có thể kích hoạt nó bằng một lệnh duy nhất. Bạn chỉ cần thực hiện việc này một lần cho mỗi cơ sở dữ liệu:

CREATE EXTENSION IF NOT EXISTS plpgsql;

Một mẹo nhỏ về quy trình làm việc: khi tôi chuẩn bị dữ liệu thử nghiệm—như chuyển đổi một file CSV 10.000 dòng thành JSON cho một hàm—tôi sử dụng toolcraft.app/vi/tools/data/csv-to-json. Nó chạy hoàn toàn trong trình duyệt của bạn. Điều này có nghĩa là dữ liệu của bạn không bao giờ rời khỏi máy tính, một điều bắt buộc đối với quyền riêng tư khi xử lý các hồ sơ fintech nhạy cảm.

Cấu hình: Triển khai Procedure và Trigger

Về mặt kỹ thuật, có một sự khác biệt nhỏ nhưng quan trọng giữa Function (Hàm) và Procedure (Thủ tục). Function rất tốt for việc tính toán và phải trả về một giá trị. Procedure, được giới thiệu từ PostgreSQL 11, cho phép bạn kiểm soát các giao dịch (transactions) bằng COMMITROLLBACK ngay bên trong khối mã.

1. Tạo Stored Procedure cho các giao dịch

Tôi nhận thấy rằng Procedure hoàn hảo cho các hoạt động hàng loạt (batch operations). Ví dụ, đây là một procedure tôi viết để xử lý việc chuyển tiền. Nó đảm bảo rằng việc cập nhật cả người gửi và người nhận diễn ra như một đơn vị nguyên tử (atomic unit) duy nhất.

CREATE OR REPLACE PROCEDURE transfer_funds(
   sender_id INT, 
   receiver_id INT, 
   amount DECIMAL
)
LANGUAGE plpgsql
AS $$
BEGIN
    -- Trừ tiền từ người gửi
    UPDATE accounts SET balance = balance - amount WHERE id = sender_id;
    
    -- Cộng tiền cho người nhận
    UPDATE accounts SET balance = balance + amount WHERE id = receiver_id;

    COMMIT;
END;
$$;

Để thực thi lệnh này, hãy sử dụng lệnh CALL:

CALL transfer_funds(101, 202, 500.00);

2. Tự động hóa với Trigger

Trigger là người hùng thầm lặng trong hệ thống audit của chúng tôi. Tôi cần theo dõi các thay đổi email mà không làm rối mã API bằng logic ghi nhật ký. Đầu tiên, bạn định nghĩa một hàm trả về một TRIGGER, sau đó bạn liên kết nó với bảng của mình.

-- Hàm xử lý Logic
CREATE OR REPLACE FUNCTION log_email_change()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.email <> NEW.email THEN
        INSERT INTO audit_logs(user_id, old_email, new_email, changed_at)
        VALUES (OLD.id, OLD.email, NEW.email, NOW());
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Liên kết Trigger
CREATE TRIGGER trigger_email_update
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION log_email_change();

Giờ đây, mỗi khi có lệnh UPDATE tác động vào bảng users, PostgreSQL sẽ tự động xử lý việc ghi nhật ký. Các nhà phát triển backend của tôi không phải thay đổi bất kỳ dòng mã JavaScript nào, nhưng đội ngũ tuân thủ vẫn nhận được chính xác những gì họ cần.

Xác minh & Giám sát: Duy trì hệ thống hoạt động ổn định

Sau khi chạy những thứ này trên production, tôi nhận ra rằng tư duy “thiết lập một lần rồi quên luôn” là rất nguy hiểm. Bạn phải giám sát hiệu suất. PostgreSQL cung cấp các view nội bộ giúp việc này trở nên dễ dàng đến ngạc nhiên.

Kiểm tra hiệu suất của Hàm

Tôi sử dụng view pg_stat_user_functions để theo dõi số lượng cuộc gọi và thời gian thực thi. Điều này đã giúp tôi phát hiện một hàm xác thực chậm chạp đang gây thêm 50ms độ trễ cho luồng đăng ký của chúng tôi.

SELECT funcname, calls, total_time, self_time 
FROM pg_stat_user_functions 
ORDER BY total_time DESC;

Gỡ lỗi với Nhật ký (Logs)

Gỡ lỗi PL/pgSQL có thể khó khăn vì bạn không thể chỉ đặt một breakpoint trong VS Code. Tôi dựa vào RAISE NOTICE để in giá trị các biến trong quá trình phát triển. Những thông báo này sẽ xuất hiện trong SQL console hoặc server logs của bạn.

RAISE NOTICE 'Đang chuyển % từ người dùng %', amount, sender_id;

Một bài học mà tôi đã học được một cách xương máu: hãy cẩn thận với trigger đệ quy. Tôi từng viết một trigger cập nhật chính cái bảng mà nó đang theo dõi. Nó đã gây ra một vòng lặp vô tận làm sập kết nối. Luôn kiểm tra các trigger của bạn với khối BEGIN; ... ROLLBACK; để đảm bảo chúng hoạt động đúng trước khi bạn commit vĩnh viễn.

Bằng cách tận dụng các công cụ này, chúng tôi đã giảm 20% độ phức tạp của mã backend và cuối cùng đạt được độ tin cậy dữ liệu 100%. Nếu bạn đang quản lý một cơ sở dữ liệu phục vụ nhiều ứng dụng, việc chuyển logic cốt lõi vào PostgreSQL không chỉ là một sự tối ưu hóa. Đó là một điều cần thiết để giữ cho hệ thống (và chính bạn) luôn ổn định.

Share: