Bài viết được lược dịch từ chương 16 cuốn Patterns of Enterprise Application Architecture của tác giả Martin Fowler

Các hệ quản trị cơ sở dữ liệu có quan hệ như: Mysql, Postgresql,… là những cơ sở dữ liệu có quan hệ được sử dụng rất rộng rãi trong các hệ thống ngày nay nhờ vào những ưu điểm như: Tính minh bạch dữ liệu cao, có đầy đủ tính ACID để đảm bảo toàn vẹn dữ liệu nhờ sử dụng transaction,….. Và trong các hệ thống yêu cầu khắt khe phải đảm bảo tính toàn vẹn của dữ liệu cao thì transaction là một giải pháp hợp lý, nhưng trong thực tế, việc sử dụng nhiều transaction ở cùng một thời điểm sẽ phát sinh ra nhiều vấn đề về xung đột dữ liệu.

Offline Concurrent Patterns là một tập hợp các cơ chế Locking được Martin Fowler mô tả trong cuốn Patterns of Enterprise Application Architecture để giải quyết bài toán về transaction trong các trường hợp khác nhau. Và trong phạm vi nội dung của bài viết này, mình sẽ trình bày 2 cơ chế Lock là: Optimistic Offline Lock và Pessimistic Offline Lock.

Optimistic Offline Lock
Với những hệ thống yêu cầu xử lý nhiều transaction, thì việc xung đột dữ liệu hay không nhất quán giữa các transaction là điều không thể tránh khỏi.

Optimistic Offline Lock sẽ giải quyết vấn đề này bằng cách validating dữ liệu thay đổi trong commit ở session đó sẽ không bị conflict thay đổi ở các session khác. Cụ thể hơn, về việc validating, ở thời điểm một session đã load xong một bản ghi, các session khác sẽ không thể thay đổi nó.

Trong Optimistic Offline Lock, một cách thực hiện rất phổ biến là mỗi bản ghi sẽ đi cùng với một version number cụ thể, khi bản ghi đó được load, version number đó sẽ được gán cho session đó cùng với các session state khác, và khi thực hiện so sánh giữa các version trong session với version hiện tại trong bản ghi thành công, lúc này dữ liệu thay đổi có thể được commit đồng thời tăng version number để tránh việc không nhất quán dữ liệu, lúc này, session với version number trước đó không thể acquire lock được nữa. Trong các RDBMS, việc verification dựa theo version number này thường được sử dụng ở các câu lệnh: UPDATE, DELETE một bản ghi nào đó.

Sau khi thực hiện thành công câu lệnh SQL, kết quả trả về sẽ là số bản ghi bị tác động bởi câu lệnh đó. Số bản ghi bằng một là khi thực hiện thành công, ngược lại, bằng không là khi bản ghi đó đã bị xóa hoặc thay đổi trước đó. Khi số bản ghi bằng không, hệ thống phải thực hiện rollback để hạn chế bất kì thay đổi nào từ việc entering data bản ghi đó. Bổ sung thêm thông tin về version number tương ứng với mỗi bản ghi, nó sẽ lưu lại thông tin người cuối cùng chỉnh sửa bản ghi đó, điều này tương đối hữu ích khi quản lý concurrency conflicts.

Ngoài cách sử dụng version cho mỗi bản ghi trong hệ thống, việc sử dụng câu lệnh where sử dụng với các thuộc tính trong bản ghi đó để thực hiện UPDATE cũng là một giải pháp khác. Việc này có ưu điểm là không phải sử dụng version number và khi thực hiện UPDATE một lượng lớn bản ghi, performance lúc này lại phụ thuộc khá nhiều vào việc sử dụng primary key index.

Việc sử dụng version với các câu lệnh UPDATE, DELETE cũng có một vài vấn đế về inconsistent read. Giả sử, với một hệ thống thanh toán, hệ thống này sẽ thực hiện tính toán chi phí, thuế của khách hàng sao cho phù hợp. Khi một session được tạo và sẽ tính toán chi phí, thuế dựa theo địa chỉ của khách hàng. Nhưng trong quá trình thực hiện tính toán, cùng lúc đó, khách hàng thay đổi địa chỉ của họ ở một session khác. Việc này dẫn đến kết quả tính thuế dựa trên địa chỉ bị sai lệch và không hợp lệ nhưng bởi vì session khởi tạo để thực hiện tính toán chưa thay đổi gì đến địa chỉ của khách hàng nên việc conflict sẽ không bị phát hiện.

