logoMột hệ thống khó hiểu thì cũng khó thay đổi
Offline Concurrency Control - Phần 1: Hiện tượng

Series bài viết Offline Concurrency Control:

Part 1: Offline Concurrency Control - Phần 1: Hiện tượng

Part 2: Offline Concurrency Control - Phần 2: Optimistic - "Lời xin lỗi"

Part 3: Offline Concurrency Control - Phần 3: Optimistic và Inconsistent Read

***

Trong những ngày đầu của công nghệ phần mềm, các ứng dụng thường được thiết kế theo mô hình phân tán, nơi mỗi người dùng chạy ứng dụng riêng trên máy tính cá nhân của họ. Vì dữ liệu và tài nguyên được tách biệt, không ai phải lo lắng về việc người khác can thiệp vào công việc cá nhân của họ, từ nhà phát triển phần mềm cho tới người dùng cuối.

Tuy nhiên, khi các hệ thống chuyển sang mô hình tập trung – nơi một hệ thống cung cấp dịch vụ cho nhiều người dùng đồng thời – như trong các ứng dụng web hay mobile app, xung đột dữ liệu đã trở thành một thách thức cực kỳ lớn cho các nhà phát triển. Chúng ta (những software developer) gọi đó là vấn đề Concurrency (mình sẽ viết hoa từ này trong bài viết để coi đó là một thứ quan trọng).

Nếu đã xem về cách mô tả một Enterprise Application (EA), bạn sẽ thấy những đặc điểm quan trọng của loại ứng dụng này. Một số thứ có thể được nêu ra như chứa rất nhiều dữ liệu, có nhiều người dùng, dữ liệu được truy cập và thao tác đồng thời,…

EA là nơi mà Concurrency xuất hiện rõ rệt nhất. Những hệ thống này thường xử lý khối lượng lớn dữ liệu và phục vụ hàng trăm hoặc hàng nghìn người dùng đồng thời, từ các nhân viên nhập liệu, khách hàng trên website, đến hệ thống tự động hóa.

Mặc dù quan trong thế nhưng lại tồn tại một nghịch lý: Concurrency ít được quan tâm trong phát triển ứng dụng EA. Lý do chủ yếu tới từ việc nhà phát triển tập trung chủ yếu vào logic nghiệp vụ và họ ủy thác trách nhiệm kiểm soát vấn đề này cho cơ chế giao dịch (transaction control) của database. Các hệ quản trị cơ sở dữ liệu (DBMS) cung cấp các tính năng như Isolation Level, Locking để tự động đảm bảo tính toàn vẹn của dữ liệu. Điều này khiến người dùng cho rằng Concurrency là vấn đề “đã được giải quyết”.

Thực tế rất khác biệt. Để làm rõ vấn đề tại sao Transaction Control trong database là chưa đủ, ta sẽ đi dần từng khía cạnh và thống nhất với nhau một vài thuật ngữ.

Let’s go.

Ví dụ

Ví dụ 1

Tại một ứng dụng quản lý hệ thống kho hàng, Tuấn – một nhân viên quản lý và là một end user của ứng dụng, mở một giao diện quản lý sản phẩm để chỉnh sửa mô tả một sản phẩm từ “Áo thun nam” thành “Áo thun nam cao cấp”:

Hình 1: Tuấn mở thông tin sản phẩm và cập nhật mô tả sản phẩm từ “Áo thun nam” sang “Áo thun nam cao cấp”

Trong lúc Tuấn đang gõ chữ cho phần mô tả mới của mình thì Hùng – cũng là một quản lý và cũng là một end user, cũng làm như vậy. Anh mở thông tin sản phẩm ra và cập nhật số lượng tồn kho từ 50 xuống còn 45 vì anh vừa được thông báo sau khi hàng tồn kho được kiểm kê. Dù xuất phát sau Tuấn nhưng Hùng lại thực hiện rất nhanh, nhanh hơn cả Tuấn. Anh lưu thông tin sản phẩm vào cơ sở dữ liệu. Lúc này, Tuấn mới làm xong phần việc của mình, anh cũng bấm “Lưu & Đóng”. Cả hai cùng vui vẻ vì đã hoàn thành một task của mình trong ngày hôm đó.

