Tìm hiểu về Locking trong Rails ActiveRecord
Bài đăng này đã không được cập nhật trong 7 năm
Giới thiệu
Tính nhất quán của dữ liệu (Data consistency) là một vấn đề cực kỳ quan trọng trong bất cứ ứng dụng nào, đặc biệt là những ứng dụng liên quan đến tài chính, ngân hàng, ... . Dù chỉ là một lỗi nhỏ nhưng nếu không được xử lý kịp thời và đúng cách thì hậu quả để lại sẽ rất khôn lường. Trong bài viết này, tôi sẽ giới thiệu với các bạn về Locking trong Rails ActiveRecord và cách sử dụng nó để đảm bảo tính nhất quán của dữ liệu.
Tầm quan trọng của Locking
Giả sử chúng ta có một website thương mại điện tử mà mỗi người dùng sẽ có tài khoản cùng với một số tiền trong đó. Ở một thời điểm nào đó, người dùng có account id là 5 truy cập vào website và muốn mua vài mặt hàng. Khi đó, hệ thông của chúng ta sẽ lấy thông tin về người dùng đó như sau
account = Account.find_by_user_id(5)
Sau khi chọn được mặt hàng cần với mức giá là 50$, người dùng sẽ click vào phần checkout và bắt đầu thanh toán cho mặt hàng đó. Hiển nhiên là trước khi xử lý request, chúng ta sẽ phải kiểm tra xem tài khoản của người dùng này có đủ tiền để chi trả cho mặt hàng đó không, nếu thỏa mãn thì lúc này giao dịch mới được tiến hành và số tiền trong tài khoản của họ sẽ bị trừ đi một lượng bằng giá sản phẩm.
if account.balance >= item.price
account.balance = account.balance - item.price
#some other long processes here
account.save
end
Trong trường hợp này thì mọi thứ có vẻ như rất đơn giản và chúng ta sẽ không thấy có gì đó bất thường cả. Tuy nhiên hãy xét đến trường hợp người dùng này mở 1 tab khác của website, chọn một mặt hàng khác có giá là 80$ và bằng một cách nào đó, họ có thể click vào phần checkout của cả 2 tab vào cùng một thời điểm. Mặc dù xác suất của việc request ở cả 2 tab đến server gần như cũng một thời điểm là rất thấp nhưng điều đó không có nghĩa là trường hợp này không xảy ra. Và trong trường hợp đó thì cả request sẽ được server xử lý gần như đồng thời. Hãy cùng xem việc xử lý các request như thế nào qua đoạn code sau:
#account.balance = 100
account = Account.find_by_user_id(5)
#item.price is 50
if account.balance >= item.price
#it's good, allow user to buy this item
account.balance = account.balance - item.price
#account.balance is now 50
account.save
end
Sau khi xử lý đoạn mã account.balance = account.balance - item.price
và trước khi object account được lưu lại để update những thay đổi, CPU sẽ chuyển qua xử lý request thứ 2 với đoạn code tương tự:
account = Account.find_by_user_id(5)
#account.balance is still 100
#item.price is 80
if account.balance >= item.price
#it's good, allow user to buy this item
account.balance = account.balance - item.price
#account.balance is now 20
account.save
end
Chắc chắn lúc này bạn đã thấy được vấn đề ở đây. Sau khi mua sản phẩm thứ nhất, chúng ta sẽ nghĩ rằng tài khoản của user này chỉ còn 50$ và trên lý thuyết là người đó sẽ không thể mua được những sản phẩm có giá lớn hơn 50$ nữa. Tuy nhiên thì khi thực thi đoạn code như trên, chúng ta thấy được rằng người dùng này vẫn có thể mua một sản phẩm có giá 80$ sau khi đã mua sản phẩm đầu tiên với giá 50$.
Vậy làm thế nào để giải quyết được vấn đề này? Với Locking thì việc này không quá khó khăn. Khi khóa được thiết lập, chúng sẽ không cho phép hai tiến trình đồng thời cập nhật các đối tượng trong cùng một thời điểm.
Có 2 loại khóa đó là Optimistic và Pessimistic.
Optimistic Locking
Với loại khóa này, nhiều user có thể truy cập vào cùng 1 object, đọc dữ liệu từ nó nhưng khi có hai hoặc nhiều người cùng thực hiện việc update trên object này thì chỉ một người có thể update được, tiến trình của những người còn lại sẽ bắn ra exception.
p1 = Person.find(1)
p2 = Person.find(1)
p1.first_name = "Michael"
p1.save
p2.first_name = "should fail"
p2.save # Raises a ActiveRecord::StaleObjectError
Để tạo Optimistic Locking, bạn cần tạo một cột tên là lock_version
trong model mà bạn muốn sử dụng locking và Rails sẽ tự động kiểm tra cột này trước khi update object. Cơ chế hoạt động của nó khá đơn giản. Mỗi lần object được cập nhật, giá trị của lock_version
cũng sẽ được tăng lên. Do đó, nếu có 2 hoặc nhiều hơn các request muốn cập nhật cùng một object thì chỉ có request đầu tiên là có thể thực hiện được do lúc này lock_version
có giá trị giống như lúc nó được đọc, còn từ request thứ 2 trở đi thì sẽ không thể thực hiện do lúc này, giá trị của lock_version
đã tăng lên và không trùng khớp với giá trị giống như lúc nó được đọc.
Với loại khóa này, bạn sẽ phải xử lý exception được bắn ra mỗi khi tiến trình update bị fail. Bạn có thể đọc thêm về Optimistic Locking tại đây: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
Pessimistic Locking
Với loại khóa này thì chỉ có người đầu tiên truy cập vào object thì mới có thể update được nó. Tất cả những người dùng khác thậm chí còn không thể đọc dữ liệu từ object đó. Đây là một điểm rất khác với Optimistic Locking khi người dùng vẫn còn thể đọc được dữ liệu từ object bị khóa.
Rails thực hiện Pessimistic Locking bằng cách sử dụng một truy vấn đặc biệt trong database. Ví dụ bạn muốn lấy ra một object là account và muốn khóa nó lại cho đến khi update xong, chúng ta sẽ xử lý như sau:
account = Account.find_by_user_id(5)
account.lock!
#no other users can read this account, they have to wait until the lock is released
account.save!
#lock is released, other users can read this account
Bạn có thể đọc thêm về Pessismistic Locking tại đây: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
Việc sử dụng loại khóa nào sẽ tùy thuộc vào các trường hợp cụ thể. Thông thường thì sử dụng Optimistic locking là đủ vì nó linh hoạt hơn và nó cho phép nhiều người có thể truy cập để đọc dữ liệu từ 1 object cùng lúc. Còn với Pessimistic, hãy nhớ mở khóa sau khi đã thực hiện xong việc update object.
Bạn có thể tìm hiểu thêm về chúng tại đây:
- http://blog.couchbase.com/optimistic-or-pessimistic-locking-which-one-should-you-pick
- https://4loc.wordpress.com/2009/04/25/optimistic-vs-pessimistic-locking
Reference
http://thelazylog.com/understanding-locking-in-rails-activerecord/
All rights reserved