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:
- Admin 1 thêm sản phẩm mới tên “Bench Fitness Equipment for Home”
- 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”
- 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”
- 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.
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:
- 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.
- 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.
- Request 1 – Tạo user mới và insert vào database.
- 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
- Xây dựng hệ thống referral trong Laravel
- Indexeddb là gì? – Tất cả những điều cần biết
- Singleton design pattern – tất cả những điều cần biết
Xem thêm Việc làm Developer hấp dẫn trên TopDev