Vì đã “nâng” lên cao cấp, sản phẩm được bán nhanh hơn trước. Tới chiều hôm đó, có một khách hàng tới mua hàng. Họ quyết định mua 3 chiếc. Nhân viên bán hàng thấy cửa hàng còn 6 chiếc trong kho, họ vui vẻ xuất hóa đơn và thanh toán cho người khách này. Khách hàng chờ lấy hàng là có thể ra về. Sự cố xảy ra, nhân viên lấy hàng báo rằng chỉ tìm thấy đúng 1 chiếc trong kho và dù đã cố gắng tìm kiếm mọi ngóc ngách vẫn không thể thấy bất cứ chiếc áo nào khác. Cửa hàng xin lỗi khách và tiến hành các thủ tục hoàn trả, làm bù sổ kế toán.

Ví dụ 2

Vẫn là anh Tuấn, muốn kiểm tra trạng thái của các sản phẩm đang bán chạy nhất để xem có cần bổ sung thêm hàng hay không. Anh vào mục “Thời trang nam”, có hai mục con: “Quần áo nam” và “Giày dép nam”.

Hình 2: Hùng muốn đếm số lượng hàng tồn kho từ hai danh mục “Quần áo nam” và “Giày dép nam”

Đầu tiên, Tuấn vào mục “Quần áo nam” và thấy số lượng đang là 10 sản phẩm. Có nhân viên vào trao đổi về thông tin của một chuyến hàng đang về, anh quay qua nói chuyện với nhân viên. Lúc này, Hùng – vẫn là anh, được nhân viên kiểm kê kho báo rằng đã tìm thấy 2 chiếc quần của nam và 3 đôi giày nam bị thất lạc cách đó mấy tuần. Hùng mở chức năng quản lý sản phẩm và bổ sung thêm số sản phẩm này vào kho hiện tại. Hùng quay sang việc khác vì đã hoàn thành một task của ngày hôm đó.

Sau đó, Tuấn quay lại việc rà soát số lượng hàng tồn kho của mình và mở tiếp vào mục “Giày dép nam” và thấy có 53 đôi. Tuấn tổng kết lại: Quần áo nam: 10 sản phẩm, giày dép nam: 53 sản phẩm, tổng cộng là 63 sản phẩm. Đáng tiếc là 63 sản phẩm không thể là câu trả lời đúng được. Câu trả lời đúng chỉ có thể là 60 hoặc 65 chứ không thể là 63.

Nhận xét

Vấn đề cơ bản

Qua hai ví dụ trên, chúng ta thấy rằng ứng dụng đều gây ra một số vấn đề cơ bản về Concurrency.

Với ví dụ 1, vấn đề được xác định là “lost updates – mất cập nhật”. Đây là vấn đề phổ biến trong những ứng dụng nhiều người dùng khi họ cùng có thể cập nhật một dữ liệu nghiệp vụ. Ở ví dụ của chúng ta, số lượng tồn kho mà Hùng cập nhật là 45 phản ánh đúng thực tế nhưng bản ghi này đã bị bản ghi mà Tuấn trước đó lấy về ghi đè lên. Thông tin của Hùng do đó bị mất vĩnh viễn. Để không xảy ra lỗi này, xử lý đúng sẽ là gì? Chắc mọi người đều thấy ngay: một là không cho Tuấn cập nhật, hai là để Hùng phải chờ Tuấn cập nhật xong.

Với ví dụ 2, hiện tượng có tên là “inconsistent read – đọc không nhất quán” (“đọc bất nhất” – tôi nghĩ cách gọi này đủ súc tích). Con số 63 sản phẩm mang tính chắp vá vì một nửa thông tin là quá khứ ghép với một nửa thông tin thuộc hiện tại. Ta thà nhận được hoặc toàn bộ là quá khứ hoặc hoàn hảo với toàn bộ là hiện tại: 60 hoặc 65! Từ hai con số này ta có giải pháp: Hoặc để Tuấn luôn đọc dữ liệu quá khứ, tại thời điểm Tuấn bắt đầu mở màn hình này, hoặc thông báo với anh ấy khi dữ liệu bị thay đổi để giúp anh ấy quay lại đọc dữ liệu đã đọc trước đó.