Trong ví dụ trên, việc tính toán chi phí phụ thuộc vào địa chỉ chính xác của khách hàng, chính vì vậy, việc bổ sung check version địa chỉ của khách hàng, hoặc các thuộc tính khác là giải pháp không tồi nhưng việc này thường mất nhiều công sức để set up trước đó. Việc phát hiện inconsistent read thường gặp nhiều khó khăn khi transaction phụ thuộc vào kết quả của dynamic query hơn là đọc từ một bản ghi cụ thể nào đó.

Một ví dụ điển hình của Optimistic Offline Lock là các công cụ: Source Code Management (SCM) như git. Khi hệ thống SCM detect được các conflict giữa các programmer, thông thường nó sẽ tìm ra correct merge và retry lại commit đó. Optimistic thường được sử dụng trong các trường hợp mức độ conflict giữa hai transaction thấp và trong thực tế khi có conflict xảy ra, điều này ảnh hưởng trực tiếp đến người dùng, trường hợp xấu nhất là rời bỏ ứng dụng. Cách tiếp cận hợp lý nhất để quản lý các concurrency là giới hạn tối đa số lượng concurrency truy cập vào dữ liệu để giảm thiểu các conflict.

Pessimistic Offline Lock
Với cách tiếp cận của Optimistic Offline Lock, giả sử khi có nhiều user cùng truy cập vào một bản ghi trong database ở cùng một thời điểm thì khi đó, một user có thể commit dễ dàng nhưng các user còn lại sẽ bị conflict, và ít nhiều điều này ảnh hưởng đến trải nghiệm và độ tin cây của user.

Pessimistic Offline Lock là cơ chế lock giúp cho các transaction hạn chế bị conflict nhất có thể bằng cách lock lại dữ liệu trước khi sử dụng nó, và điều đó sẽ đảm bảo dữ liệu sẽ không bị ảnh hưởng bởi các transaction khác.

Cơ chế này gồm 3 phase: xác định loại lock cần phải sử dụng, xây dựng cơ chế quản lý lock, xác định quy trình cho các business transaction để sử dụng lock.

Cụ thể hơn, với phase đầu tiên xác định các loại lock, lựa chọn đầu tiên là exclusive write lock - loại lock này là business transaction thực hiện lock để có thể thay đổi dữ liệu trong session, việc này có thể tránh được conflict vì nó không cho phép hai hay nhiều transaction cùng thay đổi một bản ghi đồng thời nhưng cơ chế này lại không sử dụng lock cho việc đọc dữ liệu.

Loại lock tiếp theo là exclusive read lock - nếu có bất cứ xung đột xảy ra thì việc đọc lại dữ liệu gần nhất là điều bắt buộc và loại lock cuối cùng là read/write lock - kết hợp của của exclusive read lock và exclusive write lock, loại lock này có một vài ưu điểm như: một bản ghi không thể write lock nếu đang có bất kì transaction nào đang read lock lên nó và ngược lại, việc thực hiện single read lock sẽ hạn chế rất nhiều việc các transaction đang thực hiện chỉnh sửa bản ghi, chính vì vậy các session đang thực hiện việc đọc bản ghi đó sẽ không bị ảnh hưởng.

Việc lựa chọn đúng loại lock sẽ làm tăng concurrency trong hệ thống, giảm thiệu tính phức tạp của code đi rất nhiều. Locking không chỉ liên quan đến vấn đề kĩ thuật, nếu không sử dụng các kiểu lock cho phù hợp có thể không đạt được hiệu quả như mong muốn. Và với một cơ chế Pessimistic Offline Lock không hiệu quả sẽ dẫn đến việc không hạn chế được conflict transaction như mong muốn.

