Hãy tưởng tượng tình huống giả định sau:trong hệ thống quản lý bất động sản cho thuê, Nhân viên A bắt đầu chỉnh sửa thông tin liên hệ cho Rental X, thêm một số số điện thoại bổ sung. Cùng lúc đó, Nhân viên B nhận thấy lỗi đánh máy trong thông tin liên hệ về chính xác Người cho thuê X đó và thực hiện cập nhật. Vài phút sau, Nhân viên A cập nhật thông tin liên hệ của Rental X với các số điện thoại mới và ... bản cập nhật sửa lỗi đánh máy hiện đã biến mất!
Điều đó chắc chắn không phải là tuyệt vời! Và đây là một kịch bản khá tầm thường. Hãy tưởng tượng một xung đột tương tự xảy ra trong hệ thống tài chính!
Liệu chúng ta có thể tránh được những tình huống như vậy trong tương lai? May mắn thay, câu trả lời là có! Chúng tôi cần bảo vệ đồng thời và khóa - cụ thể là khóa lạc quan - để ngăn chặn những vấn đề như vậy.
Hãy cùng khám phá khóa lạc quan trong Rails REST API.
'Cập nhật bị mất' và Khóa lạc quan so với Khóa bi quan
Kịch bản mà chúng tôi vừa trải qua là một loại 'Cập nhật bị mất'. Khi hai giao dịch đồng thời cập nhật cùng một cột của cùng một hàng, giao dịch thứ hai sẽ ghi đè các thay đổi từ giao dịch đầu tiên, về cơ bản như thể giao dịch đầu tiên chưa bao giờ xảy ra.
Thông thường, vấn đề này có thể được giải quyết bằng cách:
- Đặt mức Cách ly giao dịch thích hợp, mức này sẽ xử lý sự cố ở mức cơ sở dữ liệu.
- Khóa bi quan - ngăn các giao dịch đồng thời cập nhật cùng một hàng. Giao dịch thứ hai đợi giao dịch đầu tiên kết thúc trước khi nó thậm chí đọc dữ liệu. Ưu điểm lớn ở đây là không thể hoạt động trên dữ liệu cũ. Tuy nhiên, nhược điểm lớn là nó cũng chặn việc đọc dữ liệu từ một hàng nhất định.
- Khóa lạc quan - dừng sửa đổi hàng nhất định nếu trạng thái của hàng đó tại thời điểm sửa đổi khác với khi hàng được đọc.
Vấn đề của chúng tôi không phải là về các giao dịch cơ sở dữ liệu đồng thời (giống các giao dịch kinh doanh hơn) - vì vậy giải pháp đầu tiên không thực sự áp dụng được. Điều này có nghĩa là chúng ta còn lại với khóa bi quan và khóa lạc quan.
Khóa bi quan sẽ ngăn chặn việc cập nhật bị mất xảy ra trong kịch bản giả định của chúng tôi ngay từ đầu. Tuy nhiên, nó cũng sẽ gây khó khăn cho cuộc sống của người dùng nếu nó chặn quyền truy cập vào dữ liệu trong một thời gian dài (hãy tưởng tượng nó đọc và chỉnh sửa một số trường trong 30 phút trở lên).
Khóa lạc quan sẽ ít hạn chế hơn nhiều, vì nó sẽ cho phép nhiều người dùng truy cập dữ liệu. Tuy nhiên, nếu nhiều người dùng bắt đầu chỉnh sửa dữ liệu đồng thời, thì chỉ một người có thể thực hiện thao tác. Phần còn lại sẽ gặp lỗi cho biết rằng họ đã hoạt động trên dữ liệu cũ và cần phải thử lại. Không lý tưởng, nhưng với trải nghiệm người dùng thích hợp, điều này có thể không nhất thiết phải gây đau đớn.
Hãy xem cách chúng tôi có thể triển khai khóa lạc quan trong Rails REST API giả định.
Khóa lạc quan trong API REST
Trước khi bắt đầu triển khai trong ứng dụng Rails thực tế, chúng ta hãy nghĩ về việc khóa lạc quan có thể trông như thế nào trong bối cảnh của các API REST chung.
Như đã mô tả ở trên, chúng ta cần theo dõi trạng thái ban đầu của đối tượng khi đọc nó để so sánh với trạng thái sau đó của nó trong quá trình cập nhật. Nếu trạng thái không thay đổi kể từ lần đọc cuối cùng, hoạt động được phép. Tuy nhiên, nếu nó đã thay đổi, nó sẽ không thành công.
Những gì chúng ta cần tìm ra trong ngữ cảnh của REST API là:
- Khi đọc dữ liệu của một tài nguyên nhất định, làm cách nào để chúng tôi thể hiện trạng thái hiện tại của một đối tượng và trả lại nó theo phản hồi cho người tiêu dùng?
- Người tiêu dùng nên tuyên truyền trạng thái ban đầu của tài nguyên với API như thế nào khi thực hiện cập nhật?
- API sẽ trả lại cho người tiêu dùng những gì nếu trạng thái đã thay đổi và không thể cập nhật?
Tin tốt là tất cả những câu hỏi này có thể được trả lời và xử lý bằng ngữ nghĩa HTTP.
Trong chừng mực theo dõi trạng thái của tài nguyên, chúng tôi có thể tận dụng lợi thế của Entity Tags
(hoặc ETags). Chúng tôi có thể trả lại số tham chiếu / tổng kiểm tra / phiên bản của tài nguyên trong ETag
chuyên dụng tiêu đề cho người tiêu dùng API để gửi sau cùng với yêu cầu PATCH. Chúng ta có thể sử dụng If-Match
tiêu đề, giúp máy chủ API kiểm tra xem tài nguyên đã thay đổi hay chưa. Nó chỉ là một trường hợp so sánh tổng kiểm tra / số phiên bản / bất kỳ thứ gì khác mà bạn chọn làm ETag.
Yêu cầu sẽ thành công nếu ETag
hiện tại và If-Match
các giá trị giống nhau. Nếu không, API sẽ phản hồi với 412 Precondition Failed
trạng thái, trạng thái thích hợp và biểu đạt nhất mà chúng tôi có thể sử dụng cho mục đích này.
Có một kịch bản khác có thể xảy ra. Chúng tôi chỉ có thể so sánh các ETags nếu người tiêu dùng API cung cấp If-Match
đầu trang. Nếu không thì sao? Bạn có thể bỏ qua bảo vệ đồng thời và quên đi khóa lạc quan, nhưng điều đó có thể không lý tưởng. Một giải pháp khác là đưa ra yêu cầu cung cấp If-Match
tiêu đề và trả lời với 428 Precondition Required
trạng thái nếu không.
Bây giờ chúng ta đã có một cái nhìn tổng quan chắc chắn về cách khóa lạc quan có thể hoạt động trong các API REST, hãy triển khai nó trong Rails.
Lạc quan Khóa trong Rails
Tin tuyệt vời là Rails cung cấp khả năng khóa lạc quan - chúng ta có thể sử dụng tính năng được cung cấp bởi ActiveRecord::Locking::Optimistic
. Khi bạn thêm lock_version
(hoặc bất cứ thứ gì bạn muốn, mặc dù điều đó yêu cầu khai báo bổ sung ở cấp độ mô hình để xác định cột khóa), ActiveRecord sẽ tăng nó sau mỗi lần thay đổi và kiểm tra xem phiên bản hiện tại có phải là phiên bản mong đợi hay không. Nếu nó cũ, ActiveRecord::StaleObjectError
ngoại lệ sẽ được đưa ra khi cố gắng cập nhật / hủy bỏ.
Cách dễ nhất để xử lý khóa lạc quan trong API của chúng tôi là sử dụng giá trị từ lock_version
như một ETag. Hãy làm điều này như bước đầu tiên trong RentalsController
giả định của chúng tôi :
class RentalsController
after_action :assign_etag, only: [:show]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
end
Tất nhiên, đây là một phiên bản rất đơn giản của bộ điều khiển vì chúng tôi chỉ quan tâm đến bất cứ điều gì được yêu cầu để khóa lạc quan, không phải xác thực, ủy quyền hoặc các khái niệm khác. Điều này là đủ để hiển thị ETag thích hợp cho người tiêu dùng. Bây giờ chúng ta hãy quan tâm đến If-Match
tiêu đề mà người tiêu dùng có thể cung cấp:
class RentalsController
after_action :assign_etag, only: [:show, :update]
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes).merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
Và điều đó thực sự đủ để phiên bản tối thiểu của khóa lạc quan hoạt động! Mặc dù, rõ ràng, chúng tôi không muốn trả lại 500 phản hồi nếu có bất kỳ xung đột nào. Chúng tôi sẽ tạo If-Match
cũng cần thiết cho bất kỳ bản cập nhật nào:
class RentalsController
before_action :ensure_if_match_header_provided, only: [:update]
after_action :assign_etag, only: [:show, :update]
rescue_from ActiveRecord::StaleObjectError do
head 412
end
def show
@rental = Rental.find(params[:id])
respond_with @rental
end
def update
@rental = Rental.find(params[:id])
@rental.update(rental_params)
respond_with @rental
end
private
def ensure_if_match_header_provided
request.headers["If-Match"].present? or head 428 and return
end
def assign_etag
response.headers["ETag"] = @rental.lock_version
end
def rental_params
params
.require(:rental)
.permit(:some, :permitted, :attributes)
.merge(lock_version: lock_version_from_if_match_header)
end
def lock_version_from_if_match_header
request.headers["If-Match"].to_i
end
end
Và đó là khá nhiều thứ cần thiết để triển khai tất cả các chức năng mà chúng ta đã thảo luận trước đó. Chúng tôi có thể cải thiện nhiều thứ hơn - ví dụ:bằng cách cung cấp thêm một số thông báo lỗi bên cạnh mã phản hồi - nhưng điều đó sẽ nằm ngoài phạm vi của bài viết này.
Tóm tắt:Tầm quan trọng của việc Khóa Lạc quan trong API Rails
Bảo vệ đồng thời thường bị bỏ qua khi thiết kế API REST, điều này có thể dẫn đến hậu quả nghiêm trọng.
Tuy nhiên, việc triển khai khóa lạc quan trong API Rails khá đơn giản - như được trình bày trong bài viết này - và sẽ giúp tránh các vấn đề nghiêm trọng tiềm ẩn.
Chúc bạn viết mã vui vẻ!
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!