Đó là những vấn đề cơ bản nhất với Concurrency. Để giải quyết chúng, ta cần sử dụng các cơ chế kiểm soát khác nhau và thật không may, không có giải pháp nào là miễn phí. Các giải pháp thường dẫn tới những vấn đề mới dù ít nghiêm trọng hơn.

Tại sao chúng ta cần giải quyết những vấn đề này? Bởi vì chúng ảnh hưởng tới tính đúng đắn (correctness) của ứng dụng. Dữ liệu bị sai chứng tỏ ứng dụng bị lỗi và có thể gây ra những hậu quả khôn lường cho doanh nghiệp, tổ chức. Giải quyết vấn đề Concurrency không thể chỉ bằng cách bắt người dùng “xếp hàng” để lần lượt vào sử dụng máy tính, để tránh khỏi tình trạng cùng truy cập vào một dữ liệu tại cùng một thời điểm, ta vẫn cần phải đảm bảo hiệu năng của hệ thống vì ứng dụng EA cần phục vụ nhiều người đồng thời. Hai tiêu chí mẫu thuẫn lẫn nhau ở một mức độ nào đó khiến chúng ta không thể nào đạt được tuyệt đối cả hai.

Tại sao Database Transaction là chưa đủ?

Nhìn lại vào tiêu đề bài viết này, nếu đã từng xử lý Concurrency có thể bạn vẫn thấy khó hiểu với từ “Offline” đặt phía trước. Nó được đặt có chủ ý.

Phần trên, tôi đã cho rằng cơ chế kiểm soát transaction ở database là chưa đủ và điều này đã thể hiện rõ ràng với hai ví dụ của ta. Ở mỗi ví dụ, từng giao dịch đọc dữ liệu và cập nhật dữ liệu đều hoạt động chính xác với những gì mà ACID thể hiện. Nhưng nó không đảm bảo được tính đúng đắn giữa nhiều giao dịch. Nếu muốn chính xác, ta phải mở một giao dịch dài “long-live transaction” trong database. Bạn thấy có ổn không với trường hợp Tuấn đang mở màn hình chỉnh sửa sản phẩm thì quay sang trao đổi với đồng nghiệp, một lúc sau mới cập nhật thông tin? Transaction dưới database sẽ phải mở quá lâu, và do đó, giảm đi hiệu suất chung của hệ thống khi phục vụ nhiều người dùng đồng thời. Vì thế, cơ chế kiểm soát giao dịch của database sẽ mạnh mẽ nhất khi ta chỉ mở những “short transaction”.

Những transaction như vậy, ta gọi là System Transaction – giao dịch hệ thống. Chúng có đặc điểm là cần phải ngắn, nhanh gọn nhất có thể. Tuy nhiên, nghiệp vụ lại khác. Một giao dịch nghiệp vụ sẽ có thể cần trải qua vài giây, vài phút, cho tới vài tiếng đồng hồ mới có thể kết thúc. Anh Tuấn mở thông tin sản phẩm lên để chỉnh sửa, anh nghĩ về cái gì cần sửa, sau đó mới cập nhật. Hay một giao dịch trong hệ thống ngân hàng có thể cần đi qua các bước phê duyệt khác nhau, phải mất hàng giờ mới có thể hoàn thành. Những giao dịch như vậy ta gọi là Business Transaction.

Chữ “offline” cho thấy điều này: Đảm bảo Concurrency giữa những giao dịch hệ thống System Transaction, tức là nằm bên ngoài cơ chế của hệ thống.

Để áp dụng Business Transaction, ta phải chia nó thành các System Transaction khác nhau. Mỗi System Transaction vốn nó đã đảm bảo tính chất ACID nhưng để làm được với Business Transaction là rất khó.

Cơ chế

Qua nhận xét bên trên, ta thấy rằng lỗi xảy ra ở Business Transaction là vì những giao dịch này đang xử lý dữ liệu có thể thay đổi được nhưng lại không thể cô lập được chúng (isolation) như trong những System Transaction.

Để khắc phục sự thiếu sót này, ta có hai cơ chế chính để áp dụng: Optimistic (lạc quan) và Pessimistic (bi quan). Ngoài ra, còn có một cơ chế đặc biệt nữa, áp dụng cho những dịch vụ dựa trên event (event-driven): Tránh hoàn toàn Concurrency.

