Phỏng vấn ở Banking (Backend): Cốt lõi là phải xử lý được nhiều giao dịch cùng một lúc? Tôi đã vượt qua thế nào?

Nội dung bài viết

Video học lập trình mỗi ngày

Bài viết này được đưa vào Series: Phỏng vấn kỹ sư backend

Mùa nhảy việc lại đến rồi anh em. CV gửi đi, qua được vòng lọc của HR, và giờ là lúc đối mặt với vòng phỏng vấn kỹ thuật. Nếu anh em đang nhắm vào vị trí Backend Java cho Banking hay Fintech, sớm muộn gì cũng sẽ gặp một câu hỏi na ná thế này:

"Hệ thống của em thiết kế thế nào để xử lý được 1000–2000 transaction/giây (TPS)?"

Đây không phải là câu hỏi để doạ ma. Đây là câu hỏi để phân loại.

Nó tách biệt một Senior thực thụ — người đã từng "toát mồ hôi" với hệ thống product — với một developer chỉ quen code CRUD trên máy cá nhân.

Nhà tuyển dụng không chỉ muốn nghe bạn nói về scale-out, về load balancing. Cái họ muốn thấy là chiều sâu tư duy về sự ổn định của hệ thống. Và một trong những kẻ thù thầm lặng, nguy hiểm nhất khi xử lý đa luồng (multithreading) với throughput cao chính là Deadlock.

Cái này AI không cứu được đâu. Chỉ có kinh nghiệm và sự hiểu biết bản chất mới giúp anh em sống sót.


"Deadlock" — Nó Là Cái Quái Gì?

Nói cho dễ hiểu, deadlock là tình trạng "khóa chết". Nhìn vào đoạn code "ngây thơ" dưới đây:

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // Luồng 1: Khóa lock1 trước, rồi tìm cách khóa lock2
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Đã giữ lock 1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Đang chờ lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Đã giữ lock 1 & 2.");
                }
            }
        });

        // Luồng 2: Khóa lock2 trước, rồi tìm cách khóa lock1
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Đã giữ lock 2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Đang chờ lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Đã giữ lock 1 & 2.");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

Luồng 1 giữ lock1 và chờ lock2. Luồng 2 thì giữ lock2 và chờ lock1. Kết quả? Hai thằng cứ đứng nhìn nhau đến vô tận. Chương trình sẽ bị treo cứng.

Ví dụ này thì đơn giản, nhưng để thấy tận mắt hai luồng code nó "nhìn nhau" đứng hình — im lặng theo đúng nghĩa đen — thì lại là chuyện khác.


Phát Hiện Deadlock Trong Môi Trường Product — Khi Hệ Thống Bất Ngờ "Lặng Thinh"

Trên máy dev, chạy code trên thì thấy treo ngay. Nhưng trên môi trường product, 2 giờ sáng, hàng ngàn request đổ vào, và... "BÙM". Hệ thống treo.

Đây là 2 dấu hiệu kinh điển cho thấy deadlock có thể đã xảy ra:

Triệu chứngGiải thích
CPU Usage tụt xuống gần 0%Các luồng đang ở trạng thái BLOCKED — chờ đợi chứ không làm gì. CPU rảnh nhưng không có việc.
Throughput = 0, Response Time = ∞Request vẫn vào nhưng không có response nào được trả ra.

Làm Sao Để "Bắt Bệnh"?

Lúc này cần sự bình tĩnh. Anh em không thể dí debugger vào server product được. Chúng ta cần dùng "đồ nghề" chuyên dụng được JDK cung cấp sẵn.

Bước 1 — SSH vào server đang chạy ứng dụng Java.

Bước 2 — Tìm Process ID (PID) của ứng dụng:

jps -l

# Output sẽ ra danh sách các tiến trình Java đang chạy, ví dụ:
# 94518 com.xxx.DeadlockDemo

Bước 3 — Dùng jstack để "dump" trạng thái của tất cả các luồng:

