Chào các bạn, ở 2 phần trước về ghi log
[Java] Phần 12 – Log 1: Tầm quan trọng của ghi log trong phát triển phần mềm
[Java] Phần 13 – Log 2: Tích hợp log4j vào phần mềm
mình đã đề cập về tầm quan trọng của ghi log, một số nguyên tắc khi ghi log, và hướng dẫn cách tích hợp log4j vào một chương trình java.
Bài này mình sẽ hướng dẫn các bạn tự tạo một tính năng ghi log bất đồng bộ vào database.
Để làm được tính năng ghi log vào database thì Log4j hoàn toàn có khả năng đáp ứng sẵn việc này. Nhưng ở phần này mình sẽ hướng dẫn cách tự tạo một module ghi log vào database để các bạn có thể hiểu rõ hơn về cơ chế ghi log bất đồng bộ.
Trước tiên, để ghi log vào database thì cần phải có table lưu log, và tạo một sequence để lấy value cho trường ID của bảng log

Trên ứng dụng, ta sẽ tạo một entity tương ứng với table đã tạo để thuận tiện cho việc insert dữ liệu vào bảng log.

Nếu bình thường chỉ nhắm mục đích ghi log thành công thì các bạn hoàn toàn có thể thực hiện trực tiếp câu lệnh insert into dữ liệu vào bảng log theo cách như sau

Tư tưởng là như vậy, đoạn code trên mình đã tách các xử lý ngoài phạm vi bài viết như việc kết nối database, đóng connection,... vào 1 class khác đặt tên DatabaseUtils để tránh làm loãng bài viết. Các bạn có thể xem chi tiết các phần đó trong source full ở cuối bài.
Nhưng như bài trước mình đã đề cập đến nguyên tắc khi ghi log là không được làm ảnh hưởng đến thời gian xử lý của nghiệp vụ chính. Như vậy đoạn code trên đã phạm phải nguyên tắc này, vì có thể việc connect databaseexecute lệnh insert kia sẽ chiếm 1 phần thời gian nhất định.
Để khắc phục việc này ta cần xử lý việc connect database và execute insert trong 1 thread riêng. Đơn giản nhất là new Thread mới để thực thi Runnable.

Nhưng cách này sẽ gặp một vấn đề khá nguy hiểm đó là khi chức năng nghiệp vụ bị gọi liên tục, dẫn đến việc phải tạo quá nhiều Thread trong ứng dụng mà không giải phóng được, việc này sẽ làm ảnh hưởng rất nhiều đến hiệu năng của ứng dụng và ảnh hưởng cả đến server. Do vậy các bạn nên sử dụng một dạng pool để quản lý và hạn chế việc sinh Thread vô tội vạ như trên. Ở đây mình thường sử dụng ExecutorService để đảm nhiệm công việc này, khi nào hay ở chỗ nào cần ghi log thì chỉ cần submit log vào rồi kệ cho thằng ExecutorService xử lý ghi log dần dần, như vậy sẽ không làm ảnh hưởng đến thời gian xử lý của nghiệp vụ chính.
Đầu tiên mình tạo một abstract class Task để quản lý danh sách các đối tượng log cần save vào database, abstract class Task được implements Callable để execute nghiệp vụ.

Sau đó mình tạo một abstract class ThreadManager để khai báo và định nghĩa khung làm việc cho quá trình ghi log.