Optimistic Concurrency Control

Quay lại ví dụ 1, khi Tuấn cập nhật mô tả sản phẩm, anh ấy sẽ được ứng dụng thông báo "Cập nhật này là không hợp lệ do dữ liệu đã lỗi thời, hãy thử lại bằng cách mở lại chức năng". Đây thực ra là một "lời xin lỗi" của ứng dụng. Nếu chức năng đó đơn giản, Tuấn vui vẻ làm lại mà không thấy vấn đề gì. Nhưng nếu đó lại là một màn hình bao gồm hàng tá thông tin cần nhập thì anh ấy không thể vui được. Lời "gợi ý" của ứng dụng "...mở lại chức năng" cho thấy rằng khi vào lại chức năng này, ứng dụng sẽ mở một System Transaction ngắn để truy vấn thông tin dữ liệu mới nhất lên màn hình.

Cái tên "Optimistic" thể hiện đúng ý tưởng của cơ chế: Hãy áp dụng cơ chế này vào những chức năng có khả năng xung đột dữ liệu thấp. Optimistic đúng hơn là cơ chế "phát hiện xung đột" vì nó chỉ thực thi khi dữ liệu cập nhật vào database bị xung đột.

Ưu điểm nổi trội của nó là giúp hệ thống phục vụ được nhiều người hơn vì khóa chỉ được giữ ở thời điểm cập nhật. Nếu tôi bị xung đột dữ liệu với người khác, tôi sẽ được thông báo và tôi bắt buộc phải cập nhật dữ liệu sao cho hợp lệ thì mới có thể hoàn thành công việc của mình. Việc phát hiện xung đột về cơ bản có thể được hệ thống tự giải quyết được (như các ứng dụng quản lý source code, chúng có thể tự merge code trong nhiều tình huống) nhưng với ứng dụng doanh nghiệp, điều này là rủi ro và thường nên tránh. Trách nhiệm giải quyết nên thuộc về người dùng - những người nắm nghiệp vụ của họ.

Một ưu điểm nhỏ nữa của Optimistic là dễ triển khai hơn so với Pessimistic. Chúng ta sẽ thấy điều này ở phần demo.

Pessimistic Concurrency Control

Ở ví dụ 2, khi Tuấn đang thực hiện đếm số lượng sản phẩm trong từng danh mục con, ứng dụng có thể xử lý với yêu cầu của Hùng - người đến sau như sau: hoặc là cho phép Hùng đọc được dữ liệu nhưng không được cập nhật, hoặc là ngăn chặn Hùng hoàn toàn, khi đó, Hùng sẽ phải chờ Tuấn "nhả khóa". Cơ chế này được đặt là "Pessimistic - bi quan" vì tình huống này khả năng có xung đột dữ liệu cao nên ứng dụng cần thiết kế vậy để đảm bảo rằng những người truy cập vào cùng một dữ liệu cần phải xếp hàng lần lượt. Khả năng phục vụ số lượng người dùng lớn của hệ thống sẽ bị giảm nghiêm trọng.

Pessimistic thay vì "xin lỗi", nó "xin phép". Mỗi người dùng muốn truy cập vào một dữ liệu, cần phải "xin phép" bằng cách xin một lock từ hệ thống. Để đọc dữ liệu, ta cần một read lock. Để ghi dữ liệu, ta cần một write lock. Nhiều người có thể cùng có read lock trên dữ liệu tại một thời điểm, khi đó không ai có thể có được write lock. Còn nếu một ai đó có write lock thì không ai có thể có read lock cũng như write lock.

Tránh Concurrency trong Event-Driven

Cơ chế này sẽ được mô tả rõ trong bài viết riêng vì có kết hợp thêm một số nội dung về Event-Driven Architecture.

Patterns

Martin Fowler tổng hợp 4 patterns để xử lý Offline Concurrency:

  1. Optimistic Offline Lock
  2. Pessimistic Offline Lock
  3. Coarse-Grain Lock
  4. Implicit Lock

Trong các bài viết tiếp theo, chúng ta sẽ bàn về thiết kế và demo cho từng pattern này.

 

Bình luận
Gửi bình luận
Bình luận