Mặc dù không có thuốc chữa bách bệnh để khắc phục tình trạng cuộc đua, nhưng nhiều điều kiện có thể được khắc phục bằng các chiến lược phù hợp. Lấy ví dụ về hai loại tình trạng cuộc đua và ba cách để giải quyết chúng.
Điều kiện cuộc đua là gì?
Tôi đã tìm kiếm một định nghĩa hay về tình trạng chủng tộc và đây là định nghĩa tốt nhất tôi tìm thấy:
Tình trạng dồn đuổi là hành vi không lường trước được gây ra bởi nhiều quy trình tương tác với các tài nguyên được chia sẻ theo thứ tự khác với dự kiến.
Điều này khá hay và vẫn chưa rõ ràng về cách các điều kiện chủng tộc hiển thị trong Rails .
Khi sử dụng Rails, chúng tôi luôn làm việc với nhiều quy trình - mỗi yêu cầu hoặc công việc nền là một quy trình riêng lẻ có thể hoạt động hầu như độc lập với các quy trình khác.
Chúng tôi cũng luôn làm việc với các tài nguyên được chia sẻ. Ứng dụng có sử dụng cơ sở dữ liệu quan hệ không? Đó là một nguồn tài nguyên được chia sẻ. Ứng dụng có sử dụng một số loại máy chủ bộ nhớ đệm không? Đúng, đó là tài nguyên được chia sẻ. Bạn có sử dụng một số loại API bên ngoài không? Bạn đoán đúng rồi - đó là tài nguyên được chia sẻ.
Có hai loại ví dụ về điều kiện chủng tộc mà tôi muốn nói đến và sau đó đề cập đến cách tiếp cận giải quyết chúng.
Đọc-sửa-ghi
Danh mục đọc-sửa-ghi là một loại điều kiện chạy đua trong đó một quy trình sẽ đọc các giá trị từ tài nguyên được chia sẻ, sửa đổi giá trị trong bộ nhớ và sau đó cố gắng ghi lại giá trị đó vào tài nguyên được chia sẻ. Điều này có vẻ rất đơn giản khi chúng ta nhìn nó qua lăng kính của một quá trình duy nhất. Nhưng khi quy trình thứ hai xuất hiện, nó có thể dẫn đến một số hành vi không lường trước được.
Hãy xem xét mã trông như thế này:
class IdeasController < ActionController::Base def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! end end
Ở đây chúng ta đang đọc ( Idea.find(params[:id]) ), sửa đổi ( @idea.votes += 1 ), sau đó viết ( @idea.save! ).
Chúng ta có thể thấy rằng điều này sẽ làm tăng số phiếu bầu cho một ý tưởng lên một. Nếu có một ý tưởng không có phiếu bầu, nó sẽ kết thúc bằng một phiếu bầu. Tuy nhiên, nếu có yêu cầu thứ hai đến và đọc ý tưởng từ cơ sở dữ liệu trong khi nó vẫn không có phiếu bầu nào và tăng giá trị đó trong bộ nhớ, chúng ta có thể gặp phải tình huống có hai phiếu bầu đến cùng lúc - nhưng kết quả cuối cùng là số phiếu bầu trong cơ sở dữ liệu chỉ có một.
Điều này còn được gọi là tình trạng cuộc đua cập nhật bị mất .
Kiểm tra rồi hành động
Danh mục kiểm tra rồi hành động là một loại điều kiện chạy đua trong đó dữ liệu được tải từ tài nguyên dùng chung và tùy thuộc vào giá trị hiện tại, chúng tôi xác định xem có cần thực hiện một hành động hay không.
Một trong những ví dụ kinh điển về cách điều này hiển thị là trong quá trình xác thực validates_uniqueness_of trong Rails, như sau:
class User < ActiveRecord::Base validates_uniqueness_of :email end
Với việc xác thực tại chỗ, Rails sẽ kiểm tra xem có người dùng nào hiện có với email đó không. Nếu không có cái nào khác, nó sẽ hoạt động bằng cách lưu người dùng vào cơ sở dữ liệu. Tuy nhiên, điều gì sẽ xảy ra nếu yêu cầu thứ hai thực thi cùng một mã vào cùng một thời điểm? Chúng ta có thể rơi vào tình huống cả hai yêu cầu đều kiểm tra để xác định xem có dữ liệu trùng lặp hay không (và không có dữ liệu nào) — khi đó cả hai sẽ hành động bằng cách lưu dữ liệu, dẫn đến một người dùng trùng lặp trong cơ sở dữ liệu.
Giải quyết các điều kiện cuộc đua
Không có viên đạn bạc nào để khắc phục các điều kiện của cuộc đua, nhưng có một số chiến lược có thể được tận dụng cho bất kỳ vấn đề cụ thể nào. Có ba loại chính để loại bỏ điều kiện chủng tộc:
1. Xóa phần quan trọng
Mặc dù điều này có thể được coi là xóa mã vi phạm, nhưng đôi khi bạn có thể cấu trúc lại mã để nó không dễ bị ảnh hưởng bởi các điều kiện cạnh tranh. Những lúc khác, bạn có thể xem xét các hoạt động nguyên tử. Hoạt động nguyên tử là hoạt động mà không có quy trình nào khác có thể làm gián đoạn hoạt động nên bạn biết rằng nó sẽ luôn thực thi dưới dạng một đơn vị.
Đối với ví dụ đọc-sửa-ghi, thay vì tăng số phiếu ý tưởng trong bộ nhớ, chúng có thể được tăng trong cơ sở dữ liệu:
@ideas.increment!(:votes)
Điều đó sẽ thực thi sql trông như thế này:
UPDATE "ideas" SET "votes" = COALESCE("votes", 0) + 1 WHERE "ideas"."id" = 123
Việc sử dụng điều này sẽ không phải tuân theo các điều kiện chủng tộc tương tự.
Đối với ví dụ kiểm tra rồi hành động, thay vì cho phép Rails xác thực mô hình, chúng ta có thể chèn bản ghi trực tiếp vào cơ sở dữ liệu bằng một upsert:
Điều đó sẽ chèn bản ghi vào cơ sở dữ liệu. Nếu có xung đột trên email (cần có một chỉ mục duy nhất trên email), nó sẽ đơn giản bỏ qua phần chèn.
2. Phát hiện và phục hồi
Đôi khi bạn không thể loại bỏ phần quan trọng. Có thể có một hành động nguyên tử, nhưng nó không hoạt động theo cách mà mã yêu cầu. Trong những tình huống đó, bạn có thể thử phương pháp phát hiện và phục hồi. Với phương pháp này, các biện pháp bảo vệ được thiết lập sẽ thông báo cho bạn nếu điều kiện chạy đua xảy ra. Bạn có thể hủy bỏ một cách duyên dáng hoặc thử lại thao tác.
Đối với ví dụ đọc-sửa-ghi, điều này có thể được thực hiện bằng . Khóa lạc quan được tích hợp vào Rails và có thể cho phép phát hiện khi có nhiều quy trình đang hoạt động trên cùng một bản ghi cùng một lúc. Để bật khóa tối ưu, bạn chỉ cần thêm cột lock_version vào bảng của mình và Rails sẽ tự động kích hoạt nó.
change_table :ideas do |t| t.integer :lock_version, default: 0 end
Sau đó, khi bạn cố gắng cập nhật một bản ghi, Rails sẽ chỉ cập nhật nó nếu lock_version có cùng phiên bản trong bộ nhớ. Nếu không, nó sẽ đưa ra một ngoại lệ ActiveRecord::StaleObjectError , ngoại lệ này có thể được giải cứu để xử lý nó. Việc xử lý nó có thể là retry hoặc có thể chỉ là một thông báo lỗi được báo cáo lại cho người dùng.
Đối với ví dụ kiểm tra rồi hành động, điều này có thể được thực hiện bằng một chỉ mục duy nhất trên cột, sau đó giải cứu ngoại lệ khi lưu giữ dữ liệu.
add_index :users, [:email], unique: true
Với một chỉ mục duy nhất, nếu dữ liệu đã tồn tại trong cơ sở dữ liệu với email đó, Rails sẽ đưa ra lỗi ActiveRecord::RecordNotUnique và lỗi đó có thể được cứu và xử lý thích hợp.
begin user = User.create(email: "[email protected]") rescue ActiveRecord::RecordNotUnique user = User.find_by(email: "[email protected]") end
sự bình thường
Để thử lại các hành động, điều quan trọng là toàn bộ hoạt động không có hiệu lực. Điều này có nghĩa là nếu một thao tác được thực hiện nhiều lần thì kết quả sẽ giống như khi nó chỉ được áp dụng một lần.
Ví dụ: hãy tưởng tượng nếu một công việc gửi email và nó được thực hiện bất cứ khi nào phiếu bầu của một ý tưởng được thay đổi. Sẽ thực sự tệ nếu một email được gửi đi cho mỗi lần thử lại. Để thực hiện thao tác bình thường, bạn có thể tạm dừng gửi email cho đến khi toàn bộ thao tác bỏ phiếu hoàn tất. Ngoài ra, bạn có thể cập nhật việc triển khai quy trình gửi email để chỉ gửi email nếu số phiếu bầu thay đổi so với lần cuối cùng nó được gửi đi. Nếu tình trạng dồn đuổi xảy ra và bạn cần thử lại, lần thử gửi email đầu tiên có thể dẫn đến kết quả không hoạt động và bạn có thể kích hoạt lại lần nữa một cách an toàn.
Nhiều hoạt động có thể không bình thường — chẳng hạn như xếp hàng công việc nền, gửi email hoặc gọi API của bên thứ ba.
3. Bảo vệ mã
Nếu bạn không thể phát hiện và khôi phục, bạn có thể cố gắng bảo vệ mã. Mục tiêu ở đây là tạo một hợp đồng trong đó tại một thời điểm chỉ có một quy trình có thể truy cập tài nguyên được chia sẻ. Thực tế là bạn đang loại bỏ tính đồng thời - vì chỉ một quy trình có thể có quyền truy cập vào tài nguyên được chia sẻ nên chúng tôi có thể tránh được hầu hết các điều kiện chạy đua. Tuy nhiên, sự đánh đổi là càng loại bỏ nhiều đồng thời thì ứng dụng càng chậm vì các quy trình khác sẽ đợi cho đến khi chúng được phép truy cập.
Điều này có thể được xử lý bằng cách sử dụng khóa bi quan được tích hợp trong Rails. Để sử dụng , bạn có thể thêm lock vào các truy vấn đang được xây dựng và Rails sẽ yêu cầu cơ sở dữ liệu giữ khóa hàng trên các bản ghi đó. Cơ sở dữ liệu sau đó sẽ ngăn chặn bất kỳ quá trình nào khác lấy được khóa cho đến khi quá trình đó được thực hiện. Đảm bảo bọc mã trong transaction để cơ sở dữ liệu biết khi nào nên mở khóa.
Idea.transaction do @idea = Idea.lock.find(params[:id]) @idea.votes += 1 @idea.save! end
Nếu không thể khóa cấp hàng thì có thể sử dụng các công cụ khác như Redlock hoặc with_advisory_lock. Điều này sẽ cho phép khóa một khối mã tùy ý. Sử dụng cái này có thể đơn giản như thế này:
email = "[email protected]" User.with_advisory_lock("user_uniqueness_#{email}"} do User.find_or_create_by(email: email) end
Những chiến lược này sẽ khiến các tiến trình phải đợi cho đến khi nhận được khóa. Vì vậy, họ cũng sẽ muốn có một số hình thức thời gian chờ để ngăn quá trình chờ đợi mãi mãi — cũng như một số cách xử lý những việc cần làm trong trường hợp hết thời gian chờ.
Mặc dù không có thuốc chữa bách bệnh để khắc phục tình trạng chủng tộc, nhưng nhiều điều kiện chủng tộc có thể được khắc phục thông qua các chiến lược này. Tuy nhiên, mỗi vấn đề có một chút khác biệt nên chi tiết về giải pháp có thể khác nhau. Bạn có thể xem để biết chi tiết hơn về điều kiện cuộc đua.
Giới thiệu về tác giả
Kyle d'Oliveira
Kyle đam mê biến những ý tưởng trừu tượng thành những phần mềm có thể hoạt động được. Anh ấy là kỹ sư phần mềm chính tại . Khi không phát triển, Kyle thích thú với những món ăn tuyệt vời và những nhà máy bia thủ công gần nhà ở Vancouver, Canada.