Phần này sẽ tương đối phức tạp đối với bạn nào chưa được tiếp xúc nhiều với xử lý đa luồng trong Java Core. Mình sẽ mô tả qua như sau:
BlockingQueue sourceQueue: hàng đợi lưu các đối tượng log cần insert, bên ngoài thêm phần tử vào hàng đợi thông qua method submit()
ExecutorService executorService: Service thực thi, đã được cấu hình cố định chỉ cho phép tạo ra 5 Thread đồng thời, dù có bị gọi nhiều lần thì cũng chỉ tối đa 5 thread được tạo ra, tránh việc tạo quá nhiều thread ảnh hưởng hiệu năng server
ArrayList items: danh sách lưu các đối tượng được lấy ra từ hàng đợi sourceQueue truyền cho doProcess để insert vào database
Luồng xử lý: ở đây mình có 1 thread được tạo ra và start 1 lần duy nhất bằng method listen(), thread này sẽ tồn tại trong suốt quá trình chạy của ứng dụng. Bên trong thread xử lý việc liên tục đọc dữ liệu từ hàng đợi sourceQueue để add vào danh sách items. Điều quan trọng ở đây là khi thoả mãn 1 trong 2 điều kiện: danh sách items vượt quá BATCH_SIZE hoặc các phần tử được lưu trong danh sách items quá TIME_OUT thì sẽ đẩy items đi insert. Điều kiện này để tránh việc insert quá nhiều bản ghi 1 lúc, và tránh việc lưu dữ liệu trong items quá lâu mà không được insert. Sau khi đã gửi đi doProcess thì phải clear danh sách itemsreset biến đếm thời gian timeout. Các bạn có thể xem ảnh dưới để hình dung dễ hơn.


Sau khi tạo xong 2 abstract tổng quát ở trên, mình sẽ tạo các class xử lý việc ghi log.
Đầu tiên là class LogThread được extends Task, có nhiệm vụ insert 1 List đối tượng Logs vào database. Phần này tương tự như ví dụ trên nhưng khác là insert 1 list thôi.

Tiếp theo mình sẽ tạo class LogManager được extends ThreadManager, nhiệm vụ là nhận danh sách đối tượng cần ghi log từ hàng đợi trong ThreadManager để submit vào ExecutorService.
Đoạn này có thể sẽ có 1 số bạn thắc mắc tại sao không dùng insert theo lô. Bởi vì ở ThreadManager đã đọc từ hàng đợi ra, và tối đa cũng chỉ có 10 phần tử được đẩy vào list để doProcess, nghĩa là bản chất đã là batch rồi không cần xử lý ở đây nữa.

Đến đây chắc các bạn cũng mường tượng ra, khi phát sinh thêm nhiều loại log khác cần ghi vào table khác, hoặc send qua FTP,… hoặc bất kỳ task vụ gì khác cần xử lý bất đồng bộ thì đều có thể tái sử dụng abstract class ThreadManager làm khung, chỉ cần định nghĩa lại các class khác tương tự như LogThread để xử lý nghiệp vụ chính và LogManager để đọc dữ liệu từ hàng đợi. Như vậy các bạn đã có được một khung tiến trình cơ bản.
Cuối cùng để sử dụng được bộ ghi log bất đồng bộ vào database đã tạo ở trên thì mình viết 1 đoạn test trong Main.java

Ở đoạn test này mình kết hợp cả cách ghi không dùng thread ban đầu và cách ghi bất đồng bộ mới dựng để đo thời gian xử lý
Thử chạy và xem kết quả

Nhìn kết quả ta thấy sự khác biệt rất lớn. Với ghi log bất đồng bộ thì thời gian xử lý chỉ dưới 10 mili giây, bởi vì cái gì chạy lâu thì nó để cho thằng khác xử lý rồi. Còn insert thông thường thì phải mất 1.5 đến 2 giây (lâu gấp 500 lần), rất đáng kể để sử dụng.
Ở đoạn test trên mình chỉ đưa hết vào hàm main để test cho dễ, các bạn có thể init cái ThreadManager trong 1 khối static để sử dụng luôn hàm submit mà không phải khởi tạo lại nhiều lần, hoặc nếu các bạn dùng Spring thì nên tạo 1 Bean cho LogManager với hàm khởi tạolisten()destroystop() để sử dụng

Khi cần dùng Bean logManager này ở đâu thì chỉ cần Autowired như mọi Bean khác là có thể sử dụng được.

Tới demo này thì các bạn đã tạo được một module ghi log bất đồng bộ vào database đủ dùng và an toàn. Các bạn hoàn toàn có thể bổ sung các tính năng ghi log bằng cách gọi một API lưu log khác, gửi log sang server khác qua FTP,…. thay vì chỉ ghi vào database, tùy theo mục đích sử dụng mà không ảnh hưởng đến ứng dụng chính.
Chúc các bạn thành công 🙂

One Reply to “[Java] Phần 14 – Log 3: Ghi log bất đồng bộ vào Database sử dụng ExecutorService”

Bình luận