jstack 94518

Kéo xuống cuối cùng, nếu có deadlock, bạn sẽ thấy một thông báo không thể rõ ràng hơn:

Found 1 deadlock.
======================
"Thread-1":
  waiting to lock monitor 0x0000... (a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000... (a java.lang.Object),
  which is held by "Thread-1"

Nó chỉ thẳng mặt vấn đề! Chỉ 4 dòng output — nhưng tôi biết chính xác thằng nào đang "cắn" thằng nào. Ở đây chỉ là văn bản khô, cái hay là lúc nó hiện ra trên terminal lúc 2 giờ sáng.


Các Cấp Độ "Fix" Deadlock — Từ Dễ Đến Khó

Khi đã biết bệnh, chữa trị sẽ dễ hơn. Để ngăn chặn deadlock, chỉ cần phá vỡ một trong các điều kiện gây ra nó.

Cách 1: Lock Ordering — Sắp Xếp Thứ Tự Khóa (Đơn giản & hiệu quả nhất)

Quy tắc rất đơn giản: Luôn luôn yêu cầu các khóa theo một thứ tự nhất quán trên toàn bộ ứng dụng.

Trong code, nếu bạn cần lock 2 object, hãy sắp xếp chúng dựa trên định danh duy nhất (System.identityHashCode) trước khi tiến hành synchronized:

public void acquireLocksInOrder(Object obj1, Object obj2) {
    Object firstLock  = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj1 : obj2;
    Object secondLock = System.identityHashCode(obj1) < System.identityHashCode(obj2) ? obj2 : obj1;

    synchronized (firstLock) {
        System.out.println(Thread.currentThread().getName() + ": Đã giữ firstLock");
        try { Thread.sleep(100); } catch (Exception e) {}

        synchronized (secondLock) {
            System.out.println(Thread.currentThread().getName() + ": Đã giữ secondLock");
        }
    }
}

Bằng cách này, tất cả các luồng đều tuân theo một "luật chơi" — deadlock sẽ không thể xảy ra.


Cách 2: Lock Timeout — Sử Dụng Lock Với Thời Gian Chờ

Cách này phá vỡ điều kiện "Giữ và Chờ". Thay vì chờ đợi vô tận, luồng sẽ chỉ cố gắng lấy khóa trong một khoảng thời gian nhất định. Nếu không lấy được, nó nhả khóa đang giữ ra, thử lại sau.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

// ... trong thread
try {
    if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
        try {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    // Thành công lấy được cả 2 lock, tiến hành xử lý
                } finally {
                    lock2.unlock(); // QUAN TRỌNG: Luôn unlock trong khối finally!
                }
            }
        } finally {
            lock1.unlock();
        }
    }
} catch (InterruptedException e) {
    // Xử lý khi luồng bị ngắt
}

Cách này phức tạp hơn trong việc quản lý logic, nhưng cực kỳ mạnh mẽ trong các hệ thống phức tạp.


Lời Kết

Yêu cầu xử lý 2000 TPS không chỉ là bài toán về hiệu năng, mà còn là bài toán về sự ổn định. Hiểu rõ, phát hiện và xử lý được Deadlock là kỹ năng bắt buộc phải có của một Senior Backend Developer — đặc biệt trong lĩnh vực đòi hỏi sự chính xác tuyệt đối như Banking.

Lý thuyết là vậy, nhưng cái anh em cần trước buổi phỏng vấn không phải đọc thêm — mà là nhìn thấy nó xảy ra một lần.

Backend Java Banking — Cần Chuẩn Bị Gì Để Xử Lý 2000 Giao Dịch/Giây?

Xem xử lý trực tuyến tại đây nếu bạn quan tâm

Chúc anh em chinh phục thành công các vòng phỏng vấn "khó nhằn" sắp tới.

Bài viết bởi anonystick — Backend Engineering · Java · System Design · Banking

Có thể bạn đã bị missing