Làm thế nào để tránh Race Condition in Rails

Bài viết được sự cho phép của tác giả Tùng Nguyễn

Tôi nghĩ chắc các bạn developer ai cũng đã nghe qua từ Race condition rồi. Trong bài này tôi sẽ đề cập đên 2 vấn đề chính: Race Condition là gì!? và Làm sao phòng tránh nó?

Race condition rất khó để debug đặc biệt là khi mà chính tôi cũng không biết nó có phải Race Condition hay không!

Race Condition là gì?

Khi 2 hoặc nhiều user vì 1 lý do nào đó cùng lúc read và update cùng 1 record ở cùng 1 thời điểm, nó sẽ dẫn đến 1 vài problem không mong muốn. Tôi lấy ví dụ 1 customer click vào button Pay ở trang checkout của website e-commerce. Một điều hoàn toàn có thể xảy ra là customer có thể charge cùng 1 order 2 lần vì 2 request charge có thể đến gần như cùng 1 lúc. Những trường hợp giống như vậy gọi là Race Condition.

Các cách phòng tránh Race Condition

Locking

Khi hệ thống của tôi cho phép multiple users access và edit cùng 1 record, tôi cần phải tìm cách tránh trường hợp khi 1 user overrides changes của 1 user khác mà thậm chí không check xem đó có phải record mình muốn update không.

Tôi lấy ví dụ, tôi có 1 cái web e-commerce, trong đó nhiều admin đều có quyền manage kho sản phẩm. Race Condition xảy ra khi nhiều Admin cùng update thông tin cùng 1 sản phẩm:

  1. Admin 1 thêm sản phẩm mới tên “Bench Fitness Equipment for Home”
  2. Admin 2 vào thấy có sản phẩm mới tạo có tên chưa đầy đủ và không đáp ứng về mặt thương mại. Nên quyết định đổi tên lại thành “RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version”
  3. Cùng lúc đó, Admin 1 thấy rằng tên mình đặt chưa hay cho lắm nên vào đổi lại thành “Adjustable Workout Foldable Bench Fitness Equipment for Home Gym”
  4. Admin 1 save tên sản phẩm sau khi Admin update vài milliseconds, đồng thời vô tình override tên sản phẩm đầy đủ nhất mà Admin 2 mới đổi.

Locking là 1 trong nhiều cách để ngăn chặn kich bản như vậy có thể xảy ra.

  Làm thế nào để xây dựng social network bằng Ruby on Rails

  Mẫu bảng mô tả công việc lập trình Ruby on Rails

Optimistic locking

Để triển khai optimistic locking in Rails, tôi add lock_version column vào table mà tôi muốn lock. Rails sẽ tự động check cột này trước khi update record. Cứ mỗi lần record được update thì cột lock_version sẽ tự tăng lên, và cơ chế locking sẽ chắc chắn rằng nếu có record được update 2 lần thì record được save sau cùng sẽ raise StaleObjectError.

product1 = Product.find(1)
product2 = Product.find(1)

product1.name =  "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"
product1.save

product2.name = "Adjustable Workout Foldable Bench Fitness Equipment for Home Gym"
product2.save # Raises an ActiveRecord::StaleObjectError

Pessimistic locking

Pessimistic locking sẽ lock cái record đó cho đến khi tất cả transaction trên record đó kết thúc. Một khi record được lock, user khác sẽ không thể nào thay đổi record đó cho đến khi lock được release. Pessimistic locking sử dụng cho row-locking bằng SELECT…FOR UPDATE và 1 vài loại lock khác nữa.

Để sử dụng Pessimistic locking trong Rails, thay vì sử dụng ActiveRecord::Base#find thì tôi đổi thành ActiveRecord::QueryMethods#lock:

product = Product.lock.find(1) #lock the record

product.name = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"

product.save! #release the lock

Mặt khác tôi cũng có hể sử dụng ActiveRecord::Base#lock! để lock record bàng IDs của nó:

product = Product.find(1)
  order = Order.find(order_id)
  ActiveRecord::Base.transaction do
    product.lock! 
    product.name  = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"
    product.save!
    order.paid!
  end

Hoặc tôi có thể start transaction và lock record cùng lúc bằng with_lock:

product = Product.find(1)
product.with_lock do #lock the record
product.name = "RELIFE REBUILD YOUR LIFE Sit Up Bench Adjustable Workout Foldable Bench Fitness Equipment for Home Gym Ab Exercises New Version"

product.save!
end

Tham khảo các vị trí tuyển Ruby on Rails lương cao cho bạn

Sử dụng unique Index thay vì unique validation

Rails’ ActiveRecord validation không phải database-level validation, nó là application-level validation, và nó hoạt đông tuyệt vời nếu không có Race Condition.

Bây giờ lấy ví dụ thực tế trong cuộc sống. Tôi có feature sign-up, tôi yêu cầu user chỉ được sử dụng 1 số điện thoại để đăng ký và không được trùng nhau, vì thế tôi viết logic validate như sau:

class User < ApplicationRecord
  validates :phone_number, uniqueness: true
end

Giờ giả sử user click sign-up button 2 hay nhiều lần cách nhau chỉ vài millisecond, sẽ dẫn đến kịch bản như sau:

  1. Request 1 – Kiểm tra xem người dùng có tồn tại với số điện thoại đó hay không và tiếp tục, vì không tìm thấy người dùng nào.
  2. Request 2 – Kiểm tra xem người dùng có tồn tại với số điện thoại đó hay không và tiếp tục, vì không tìm thấy người dùng nào.
  3. Request 1 – Tạo user mới và insert vào database.
  4. Request 2 – Tạo user mới và insert vào database.

Request 2 vượt qua unique validate vì tại thời điểm kiểm tra xem có người dùng tồn tại hay không, Request 1 vẫn chưa lưu số điện thoại vào database. Do đó, cả hai Request cuối cùng đều có thể insert new user vào database.

Để phòng tránh trường hợp Race Condition như vậy tôi só thể thêm unique index constraint, sẽ thực hiện validate ở database-level:

class AddUniqueIndexToUsers < ActiveRecord::Migration
  def change
    # Have the database raise an exception anytime any process tries to
    # submit a record that has a code duplicated for any particular account
    add_index :users, :phone_number, unique: true
  end
end

Lời kết

Race condition, nếu không ngăn chặn sẽ ảnh hưởng tới sự toàn vẹn dữ liệu và đôi khi sẽ dẫn đến các vấn đề về security. Hy vọng các bạn có thêm chút kiến thức cơ bản trong công cuộc đi code của mình và tránh được những đêm dài chỉ để ngồi debug.

Happy Hacking

Bài viết gốc được đăng tải tại omatsuri.blog, biết thêm về tác giả tại LinkedIn

Xem thêm Việc làm Developer hấp dẫn trên TopDev