Tổng quan về hệ thống hiệu năng cao và thách thức
Trong bối cảnh phát triển nhanh chóng của Internet, các hệ thống phải đối mặt với lưu lượng truy cập lớn, thường được gọi là đồng thời cao (high concurrency). Các sự kiện như chương trình khuyến mãi chớp nhoáng (flash sale) hoặc bán vé trực tuyến là những ví dụ điển hình. Nếu không có các biện pháp tối ưu hóa phù hợp, hệ thống web có thể dễ dàng rơi vào trạng thái quá tải và không khả dụng.
Bài viết này sẽ đi sâu vào các khái niệm chính và chiến lược giải quyết vấn đề trong môi trường đồng thời cao:
- Đồng thời cao là gì và tại sao nó lại đặt ra thách thức?
- Hiện tượng "siêu phát" (over-issuance) trong môi trường đồng thời cao.
- Mối quan hệ giữa đồng thời cao và lập trình đa luồng.
- Các kỹ thuật khóa (khóa bi quan và khóa lạc quan) để đảm bảo an toàn dữ liệu.
I. Những thách thức từ lưu lượng truy cập lớn
Khi đối mặt với hàng chục nghìn yêu cầu mỗi giây, hệ thống web có thể gặp phải nhiều vấn đề nghiêm trọng. Việc thiết kế và tối ưu hóa hệ thống là chìa khóa để duy trì sự ổn định và hiệu suất.
1. Thiết kế giao diện API hiệu quả
Một trang web bán hàng chớp nhoáng thường có hai thành phần chính: nội dung tĩnh (HTML, CSS, JS) và API xử lý yêu cầu phía backend. Nội dung tĩnh thường được phân phối qua CDN (Content Delivery Network), giảm tải cho máy chủ chính. Điểm nghẽn thực sự nằm ở các API backend.
- Tốc độ là yếu tố then chốt: API backend phải xử lý yêu cầu cực kỳ nhanh chóng. Sử dụng các hệ thống lưu trữ dựa trên bộ nhớ (như Redis) thay vì truy vấn trực tiếp cơ sở dữ liệu (như MySQL) là một giải pháp tốt.
- Ghi dữ liệu bất đồng bộ: Đối với các tác vụ phức tạp hoặc ghi dữ liệu nặng, nên sử dụng cơ chế ghi bất đồng bộ để tránh làm chậm phản hồi cho người dùng.
- Phản hồi tức thì: Mặc dù một số hệ thống có thể cung cấp phản hồi "trì hoãn" (người dùng không biết kết quả ngay lập tức), điều này thường gây trải nghiệm không tốt và có thể bị nghi ngờ về tính minh bạch. Ưu tiên phản hồi ngay lập tức để cải thiện trải nghiệm người dùng.
2. Thách thức "tốc độ" trong môi trường đồng thời cao
QPS (Query Per Second - số yêu cầu mỗi giây) là chỉ số quan trọng để đánh giá hiệu suất hệ thống. Giả sử một hệ thống xử lý trung bình 100ms cho mỗi yêu cầu và có 20 máy chủ web, mỗi máy có thể xử lý tối đa 500 kết nối.
QPS lý thuyết (tính toán lý tưởng):
20 máy chủ * 500 kết nối/máy chủ / 0.1 giây/yêu cầu = 100,000 QPS
Tuy nhiên, trong thực tế, dưới tải trọng cao, thời gian phản hồi trung bình sẽ tăng lên đáng kể. Ví dụ, nếu thời gian phản hồi tăng từ 100ms lên 250ms:
20 máy chủ * 500 kết nối/máy chủ / 0.25 giây/yêu cầu = 40,000 QPS
Nếu hệ thống nhận 50,000 yêu cầu/giây nhưng chỉ xử lý được 40,000 yêu cầu/giây, sẽ có 10,000 yêu cầu bị tồn đọng. Điều này dẫn đến các kết nối có sẵn bị cạn kiệt, hệ thống rơi vào trạng thái bất thường. Tình trạng này có thể leo thang thành "hiệu ứng tuyết lở" (snowball effect) khi một máy chủ quá tải làm các máy chủ khác cũng quá tải theo, dẫn đến sập toàn bộ hệ thống.
3. Khởi động lại và bảo vệ quá tải
Nếu hệ thống gặp hiệu ứng tuyết lở, việc khởi động lại đột ngột có thể không giải quyết được vấn đề mà thậm chí còn khiến hệ thống sập ngay sau khi khởi động. Cách tiếp cận tốt hơn là từ chối lưu lượng truy cập ở lớp đầu vào (ví dụ: bằng cách hiển thị trang bảo trì hoặc trả về lỗi nhanh chóng) trước khi khởi động lại các dịch vụ.
Bảo vệ quá tải (overload protection) là điều cần thiết. Nếu hệ thống phát hiện mình đang ở trạng thái tải tối đa, việc từ chối các yêu cầu mới là một biện pháp bảo vệ. Mặc dù việc này có thể gây khó chịu cho người dùng, nhưng nó cần thiết để giữ cho hệ thống không bị sập hoàn toàn.
II. Các phương pháp gian lận và phòng chống
Trong các sự kiện săn hàng giảm giá, nhiều người dùng sử dụng các công cụ tự động hoặc script để gửi một lượng lớn yêu cầu, nhằm tăng cơ hội thành công. Đây là một cuộc chiến không ngừng nghỉ giữa "tấn công" và "phòng thủ".
1. Nhiều yêu cầu từ cùng một tài khoản
Người dùng có thể dùng plugin trình duyệt hoặc công cụ để gửi hàng trăm yêu cầu từ một tài khoản duy nhất. Điều này không chỉ phá vỡ sự công bằng mà còn có thể khai thác các lỗ hổng logic. Ví dụ, trong một logic "kiểm tra trước khi ghi", nhiều yêu cầu đồng thời có thể cùng lúc thấy rằng tài khoản chưa tham gia, dẫn đến việc ghi nhận nhiều lần.
Giải pháp:
- Giới hạn yêu cầu mỗi tài khoản: Tại điểm vào của ứng dụng, chỉ cho phép một yêu cầu từ mỗi tài khoản được xử lý cùng một lúc. Các yêu cầu khác sẽ bị lọc. Có thể sử dụng Redis để thiết lập một cờ tạm thời cho mỗi tài khoản khi yêu cầu đầu tiên đến.
- Hàng đợi theo tài khoản: Xây dựng một dịch vụ đưa các yêu cầu của cùng một tài khoản vào một hàng đợi và xử lý chúng tuần tự.
2. Nhiều yêu cầu từ nhiều tài khoản (cùng IP)
Các tài khoản giả mạo ("tài khoản zombie") được tạo tự động có thể được sử dụng để spam yêu cầu. Ví dụ: săn vé tàu, đặt iPhone...
Giải pháp:
- Phát hiện tần suất IP: Giám sát tần suất yêu cầu từ cùng một địa chỉ IP. Nếu một IP gửi quá nhiều yêu cầu, có thể hiển thị mã CAPTCHA hoặc chặn IP đó.
- CAPTCHA: Mục tiêu chính là phân biệt người dùng thật với bot. CAPTCHA cần đủ phức tạp để các công cụ nhận dạng hình ảnh tự động không thể giải mã dễ dàng.
- Chặn IP: Đây là biện pháp thô bạo hơn và có thể gây "nhầm lẫn" cho người dùng thật (ví dụ: nhiều người dùng trong cùng mạng công ty có cùng IP đầu ra). Tuy nhiên, nó hiệu quả trong một số trường hợp.
3. Nhiều yêu cầu từ nhiều tài khoản (khác IP)
Những kẻ tấn công tinh vi hơn có thể sử dụng mạng proxy hoặc các máy tính bị nhiễm phần mềm độc hại để gửi yêu cầu từ nhiều địa chỉ IP khác nhau. Điều này khiến việc phân biệt với người dùng thật trở nên khó khăn.
Giải pháp:
- Đặt ngưỡng nghiệp vụ cao hơn: Ví dụ, yêu cầu tài khoản phải có một mức độ hoạt động nhất định, đã xác minh thông tin, hoặc cấp độ người dùng cao hơn mới được tham gia.
- Khai thác dữ liệu hành vi tài khoản: Phân tích hành vi của tài khoản (ví dụ: thời gian tạo, độ hoạt động, thông tin hồ sơ, lịch sử mua hàng) để phát hiện và loại bỏ các tài khoản zombie.
III. An toàn dữ liệu trong môi trường đồng thời cao
Khi nhiều luồng cùng ghi vào một tài nguyên chung, vấn đề an toàn luồng (thread safety) sẽ phát sinh. Trong các hệ thống cơ sở dữ liệu như MySQL, có thể sử dụng cơ chế khóa tích hợp. Tuy nhiên, trong các tình huống đồng thời cao, việc lạm dụng khóa cơ sở dữ liệu có thể làm giảm hiệu suất nghiêm trọng.
Một vấn đề khác là siêu phát (over-issuance), xảy ra khi số lượng hàng hóa được bán ra vượt quá số lượng tồn kho thực tế.
1. Nguyên nhân siêu phát
Giả sử còn đúng 1 sản phẩm cuối cùng. Cùng lúc, nhiều yêu cầu đồng thời đến, tất cả đều đọc được số lượng tồn kho là 1. Sau đó, tất cả đều vượt qua điều kiện kiểm tra tồn kho và cố gắng mua hàng, dẫn đến nhiều người dùng cùng mua thành công sản phẩm đó.
Ví dụ minh họa:

Trong hình trên, cả người dùng A và B đều "mua hàng thành công", dẫn đến việc có thêm một người nhận được sản phẩm so với số lượng tồn kho.
2. Chiến lược khóa bi quan (Pessimistic Locking)
Khóa bi quan giả định rằng xung đột dữ liệu sẽ xảy ra thường xuyên. Khi một giao dịch muốn sửa đổi dữ liệu, nó sẽ khóa dữ liệu đó, ngăn chặn các yêu cầu khác truy cập hoặc sửa đổi cho đến khi khóa được giải phóng.
-- Khóa bi quan trong SQL:
START TRANSACTION;
SELECT so_luong_ton_kho FROM san_pham WHERE id = ? FOR UPDATE;
-- Giảm số lượng tồn kho
UPDATE san_pham SET so_luong_ton_kho = so_luong_ton_kho - 1 WHERE id = ?;
COMMIT;
Nhược điểm: Mặc dù giải quyết được vấn đề an toàn luồng, trong môi trường đồng thời cao, việc nhiều yêu cầu phải chờ khóa sẽ làm tăng đáng kể thời gian phản hồi trung bình, dẫn đến cạn kiệt kết nối và hệ thống rơi vào trạng thái bất thường. Một số yêu cầu có thể không bao giờ nhận được khóa.
3. Chiến lược hàng đợi FIFO (First-In, First-Out)
Một cách tiếp cận khác là đưa tất cả các yêu cầu vào một hàng đợi FIFO. Cách này đảm bảo rằng không có yêu cầu nào bị bỏ đói vĩnh viễn và xử lý tuần tự từng yêu cầu một.
Nhược điểm: Trong môi trường đồng thời cao, một lượng lớn yêu cầu có thể tràn ngập hàng đợi, làm cạn kiệt bộ nhớ và khiến hệ thống không ổn định. Tốc độ xử lý của hệ thống thường không thể theo kịp tốc độ yêu cầu đến, dẫn đến hàng đợi ngày càng dài, thời gian phản hồi tăng và hệ thống quá tải.
4. Chiến lược khóa lạc quan (Optimistic Locking)
Khóa lạc quan giả định rằng xung đột dữ liệu là hiếm. Nó cho phép tất cả các yêu cầu cố gắng sửa đổi dữ liệu, nhưng yêu cầu một "phiên bản" (version) hoặc "timestamp" cụ thể để cập nhật thành công. Nếu phiên bản của dữ liệu trên cơ sở dữ liệu không khớp với phiên bản mà yêu cầu nhận được ban đầu, thì yêu cầu đó bị từ chối và thông báo thất bại (ví dụ: sản phẩm đã được mua bởi người khác).
public class ProductService {
// ...
public boolean purchaseProduct(int productId, int quantity) {
// Bước 1: Đọc thông tin sản phẩm và phiên bản hiện tại
Product product = productRepository.findById(productId);
if (product == null || product.getSoLuongTonKho() < quantity) {
return false; // Hết hàng hoặc không đủ
}
int currentVersion = product.getVersion();
// Bước 2: Cố gắng cập nhật số lượng và tăng phiên bản
int updatedRows = productRepository.updateStock(productId, quantity, currentVersion);
return updatedRows > 0; // Nếu cập nhật thành công (ảnh hưởng 1 dòng)
}
}
// Giả định trong ProductRepository.java:
// Đây là một phương thức DAO hoặc JPA/Hibernate sẽ tạo câu lệnh SQL tương tự
public int updateStock(int productId, int quantity, int expectedVersion) {
// Câu lệnh SQL với khóa lạc quan:
String sql = "UPDATE san_pham SET so_luong_ton_kho = so_luong_ton_kho - ?, version = version + 1 " +
"WHERE id = ? AND so_luong_ton_kho >= ? AND version = ?";
// Thực thi câu lệnh SQL với các tham số
// ...
// Trả về số dòng bị ảnh hưởng
return jdbcTemplate.update(sql, quantity, productId, quantity, expectedVersion);
}
Ưu điểm: Giải pháp này không cần hàng đợi và tránh được chi phí khóa lâu dài của khóa bi quan, giúp cải thiện hiệu suất tổng thể của hệ thống dưới tải trọng cao. Tuy nhiên, nó có thể tăng chi phí tính toán CPU một chút do cần đọc và so sánh phiên bản.
Nhiều dịch vụ và phần mềm hỗ trợ khóa lạc quan, ví dụ như lệnh WATCH trong Redis có thể được sử dụng để triển khai cơ chế này.
IV. Nâng cao khả năng đồng thời của hệ thống
Khả năng đồng thời cao (High Concurrency) là một yếu tố thiết yếu trong thiết kế kiến trúc hệ thống phân tán. Có hai cách tiếp cận chính để tăng khả năng đồng thời của hệ thống:
1. Mở rộng chiều dọc (Scale Up)
Tức là cải thiện hiệu suất của từng máy chủ đơn lẻ. Điều này bao gồm:
- Nâng cấp phần cứng: Tăng CPU, RAM, ổ cứng SSD.
- Tối ưu hóa phần mềm:
- Sử dụng đa tiến trình/đa luồng để tận dụng nhiều lõi CPU.
- Giảm chuyển đổi ngữ cảnh (context switching).
- Giảm các lời gọi hệ thống.
- Tối ưu hóa cấu trúc dữ liệu và thuật toán, sử dụng pool bộ nhớ.
- Cải thiện mô hình I/O: DMA, I/O bất đồng bộ, epoll, sendfile, ánh xạ bộ nhớ.
- Cải tiến chiến lược đồng thời của máy chủ (ví dụ: một tiến trình xử lý nhiều kết nối với I/O bất đồng bộ).
Tuy nhiên, hiệu suất của một máy chủ đơn lẻ luôn có giới hạn.
2. Mở rộng chiều ngang (Scale Out)
Là giải pháp cuối cùng cho các kiến trúc phân tán hiệu năng cao, tức là tăng số lượng máy chủ.
Trong kiến trúc phân tầng của Internet, mở rộng chiều ngang được áp dụng khác nhau ở từng lớp:
- Lớp Proxy ngược: Có thể sử dụng DNS Round Robin hoặc các bộ cân bằng tải chuyên dụng.
- Lớp Web/Ứng dụng: Sử dụng Nginx, Apache HTTP Server với nhiều instance.
- Lớp Dịch vụ: Sử dụng service connection pool hoặc service mesh.
- Lớp Cơ sở dữ liệu: Chia nhỏ dữ liệu (sharding) theo phạm vi hoặc hàm băm (hashing), hoặc sử dụng sao chép (replication) và đọc/ghi tách biệt.
Sau khi triển khai mở rộng chiều ngang ở các lớp, hệ thống có thể tăng hiệu suất bằng cách thêm máy chủ, đạt được hiệu suất gần như không giới hạn về mặt lý thuyết.
V. Đa luồng và đồng thời: Mối quan hệ
1. Đa luồng (Multithreading) là gì?
Đa luồng là một mô hình lập trình cho phép một chương trình (một tiến trình) thực hiện nhiều phần công việc khác nhau (các luồng) "cùng lúc". Trên thực tế, CPU thường chuyển đổi rất nhanh giữa các luồng, tạo cảm giác như chúng đang chạy song song. Mục tiêu là giúp một tiến trình hoàn thành nhiều nhiệm vụ đồng thời, nâng cao hiệu quả sử dụng tài nguyên.
2. Đồng thời (Concurrency) là gì?
Trong bối cảnh hệ thống phân tán và Internet, đồng thời thường đề cập đến khả năng của một máy chủ hoặc hệ thống xử lý nhiều yêu cầu truy cập cùng lúc từ nhiều người dùng hoặc client khác nhau.
Các chỉ số thường dùng để đo lường khả năng xử lý đồng thời bao gồm:
- Thời gian phản hồi (Response Time): Thời gian hệ thống mất để phản hồi một yêu cầu.
- Thông lượng (Throughput): Số lượng yêu cầu được xử lý trong một đơn vị thời gian (thường đồng nghĩa với QPS).
- Số lượng người dùng đồng thời (Concurrent Users): Số lượng người dùng có thể sử dụng hệ thống một cách bình thường cùng một lúc.
3. Mối quan hệ giữa Đa luồng và Đồng thời
Có thể hiểu rằng đồng thời là mục tiêu, còn đa luồng là một phương tiện hoặc chiến lược để đạt được mục tiêu đó.
- Hệ thống đồng thời cao có nghĩa là hệ thống có thể xử lý nhiều yêu cầu song song.
- Đa luồng là một kỹ thuật lập trình cho phép các tác vụ trong một chương trình chạy gần như song song, tận dụng tài nguyên CPU tốt hơn để phục vụ nhiều yêu cầu đồng thời.
Ví dụ, một máy chủ web như Tomcat sử dụng một pool các luồng để xử lý các yêu cầu HTTP đến. Mỗi yêu cầu có thể được gán cho một luồng riêng biệt, cho phép máy chủ xử lý nhiều yêu cầu đồng thời mà không bị chặn.
Tuy nhiên, đa luồng không phải là giải pháp duy nhất cho đồng thời. Các mô hình khác như lập trình bất đồng bộ, lập trình hướng sự kiện (event-driven programming) cũng được sử dụng. Nhưng đa luồng thường là nền tảng hoặc là một phần không thể thiếu trong nhiều mô hình đồng thời.
VI. Đa luồng và Đồng thời trong dự án thực tế
1. Các trường hợp sử dụng đa luồng
Trong phát triển web Java, các framework và container như Servlet container (Tomcat) thường đã quản lý đa luồng ngầm định. Tuy nhiên, đa luồng vẫn được sử dụng rộng rãi trong các kịch bản cụ thể:
- Ứng dụng máy chủ (Server-side programming): Để xử lý nhiều client kết nối đồng thời.
- Tính toán nặng CPU: Ví dụ, xử lý dữ liệu phức tạp, mã hóa/giải mã, nén/giải nén.
- Giao diện người dùng (UI programming): Để thực hiện các tác vụ tốn thời gian ở một luồng riêng biệt, tránh làm đơ giao diện chính.
- Tác vụ nền (Background tasks/Jobs): Chạy các tác vụ định kỳ hoặc không yêu cầu phản hồi tức thì.
Lưu ý: Đa luồng không phải lúc nào cũng cải thiện hiệu suất cho các tác vụ I/O nặng (ví dụ: sao chép file đơn lẻ), vì tài nguyên I/O thường có giới hạn vật lý. Tuy nhiên, nó có thể hữu ích để tách luồng đọc/ghi I/O ra khỏi luồng xử lý chính.
2. Ví dụ cơ bản về đa luồng: Bán vé
Dưới đây là ví dụ minh họa cách triển khai đa luồng trong Java. Lưu ý rằng để đảm bảo an toàn dữ liệu khi nhiều luồng truy cập tài nguyên chung, cần sử dụng các cơ chế đồng bộ hóa.
Sử dụng `Thread` (mỗi luồng có tài nguyên riêng)
Khi mỗi `Thread` có một instance riêng của tài nguyên (ví dụ: `ticket`), chúng sẽ không chia sẻ trạng thái chung, do đó không xảy ra vấn đề an toàn luồng với tài nguyên đó.
class TicketVendorA extends Thread {
private int availableTickets = 5;
private String vendorName;
public TicketVendorA(String name) {
this.vendorName = name;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (availableTickets > 0) {
System.out.println("Vendor " + vendorName + " sold ticket " + (availableTickets--));
try {
Thread.sleep(50); // Giả lập thời gian xử lý
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
System.out.println("Vendor " + vendorName + " reports no more tickets.");
break;
}
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
TicketVendorA vendorX = new TicketVendorA("X");
TicketVendorA vendorY = new TicketVendorA("Y");
vendorX.start();
vendorY.start();
}
}
Trong ví dụ trên, mỗi `TicketVendorA` có biến `availableTickets` riêng, nên chúng hoạt động độc lập. Điều này không minh họa được vấn đề an toàn luồng khi chia sẻ tài nguyên.
Sử dụng `Runnable` (chia sẻ tài nguyên) và đồng bộ hóa
Khi nhiều luồng cùng thực thi một instance `Runnable` duy nhất, chúng sẽ chia sẻ các biến thành viên của instance đó, dẫn đến khả năng xung đột dữ liệu. Cần sử dụng `synchronized` để bảo vệ tài nguyên chung.
class SharedTicketSeller implements Runnable {
private int ticketsLeft = 5; // Tài nguyên chung
@Override
public void run() {
// Khối synchronized để bảo vệ việc truy cập và sửa đổi `ticketsLeft`
synchronized (this) { // Đồng bộ hóa trên đối tượng SharedTicketSeller hiện tại
for (int i = 0; i < 5; i++) {
if (ticketsLeft > 0) {
System.out.println(Thread.currentThread().getName() + " sold ticket " + (ticketsLeft--));
try {
Thread.sleep(50); // Giả lập thời gian xử lý
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else {
System.out.println(Thread.currentThread().getName() + " reports no more tickets.");
break;
}
}
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
SharedTicketSeller sellerTask = new SharedTicketSeller(); // Một nhiệm vụ chung
Thread thread1 = new Thread(sellerTask, "Seller 1");
Thread thread2 = new Thread(sellerTask, "Seller 2");
thread1.start();
thread2.start();
}
}
Trong ví dụ này, cả `thread1` và `thread2` đều chạy cùng một instance `sellerTask`, do đó chúng chia sẻ biến `ticketsLeft`. Khối `synchronized(this)` đảm bảo rằng chỉ một luồng có thể thực hiện phần code bên trong tại một thời điểm, ngăn chặn hiện tượng siêu phát.
3. Singleton đa luồng trong Servlet
Trong các máy chủ web như Tomcat, Servlet thường là singleton (chỉ có một instance duy nhất) nhưng được xử lý bởi nhiều luồng. Điều này có nghĩa là mọi yêu cầu đến cùng một Servlet sẽ được xử lý bởi các luồng khác nhau nhưng trên cùng một đối tượng Servlet. Do đó, cần đảm bảo an toàn luồng khi truy cập các biến thành viên của Servlet.
Ví dụ về một Singleton an toàn luồng sử dụng mẫu "Double-Checked Locking":
package com.example.app;
import java.util.UUID;
public class AppSingleton {
// volatile đảm bảo rằng các thay đổi đối với 'instance' được nhìn thấy ngay lập tức bởi các luồng khác
private static volatile AppSingleton instance = null;
// Biến thành viên của singleton (không nên dùng để lưu trữ trạng thái request-specific)
private String uniqueId;
// Constructor private để ngăn việc tạo đối tượng trực tiếp
private AppSingleton() {
System.out.println("AppSingleton: Constructor called.");
this.uniqueId = UUID.randomUUID().toString().replace("-", "");
// Giả lập khởi tạo phức tạp
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static AppSingleton getInstance() {
// Kiểm tra lần 1: Giảm overhead của synchronized nếu instance đã được tạo
if (instance == null) {
// Khóa trên đối tượng Class để đảm bảo chỉ một luồng có thể vào khối này
synchronized (AppSingleton.class) {
// Kiểm tra lần 2: Đảm bảo instance chưa được tạo bởi một luồng khác
if (instance == null) {
instance = new AppSingleton();
System.out.println("AppSingleton: Instance created with ID: " + instance.uniqueId);
}
}
}
return instance;
}
// Phương thức giả lập xử lý yêu cầu
public void processRequest() {
String requestId = UUID.randomUUID().toString().substring(0, 8);
System.out.println(Thread.currentThread().getName() +
" processing request " + requestId +
" via Singleton ID: " + this.uniqueId);
}
}
// Lớp giả lập một task của Tomcat
class TomcatWorker implements Runnable {
@Override
public void run() {
AppSingleton singletonInstance = AppSingleton.getInstance();
singletonInstance.processRequest();
}
}
// Lớp chính để chạy thử nghiệm
public class AppMain {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new TomcatWorker(), "RequestThread-" + (i + 1)).start();
}
}
}
Trong ví dụ này, `AppSingleton` được tạo một lần duy nhất. Phương thức `processRequest` được gọi bởi nhiều luồng. Nếu `uniqueId` được dùng để lưu trạng thái riêng của từng request, sẽ xảy ra vấn đề. Trong Servlet, các biến request-specific nên được lưu trong `HttpServletRequest` hoặc các đối tượng cục bộ của luồng.
4. Kiến trúc phân tán và cân bằng tải
Trong các dự án thương mại điện tử, để xử lý đồng thời cao, kiến trúc thường chuyển sang mô hình phân tán và sử dụng các công nghệ như Nginx để cân bằng tải (load balancing). Các thành phần như Redis được sử dụng làm bộ nhớ đệm (cache) hoặc hàng đợi để giảm tải cho cơ sở dữ liệu và cải thiện tốc độ phản hồi. Xu hướng hiện nay là phát triển theo kiến trúc microservices.