Có hai phương pháp chính giải quyết vấn đề khóa chết: Ngăn ngừa khóa chết, phát hiện khóa chết và khôi phục. Giao thức ngăn ngừa khóa chết đảm bảo rằng hệ thống sẽ không bao giờ đi vào trạng thái khóa chết. Lược đồ phát hiện khóa chết và khôi phục (deadlock-detection and deadlock-recovery
scheme) cho phép hệ thống đi vào trạng thái khóa chết và sau đó cố gắng khôi phục. Cả hai phương pháp đều có thể dẫn đến việc cuộn lại giao tác (rollback). Phòng ngừa khóa chết thường được sử dụng nếu xác suất hệ thống đi vào khóa chết cao, phát hiện và khôi phục hiệu quả hơn trong các trường hợp còn lại.
Phƣơng thức phòng ngừa khóa chết (Deadlock prevention protocols). Có hai cách tiếp cận phòng ngừa khóa chết: Một đảm bảo không có chờ đợi vòng tròn xảy ra bằng cách sắp thứ tự các yêu cầu khóa hoặc đòi hỏi tất cả các khóa được nhận cùng nhau. Một cách tiếp cận khác gần hơn với khắc phục khóa chết và thực hiện cuộn lại thay vì chờ đợi một khóa. Chờ đợi là tiểm ẩn của khóa chết [1].
Lược đồ đơn giản nhất dưới cách tiếp cận thứ nhất đòi hỏi mỗi giao tác khóa tất cả các mục dữ liệu trước khi nó bắt đầu thực hiện. Hơn nữa, hoặc tất cả được khóa trong một bước hoặc không mục nào được khóa. Giao thức này có hai bất lợi chính: một là khó dự đoán, trước khi giao tác bắt đầu, các mục dữ liệu nào cần được khóa, hai là hiệu suất sử dụng hạng mục dữ liệu rất thấp do nhiều mục có thể bị khóa nhưng không được sử dụng trong một thời gian dài [1,2].
Lược đồ phòng ngừa khóa chết khác là áp đặt một thứ tự bộ phận trên tất cả các mục dữ liệu và yêu cầu một giao tác khóa một mục dữ liệu theo thứ tự được xác định bởi thứ tự bộ phận này.
Cách tiếp cận thứ hai để phòng ngừa khóa chết là sử dụng ưu tiên và cuộn lại quá trình. Với ưu tiên, một giao tác T2 yêu cầu một khóa bị giữ bởi giao tác T1, khóa đã cấp cho T1 có thể bị lấy lại và cấp cho T2, T1 bị cuộn lại. Để điều khiển ưu tiên, ta gán một tem thời gian duy nhất cho mỗi giao tác. Hệ thống sử dụng các tem thời gian này để quyết định một giao tác phải chờ hay
cuộn lại. Việc khóa vẫn được sử dụng để điều khiển cạnh tranh. Nếu một giao tác bị cuộn lại, nó vẫn giữ tem thời gian cũ của nó khi tái khởi động. Hai lược đồ phòng ngừa khóa chết sử dụng tem thời gian khác nhau được đề nghị [1]:
(1) Lược đồ Wait-Die dựa trên kỹ thuật không ưu tiên. Khi giao tác Ti yêu cầu một mục dữ liệu bị chiếm bởi Tj , Ti được phép chờ chỉ nếu nó có tem thời gian nhỏ hơn của Tj nếu không Ti bị cuộn lại (die). (2) Lược đồ Wound-Wait dựa trên kỹ thuật ưu tiên. Khi giao tác Ti
yêu cầu một hạng mục dữ liệu hiện đang bị giữ bởi Tj, Ti được phép chờ chỉ nếu nó có tem thời gian lớn hơn của Tj, nếu không Tj bị cuộn lại (Wounded).
Một điều quan trọng là phải đảm bảo rằng, mỗi khi giao tác bị cuộn lại, nó không bị chết đói (starvation) có nghĩa là nó sẽ không bị cuộn lại lần nữa và được phép tiến triển.
Cả hai lược đồ Wound-Wait và Wait-Die đều tránh được sự chết đói: tại một thời điểm, có một giao tác với tem thời gian nhỏ nhất. Giao tác này không thể bị yêu cầu cuộn lại trong cả hai lược đồ. Do tem thời gian luôn tăng và do các giao tác không được gán tem thời gian mới khi chúng bị cuộn lại, một giao tác bị cuộn lại sẽ có tem thời gian nhỏ nhất (vào thời gian sau) và sẽ không bị cuộn lại lần nữa.
Tuy nhiên, có những khác nhau lớn trong cách thức hoạt động của hai lược đồ [1]:
* Trong lược đồ Wait-Die, một giao tác già hơn phải chờ một giao tác trẻ hơn giải phóng hạng mục dữ liệu. Như vậy, giao tác già hơn có xu hướng bị chờ nhiều hơn. Ngược lại, trong lược đồ
Wound-Wait, một giao tác già hơn không bao giờ phải chờ một giao tác trẻ hơn.
* Trong lược đồ Wait-Die, nếu một giao tác Ti chết và bị cuộn lại vì nó đòi hỏi một hạng mục dữ liệu bị giữ bởi giao tác Tj, khi đó Ti có thể phải tái phát ra cùng dãy các yêu cầu khi nó khởi động lại. Nếu mục dữ liệu vẫn bị chiếm bởi Tj, Ti bị chết lần nữa. Như vậy, Ti có thể bị chết vài lần trước khi nhận được mục dữ liệu cần thiết. Trong lược đồ Wound-Wait, Giao tác Ti bị lỗi và bị cuộn lại do Tj yêu cầu mục dữ liệu nó chiếm giữ. Khi Ti khởi động lại, và yêu cầu mục dữ liệu, bây giờ, đang bị Tj giữ, Ti chờ. Như vậy, có ít cuộn lại hơn trong lược đồ Wound-Wait.
Một vấn đề nổi trội đối với cả hai lược đồ là có những cuộn lại không cần thiết vẫn xảy ra.
Phát hiện khóa chết và Timeout (Deadlock detection and Timeouts) Lƣợc đồ dựa trên Timeout: Một cách tiếp cận khác để quản lý khóa chết được dựa trên khóa timeout. Trong cách tiếp cận này, một giao tác đã yêu cầu một khóa phải chờ nhiều nhất một khoảng thời gian xác định. Nếu khóa không được cấp trong khoảng thời gian này, giao tác được gọi là mãn kỳ (time out), giao tác tự cuộn lại và khởi động lại. Nếu có một khóa chết, một hoặc một vài giao tác liên quan đến khóa chết sẽ timeout và cuộn lại, để các giao tác khác tiến triển. Lược đồ này nằm trung gian giữa phòng ngừa khóa chết và phát hiện và khôi phục khóa chết [1].
Lược đồ timeout dễ thực thi và hoạt động tốt nếu giao tác ngắn và nếu sự chờ đợi lâu là do khóa chết. Tuy nhiên, khó quyết định được khoảng thời gian timeout. Lược đồ này cũng có thể đưa đến sự chết đói
Phát hiện khóa chết và khôi phục
Nếu một hệ thống không dùng giao thức phòng ngừa khóa chết, khi đó lược đồ phát hiện và khôi phục phải được sử dụng. Một giải thuật kiểm tra trạng thái của hệ thống được gọi theo một chu kỳ để xác định xem khóa chết có xẩy ra hay không. Nếu có, hệ thống phải khôi phục lại từ khóa chết, muốn vậy hệ thống phải:
* Duy trì thông tin về sự cấp phát hiện hành các mục dữ liệu cho các giao tác cũng như các yêu cầu mục dữ liệu chưa được giải quyết. * Cung cấp một thuật toán sử dụng các thông tin này để xác định hệ
thống đã đi vào trạng thái khóa chết chưa.
* Phục hồi từ khóa chết khi phát hiện được khóa chết đã xảy ra.
Khóa chết có thể được mô tả chính xác bằng đồ thị có hướng được gọi là đồ thị chờ (wait - for graph). Đồ thị này gồm một cặp G = < V, E >, trong đó V là tập các đỉnh và E là tập các cung. Tập các đỉnh gồm tất cả các giao tác trong hệ thống. Mỗi phần tử của E là một cặp Ti → Tj, nó chỉ ra rằng Ti chờ Tj giải phóng một mục dữ liệu nó cần.
Khi giao tác Ti yêu cầu một mục dữ liệu đang bị giữ bởi giao tác Tj khi đó cung Ti → Tj được xen vào đồ thị. Cạnh này bị xoá đi chỉ khi giao tác Tj không còn giữ mục dữ liệu nào mà Ti cần.
Khóa chết tồn tại trong hệ thống nếu và chỉ nếu đồ thị chờ chứa một chu trình. Mỗi giao tác tham gia vào chu trình này được gọi là bị khóa chết. Để phát hiện khóa chết, hệ thống phải duy trì đồ thị chờ và gọi theo một chu kỳ thủ tục tìm kiếm chu trình. Ta xét ví dụ sau:
Hình 2.5 - Đồ thị chờ (Phi chu trình)
Do đồ thị không có chu trình nên hệ thống không trong trạng thái khóa chết. Bây giờ, giả sử T28 yêu cầu một mục dữ liệu được giữ bởi T27, cung T28 → T27 được xen vào đồ thị, điều này dẫn đến tồn tại một chu trình T26 → T27 → T28 → T26 có nghĩa là hệ thống rơi vào tình trạng khóa chết và T26, T27, T28 bị khóa chết.
Vấn đề đặt ra là khi nào thì chạy thủ tục phát hiện ? câu trả lời phụ thuộc hai yêu tố sau:
(1) Khóa chết thường xảy ra hay không ?
(2) Bao nhiêu giao tác sẽ bị ảnh hưởng bởi khóa chết
Nếu khóa chết thường xảy ra, việc chạy thủ tục phát hiện diễn ra thường xuyên hơn. Các mục dữ liệu được cấp cho các giao tác bị khóa chết sẽ không sẵn dùng cho các giao tác khác đến khi khóa chết bị phá vỡ. Hơn nữa, số chu
T25 T26 T28 T27 T25 T26 T28 T27
trình trong đồ thị có thể tăng lên. Trong trường hợp xấu nhất, ta phải gọi thủ tục phát hiện mỗi khi có một yêu cầu cấp phát không được cấp ngay.
Sự chết đói (Starvation). Vấn đề khác có thể xảy ra khi sử dụng khóa là sự chết đói, nó xuất hiện khi một giao tác không thể tiến hành trong một chu kỳ thời gian không rõ ràng trong khi các giao tác khác trong hệ thống vẫn duy trì như thường lệ. Điều này có thể xuất hiện nếu lược đồ chờ đợi khóa các mục dữ liệu không đúng đắn, cho giao tác này ưu tiên hơn các giao tác khác. Một cách để giải quyết vấn đề chết đói này là phải có lược đồ chờ đợi đúng đắn, chẳng hạn sử dụng hàng đợi “đến trước - phục vụ trước”; các giao tác được cho phép khóa mục dữ liệu theo trật tự ban đầu được yêu cầu khóa. Lược đồ khác cho phép một số giao tác có mức độ ưu tiên hơn các giao tác khác chỉ là tăng thêm sự ưu tiên của một giao tác chờ đợi lâu hơn, cho đến khi nó nhận được quyền ưu tiên cao nhất và tiên hành. Sự chêt đói cũng có thể xuất hiện do việc lựa chọn nạn nhân nếu thuật toán chọn giao tác giống nhau khi nạn nhân lặp lại nhiều lần, nguyên do là sự thực thi của nó hủy bỏ hoặc không bao giờ hoàn thành. Thuật toán có thể sử dụng quyền ưu tiên cao hơn đối với các giao tác đã được hủy bỏ nhiều lần để tránh vấn đề này. Các lược đồ wait-die và wound-wait ở trên tránh sự chết đói.
Khôi phục từ khóa chết. Khi thuật toán phát hiện xác định được sự tồn tại của khóa chết, hệ thống phải khôi phục từ khóa chết. Giải pháp chung nhất là cuộn lại một vài giao tác để phá vỡ khóa chết. Ba việc cần phải làm là [1]:
(1) Chọn nạn nhân. Đã cho một tập các giao tác bị khóa chết, ta phải xác định giao tác nào phải cuộn lại để phá vỡ khóa chết. Ta sẽ cuộn lại các giao tác sao cho giá phải trả là tối thiểu. Nhiều nhân tố xác định giá của cuộn lại:
b. Giao tác đã sử dụng bao nhiêu hạng mục dữ liệu
c. Giao tác cần bao nhiêu hạng mục dữ liệu nữa để hoàn tất. d. Bao nhiêu giao tác bị cuộn lại.
(2) Cuộn lại (Rollback). Mỗi khi ta đã quyết định được giao tác nào phải bị cuộn lại, ta phải xác định giao tác này bị cuộn lại bao xa. Giải pháp đơn giản nhất là cuộn lại toàn bộ: bỏ dở giao tác và bắt đầu lại nó. Tuy nhiên, sẽ là hiệu quả hơn nếu chỉ cuộn lại giao tác đủ xa như cần thiết để phá vỡ khóa chết. Nhưng phương pháp này đòi hỏi hệ thống phải duy trì các thông tin bổ xung về trạng thái của tất cả các giao tác đang chạy.
(3) Sự chết đói (Starvation). Trong một hệ thống trong đó việc chọn nạn nhân dựa trên các nhân tố giá, có thể xảy ra là một giao tác luôn là nạn nhân của việc chọn này và kết quả là giao tác này không bao giờ có thể hoàn thành. Phải đảm bảo việc chọn nạn nhân không đưa đến chết đói. Một giải pháp xem số lần bị cuộn lại của một giao tác như một nhân tố về giá.
2.2 Điều khiển đồng thời dựa vào Timestamps Ordering
Sử dụng khóa, kết hợp với giao thức khóa 2 kỳ đảm bảo khả năng tuần tự của lịch biểu. Các lịch biểu có thể tuần tự được tạo ra bởi khóa 2 kỳ có các lịch biểu tuần tự tương đương dựa trên thứ tự các giao tác thực thi khóa các mục dữ liệu mà nó có được. Nếu một giao tác cần mục dữ liệu mà nó đã được khóa, có có thể bị buộc phải đợi cho đến khi mục dữ liệu được giải phóng. Một cách tiếp cận khác mà vẫn đảm bảo khả năng tuần tự cần phải sử dụng giao tác tem thời gian (timestamps) để thực thi giao tác theo thứ tự với một lịch biểu tuần tự tương đương [1,10].
2.2.1 Tem thời gian (Timestamps)
Tem thời gian (timestamp) là định danh duy nhất được tạo ra bởi DBMS để nhận dạng một giao tác. Các giá trị đặc trưng của tem thời gian được gán theo trật tự các giao tác được đưa vào hệ thống xem xét, bởi vậy tem thời gian có thể được nghĩ đến như thời gian bắt đầu giao tác. Chúng ta gán tem thời gian của giao tác T là TS(T). Các kỹ thuật điều khiển đồng thời dựa trên trật tự tem thời gian không sử dụng khóa; do đó, không xuất hiện khóa chết [1].
Tem thời gian có thể được tạo ra theo vài cách. Cách thứ nhất là sử dụng bộ đếm và giá trị được tăng lên mỗi khi gán cho một giao tác. Trong lược đồ này tem thời gian của giao tác được đánh số 1, 2, 3,.... Một bộ đếm có giới hạn giá trị lớn nhất, bởi vậy hệ thống phải đặt lại giá trị bộ đếm về 0 một cách định kỳ khi không có giao tác nào thực thi trong một số khoảng thời gian ngắn. Cách khác để thực thi tem thời gian là sử dụng giá trị ngày tháng/ thời gian hiện tại của đồng hồ hệ thống và chắc chắn rằng không có 2 giá trị tem thời gian được tạo ra trong cùng một tíc tắc của đồng hồ.
2.2.2 Thuật toán Timestamp Ordering
Ý tưởng của lược đồ này là thứ tự giao tác dựa trên tem thời gian. Một lịch biểu với các giao tác tham gia vào là có thể tuần tự, và lịch biểu tuần tự tương đương có các giao tác theo thứ tự giá trị tem thời gian, được gọi là TO - Timestamp Ordering. Thuật toán phải đảm bảo là đối với mỗi mục dữ liệu được truy cập bởi các thao tác xung đột trong lịch biểu, thứ tự mục dữ liệu đươc truy cập không vi phạm thứ tự tuần tự. Để làm được điều này, thuật toán kết hợp với mỗi mục dữ liệu X hai giá trị tem thời gian (TS):
(1) Read_TS(X): Biểu thị tem thời gian lớn nhất của giao tác bất kỳ đã đọc thành công mục dữ liệu X, có nghĩa là, Read_TS(X) = TS(T) khi T là giao tác trẻ nhất đã đọc X thành công [1,10].
(2) Write_TS(X): Biểu thị tem thời gian lớn nhất của giao tác bất kỳ đã ghi thành công mục dữ liệu X, có nghĩa là, Write_TS(X) = TS(T) khi T là giao tác trẻ nhất đã ghi X thành công [1,10].
Các tem thời gian này được cập nhật mỗi khi một Write hoặc một Read
mới được thực hiện.
Thứ tự tem thời gian cơ sở (basic TO).
Khi giao tác T cố gắng đưa ra thao tác read_item(X) hoặc write_item(X), thuật toán basic TO so sánh tem thời gian của giao tác T với tem thời gian của read_TS(X) và write_TS(X) để đảm bảo thứ tự tem thời gian thực thi giao tác không bị vi phạm. Nếu thứ tự này bị vi phạm, giao tác T bị hủy bỏ và sau đó được đưa vào hệ thống xem xét như một giao tác mới với một tem thời gian mới. Nếu giao tác T bị huy bỏ và được cuộn lại, bất kỳ giao tác T1 nào đã dùng giá trị đã được ghi bởi T cũng phải được cuộn lại, tương tự, bất kỳ giao tác T2 nào đã sử dụng giá trị đã được ghi bởi T1 cũng phải được cuộn lại và cứ