Queue – Sự sắp xếp có chủ đích giải quyết vấn đề concurrency
May 29, 2022 0 By Ntech DevelopersTiếp tục chủ đề Concurrency, với bài viết trước mình có đề cập tới giải pháp đầu tiên để giải quyết vấn đề này đó chính là Lock và Distributed Lock Manager. Bạn có thể tìm đọc lại.
Đối với bài viết này mình tiếp tục đặt vấn đề với giải pháp tiếp theo cho những request bất đồng bộ được thực hiện “đồng thời” một cách tốt hơn.
Cùng quay trở lại ví dụ mà mình nêu ở bài viết trước đó. Khi bạn đi mua hàng ở siêu thị, bạn sẽ có những quầy tính tiền được đặt dàn một hàng ngang. Nhiệm vụ của mỗi quầy thu ngân đó chính là giúp bạn và những người mua hàng khác thanh toán những gì mua được. Một ngày đẹp trời, số lượng khách rất đông và tất cả các quầy đều đã làm việc hết công suất và khách hàng thì vẫn quá đông đợi để thanh toán. Nếu bạn là quản lý bạn sẽ xử lý vấn đề này như thế nào?
Cũng xuất phát từ thực tế thì Queue – hàng đợi ra đời để giúp bạn sắp xếp thứ tự khách hàng đợi ở phía sau để đảm bảo, ai cũng được thanh toán, công bằng khi không ai phải đợi quá lâu, cũng như không có sự chen ngang gây ảnh hưởng.
Trong lập trình thì Queue – Hàng đợi xuất hiện là một dạng cấu trúc dữ liệu cùng với Stack – Ngăn xếp. Định nghĩa đúng thì Stack và Queue đều là các cấu trúc dữ liệu không nguyên thủy (non-primitive). Trong khi Stack sử dụng phương thức LIFO (last in first out) để truy cập và thêm các phần tử dữ liệu thì Queue sử dụng phương thức FIFO (First in first out) để truy cập và thêm các phần tử dữ liệu.
Bạn tưởng tượng Stack như một cái giỏ đề đồ chỉ có một đầu vừa đưa vào vừa lấy ra, chính vì thế những thứ đưa vào sau sẽ nằm ở bên trên và bạn buộc phải lấy nó ra trước thì mới lấy được thứ bên dưới.
Còn Queue thì như một cái đường ống có 2 đầu vậy, một đầu chỉ đảm nhận nơi đưa vào còn một đầu kia sẽ đảm nhận nơi lấy ra. Vì vậy những thứ gì đưa vào trước phía đầu này sẽ được lấy ra trước ở đầu còn lại.
Đó là về cách thức hoạt động của 2 cấu trúc dữ liệu mà có lẽ nếu bạn từng viết một ứng dụng nào đó sẽ có thể sử dụng qua. Với mỗi nhu cầu mỗi bài toán mà bạn sử dụng Stack hay Queue tương ứng.
Quay trở lại vấn đề Concurrency, cùng với ý tưởng FIFO của Queue thì nó đã được chọn là một trong những giải pháp thường dùng nhất cho vấn đề này.
Tại sao lại không phải là Stack? Bạn hình dung khi đứng đợi thanh toán quầy thu ngân, bạn là người đợi trước không lẽ bạn lại là người được thanh toán sau cùng sao. Liệu nó có quá bất công không?Sử dụng Queue để đảm bảo tính công bằng và khách quan hơn nhiều với những người đợi phía sau. Mặt khác chính vì nó có 2 đầu vào và ra riêng biệt nên nó dễ dàng trong việc phân luồng của các request không ảnh hưởng đến nhau.
Nếu bạn đã theo dõi blog của mình đã lâu có thể bạn đã đọc được một vài bài toán khác cũng sử dụng Queue rồi như “Messages Queue – Cách mà Microservice giao tiếp với nhau“, “RabbitMQ – Một message queue phổ biến mà dev nào cũng nên biết“, “Masstransit – Làm chủ message queue“
Một số bài toán sử dụng đến queue xử lý concurrency như:
– Bài toán 1: Xuất báo cáo lớn.
Bạn nghĩ khi một hệ thống phần mềm cần xuất dữ liệu lớn của cơ sở dữ liệu thì có nên để người dùng đợi vài phát để hệ thống xử lý hay không? Khi xuất dữ liệu lớn mà bạn muốn tăng trải nghiệm người dùng thì bạn sẽ phải đẩy luồng xử lý này vào Queue để những Worker bên dưới thực thi, khi nào xử lý xong sẽ báo lại cho người dùng thay vì bắt người dùng phải đợi.
– Bài toán 2: Gửi mail hoặc thông báo hàng loạt.
Khi bạn gửi mail hay thông báo cho rất nhiều người dùng cùng một lúc, bạn cũng chẳng cần phải ngồi đợi xem khi nào thì gửi xong hết từng đó người dùng. Việc của bạn cần làm là chọn danh sách những người cần gửi và hệ thống sẽ xử lý danh sách này dưới dạng Queue, và nó sẽ được thực hiện một cách từ từ và độc lập với nhau, gửi thông báo cho người dùng này xong thì tới người khác. Khi nào tất cả được gửi hết tức là Queue rỗng thì sẽ thông báo là đã hoàn tất quá trình gửi mail mà thôi.
– Bài toán 3: Xây dựng hệ thống logging và tracking.
Một quá trình hay được hệ thống phần mềm chú ý tới khi các luồng xử lý quá phức tạp cần phải lưu lại những lịch sử và những thao tác hành vi người dùng. Ví dụ khi bạn mua hàng thì sẽ trải qua rất nhiều quá trình như chọn sản phẩm, thanh toán, xác nhận, giao hàng, nhận hàng, hủy đơn… Có thể mỗi bước như vậy sẽ có những bên thứ ba can thiệp vào như shiper hay ngân hàng. Việc lưu vết các bước trong quá trình như vậy là điều bắt buộc. Tuy nhiên để đảm bảo không ảnh hưởng đến luồng xử lý chính thì những thao tác lưu vết này sẽ được gửi lên một nơi xử lý và tổng hợp. Các thao tác được thực hiện xong sau đó sẽ gửi một tín hiệu vào hệ thống logging, nơi đây sẽ tiếp nhận những thao tác đó. Những tín hiệu này được truyền qua Queue nhằm đảm bảo thứ tự không đổi của các bước thao tác.
– Bài toán 4: Xử lý dữ liệu thô thành tinh.
Có lẽ một hệ thống phần mềm không đơn giản là những thao tác hiển thị, đôi khi nó còn phải đảm nhận xử lý những dữ liệu mà tầng ứng dụng thu thập và tổng hợp lại ở nhiều nguồn khác nhau. Mỗi một nguồn dữ liệu đó nằm trên nhiều request khác nhau và bạn không thể nào đợi tổng hợp đầy đủ rồi mới bắt đầu xử lý dữ liệu thô đó được. Khi này bạn sẽ nghĩ tới Queue cho các request tới. Và việc tổng hợp dữ liệu từ các queue bên dưới giúp bạn xử lý được mượt mà hơn mà không ảnh hưởng đến các thành phần khác của hệ thống. Có một thuật ngữ chuyên ngành cho bài toán này đó chính là ETL. Extract Transform Load là thuật ngữ để chỉ 1 quá trình xử lý xử liệu từ hệ thống nguồn hay hệ thống hoạt động (Operation) tới hệ thống đích hay hệ thống phân tích và báo cáo (Analytic and Reporting). Mình sẽ có bài viết khác nói rõ hơn về thuật ngữ này.
Tóm lại, Queue chính là giải pháp thứ hai cho vấn đề concurrency, và nó cũng xử lý được rất nhiều những bài toán khác nữa. Queue không chỉ dừng lại ở mức độ là một dạng cơ sở dữ liệu đâu nhé.