Phase tiếp theo là xây dựng cơ chế quản lý lock, nó sẽ cho phép loại bỏ bất kỳ request nào trong một transaction để acquire hoặc release một lock. Để làm được việc đó, cơ chế quản lý phải xác định được bản ghi sẽ bị lock và transaction thực hiện lock đó. Thông thường cơ chế quản lý lock sẽ không có nhiều hơn một table map lock và sẽ chỉ có một trong hai cơ chế được sử dụng là: in-memory hash table và database table. Và việc sử dụng in-memory hash table bắt buộc phải sử dụng cùng với Singleton Pattern (có thể tham khảo trong cuốn sách nổi tiếng Gang of Four), việc sử dụng in-memonry cũng có nhược điểm là chỉ sử dụng duy nhất ở single server, điều này gặp bất lợi rất nhiều khi server triển khai cluster, khi đó database-based là lựa chọn hợp lý hơn rất nhiều.

Protocol là khái niệm trong Pessimistic Offline Lock để nói về việc một business transaction bắt buộc phải sử dụng một cơ chế quản lý lock, nghĩa là bao gồm đẩy đủ các yếu tố như: xác định dữ liệu sẽ thực hiện lock, khi nào thì thực hiện lock, khi nào thì release và cách xử lý khi mà một lock không được acquire.

Cụ thể hơn, xác định data sẽ thực hiện lock phụ thuộc một phần vào lock đó khi nào thì được thực hiện và transaction nên acquire lock trước khi load dữ liệu. Với việc release lock, nó có thể được release trước khi được hoàn thành dựa vào một số yếu tố như lock type hay việc muốn sử dụng lại trong transaction đó một lần nữa. Với bất kì bản ghi nào sử dụng lock hoặc muốn truy cập vào lock table thì bắt buộc phải serialize, giả sử với in-memory lock table, có thể dễ dàng sử dụng serialize để truy cập vào toàn bộ lock manager và được hỗ trợ bởi hầu hết các ngôn ngữ lập trình hiện nay. Nếu lock table được lưu trong database, khi đó serialize phụ thuộc hoàn toàn vào database.

Exclusive read và exclusive write lock với serialization lúc này sẽ đơn giản hơn rất nhiều khi được thực hiện với cột có giá trị unique như ID. Nhưng việc lưu read/write lock vào trong database sẽ gặp đôi chút vấn đề vì việc xử lý logic yêu cầu phải read nhiều lock table chính vì vậy nó thực hiện rất nhiều câu lệnh INSERT.

Trong một system transaction ở một cấp độ hoàn toàn độc lập, serialize giúp cho việc inconsistent read giảm thiểu đi rất nhiều nhưng nó lại gây ra một vài vấn đề về performance, chính vì vậy, việc tách riêng serialize system transaction cho việc acquire lock và giảm thiểu tính độc lập cho các công việc khác có thể giải quyết vấn đề này.

Với system transaction trong Pessimistic Offline Locking, deadlock có thể xảy ra với một vài trường hợp bởi vì cơ chế locking sẽ chờ đến khi một lock hoàn toàn sẵn sàng cho việc sử dụng. Giả sử, hai user cùng muốn sử dụng tài nguyên A và B, user thứ nhất lock tài nguyên A, user thứ hai lock tài nguyên B, lúc này, cả hai transaction sẽ chờ các lock còn lại.

Và cuối cùng là việc quản lý lock timeout, nếu user bị crash trong khi đang thưc hiện transaction, lúc này transaction sẽ phải thực hiện lại là điều khó tránh khỏi. Trong nhiều trường hợp, điều này sẽ gây ra những vấn đề lớn, điển hình là trong các ứng dụng web, việc thiết lập timeout trên server là giải pháp hợp lý trong trường hợp này.

Pessimistic Offline Lock thích hợp sử dụng trong các trường hợp mà mức độ conflict giữa các concurrency session cao. Việc locking các entity trong hệ thống thực ra tạo ra một vấn đề tranh chấp dữ liệu rất lớn và chỉ nên sử dụng Pessimistic Offline Lock khi thực sự cần thiết. Và khi sử dụng phải xem xét kĩ lưỡng thời gian thực hiện của transaction, không nên sử dụng cơ chế này với single system transaction.