images
27/09/2022 04:55 am

Java Map - Phần 2

Thế giới thật không đơn giản như logic ta nghĩ. Nghi ngờ của tôi đến từ việc có nhiều luồng xử lý đồng thời được thực hiện trong phần xử lý lưu mã vé của người dùng.

Về bản chất của web chạy ngôn ngữ Java là nó sẽ tạo ra thread pool để xử lý các request của người dùng. Do đó các request sẽ được xử lý đồng thời trên những máy chủ hỗ trợ chạy đa luồng. Nhìn lại cấu trúc dữ liệu và review code tôi thấy bug có vẻ khá tiềm tàng ở đoạn trên.


Trong lập trình, với mỗi lỗi xảy ra, trình tự bạn làm là:

- Xác định nguyên nhân, dò tìm và tái hiện lỗi

- Sửa lỗi

- Kiểm thử lại để đảm bảo lỗi sẽ hết

Việc xác định nguyên nhân thường khá mất thời gian nếu không có manh mối cụ thể. Nhiều tình huống ta có thể tái hiện lại theo các bước của người dùng là có thể thấy lỗi. Tuy nhiên một hệ thống chạy thật thường chịu tác động của nhiều luồng dữ liệu khác nhau nên cách làm trên đối với lỗi đang gặp phải là không thể.


Do đó tôi phải tìm cách viết thêm test để kiểm tra. Giả thiết là có nhiều luồng của người dùng cùng đặt vé vào một thời điểm. Việc ghi nhận vé cần đảm bảo yếu tố: vé được đặt phải lưu lại trong hệ thống và phải tồn tại khi thực hiện kiểm tra. Tôi tạo ra 2 luồng ghi nhận vé người dùng và kiểm tra xem tổng số vé ghi nhận đúng không. Việc kiểm tra tổng số nhanh và dễ dàng hơn kiểm tra từng vé riêng lẻ.

Mời các bạn cùng review.


@Test

public void testTicketSize() throws Exception { 

   int size = 10;

   Thread t1 = new Thread(()-> {

       for(int i = 0; i < size; i++) {

           ticketMap.put(UUID.randomUUID().toString(), String.valueOf(i));

       }

   });

   Thread t2 = new Thread(()-> {

       for(int i = 0; i < size; i++) {

           ticketMap.put(UUID.randomUUID().toString(), String.valueOf(i));

       }

   });

   t1.start();

   t2.start();

   // Wait for all thread done

   t1.join();

   t2.join();

   Assert.assertEquals(size * 2, ticketMap.size());

}


Hàm test đơn giản chỉ là: nếu tôi có một Map và chạy song song 2 luồng, mỗi luồng đưa vào đó 10 phần tử, vậy khi lấy ra tôi phải có tổng cộng 2 x 10 = 20 phần tử (2 là số luồng - thread). Tuy nhiên test này thi thoảng pass, rồi failed, Nếu số size càng tăng thì tỉ lệ failed càng lớn.


size = 10:

size = 100:

size=1000:


Đến đây chắc các bạn đã hiểu vấn đề. Trường hợp lỗi này từ chuyên môn được gọi là race condition: xung đột này xảy ra do nhiều luồng cùng can thiệp vào một tài nguyên. 


Thông thường khi bạn đã xác định đúng nguyên nhân rồi thì có 2 khả năng: lỗi có thể sửa dễ dàng hoặc rất khó sửa vì mức độ khó về kĩ thuật hoặc độ phức tạp, mức độ ảnh hưởng. Đối với lỗi xử lý vé ở bài toán này thì việc sửa lại khá đơn giản. Có khá nhiều cách để tránh lỗi, điển hình nhất có thể có 3 cách: dùng synchonized, lock, hoặc đổi cấu trúc dữ liệu để hỗ trợ xử lý đồng thời nhiều luồng với ConcurrentHashMap. Hiển nhiên cách làm thứ 3 là nhẹ nhàng nhất và bạn không phải thay đổi nhiều code. 


Như vậy thay vì:

Map<String, String> ticketMap = new HashMap<>();


bạn sẽ dùng:

Map<String, String> ticketMap = new ConcurrentHashMap<>();


Các kết quả chạy test đều xanh. Team chúng tôi thở phào vì có thể đưa bản vá lỗi ngay lên hệ thống thật.


Bình luận:


Các lỗi về race condition là lỗi khó nhưng cũng có thể coi là lỗi khá sơ đẳng mà đôi khi lập trình viên rất hay quên. Khi ứng dụng của bạn có lỗi khi chạy, cho dù về mặt lập trình là những xử lý vô cùng đơn giản nhưng có thể gây ra những thiệt hại nghiêm trọng. Đặc biệt khi nó xảy ra trên hệ thống thật lúc đang có rất nhiều người dùng. Lỗi có thể khiến cho tâm trí bạn cực kỳ hoang mang vì sức ép từ nhiều phía: người sử dụng dịch vụ gọi điện chửi bới, Sếp gọi điện hỏi lỗi là gì và tại sao lại có lỗi, có sửa được luôn không? còn bạn thì nghĩ: sao test chán rồi có bao giờ bị lỗi thế này đâu? giờ làm sao để hệ thống chạy bình thường đây???


Câu chuyện trên là câu chuyện hư cấu của mình thôi Tuy nhiên thực tế thì mình gặp khá nhiều lỗi xung đột tài nguyên này trên các ngôn ngữ khác nhau. Lỗi thường tinh tế và khó nhìn ra hơn nhiều do độ phức tạp của bài toán nghiệp vụ. Ngày nay, với sự hỗ trợ của các CPU đa nhân và các nhân đều hỗ trợ Hyper Threading, phần mềm được hưởng lợi nếu viết đúng cách sẽ chạy nhanh hơn. Đi kèm với hiệu năng cao hơn là rủi ro về xung đột tài nguyên. Các lập trình viên nên cân nhắc và xử lý khéo léo để tránh các lỗi nghiêm trọng liên quan tới đa luồng khi phát triển hệ thống. 


Mời các bạn xem bài Java Map - Phần 1 nhé.


- Tech Zone -

Thư giãn chút nào!!!

Bài viết liên quan