Locking with rails
Mình có lướt FB và có 1 xem đc 1 câu hỏi của 1 bạn khá hay như sau: Vậy câu hỏi đặt ra là tại sao tài khoản của 1 vài người bị âm. Và cách khắc phục như thế nào? Đầu tiên hãy giả sử Tôi là khách hàng và trong tài khoản của tôi có 10 triệu VND. Tôi bắt đầu truy cập vào 1 trang web mua hàng online ...
Mình có lướt FB và có 1 xem đc 1 câu hỏi của 1 bạn khá hay như sau: Vậy câu hỏi đặt ra là tại sao tài khoản của 1 vài người bị âm. Và cách khắc phục như thế nào? Đầu tiên hãy giả sử Tôi là khách hàng và trong tài khoản của tôi có 10 triệu VND. Tôi bắt đầu truy cập vào 1 trang web mua hàng online để mua 1 số thứ mà cần thiết. Hệ thống ngân hàng sẽ kiểm tra tài khoản của tôi với id. user = User.find(id) Sau khi chọn được món hàng ưa thích với giá 7 triệu. Tôi bắt đầu thanh toán. Lúc này hệ thống sẽ kiểm tra tài khoản của Tôi có trả đủ tiền hay không? A.balance > amount Nếu tài khoản của tôi lớn hơn số tiền tôi thanh toán thì giao dịch mới được tiến hành. Ở đây trong tài khoản của tôi có 10 triệu nên tôi đủ điều kiện để mua 1 món hàng có giá 7 triệu. Và tài khoản của tôi sẽ trừ đi số tiền tương ứng đồng thời tài khoản của shop sẽ được cộng số tiền mà tôi đã chuyển khoản.
A.balance -= amount A.save! B.balance += amount B.save!
Nếu ở trên tôi đã check A.balance > amount thì mới thực hiện giao dịch. thì không thể nào tài khoản bị âm được. Có 1 số giả thiết : Do không check amount > 0 nên tài khoản có thể bị âm. Cái này chuẩn luôn. tài khoản B có thể bị âm khi bán đồ lại còn cộng thêm cả tiền cho người mua nữa. Thật ra thì chả có hệ thống nào cho nhập giá mua < 0 cả và khi hệ thống giao dịch của ngân hàng cũng sẽ kiểm tra amount > 0. Nếu vậy tại sao tài khoản vẫn âm.
Hãy thử mở 1 tab khác và mua 1 món đồ với giá 5 triệu. Sau đó bằng 1 cách nào đó click chuột mua đồng thời 2 món hàng mà mình đã chọn. Và mặc dù là rất khó xảy ra tuy nhiên khi các yêu cầu của tab 1 và tab 2 đến máy chủ gần như cùng 1 thời điểm và cùng 1 lúc thì tôi chắc chắn bạn đã nhận ra vấn đề rồi phải không nào. Trước khi trừ tài khoản và lưu vào A.save thì server sẽ kiểm tra tab 2 với 1 món đồ là 5 triệu. Và lúc nào điều kiện A.balance > amount vẫn thỏa mãn. Như vậy tôi có thể mua 2 món đồ khi vượt qua các điều kiện kiểm tra. Và tài khoản tôi hoàn toàn có thểm bị âm khi thực hiện cách trên.
Cái này được gọi là Race conditions (Tình huống tương tranh). Là trường hợp thường xảy ra bên trong critical section. Khi có hai hay nhiều Thread cùng chia sẻ dữ liệu, hay đơn giản là cùng đọc và ghi vào một vùng dữ liệu. Khi đó vấn đề xảy ra là: Kết quả của việc thực thi multiple threads có thể thay đổi phụ thuộc vào thứ tự thực thi các thread.
Vậy là chúng ta đã giải quyết xong câu hỏi thứ nhất. Tại sao lại âm được. ^^ Vậy cách khắc phục sẽ được giải quyết như thế nào?
Sử dụng Locking chúng ta sẽ không cho phép 2 tiến trình cập nhật các đối tượng cùng 1 thời điểm. Vậy cách sử dụng locking như thế nào? Locking có 2 loại: 1, Optimistic Locking: khi có sự việc multiple threads thì nó sẽ cho 1 hành động thành công và 1 hành động khác sẽ không được thực hiện. VD: khi cả 2 cùng request tới server cùng 1 thời điểm Đầu tiên ta tạo 1 column trong bảng user là :lock_version kiểu dữ liệu interger rails g migration addColumnLockVersionToUser
add_column :users, :lock_version, :integer
trong model User.rb: self.locking_column = :lock_version
A.balance -= amount A.save # Success A.balance -= amount A.save # Raises a ActiveRecord::StaleObjectError - False
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.
2, Pessimistic Locking: Chỉ có người dùng đầu tiên có thể truy cập đến các đối tượng và vì thế mới có khả năng cập nhập được nó. Tất cả các người khác sẽ bị reject . VD: khi tôi thực hiện giao dịch. lúc đấy hệ thống sẽ tìm ID của tôi để lấy thông tin của tôi.
A = Account.find_by_user_id(1) A.lock! A.balance -= amount A.save
Lúc này đây tất cả các người dùng khác đều không thể lấy thông tin của A được.
1 cái là khóa khi cập nhật (Optimistic Locking) và 1 cái là khóa khi truy cập đối tượng (Pressimistic Locking) Trong nhiều trường hợp không có yêu cầu đặt biệt thì với Optimistic Lock là đủ vì nó sẽ linh hoạt và dễ dàng sử dụng hơn.
Kết Luận Vậy là mình đã trình bày xong về Locking trong rails cũng như nguyên nhân và giải pháp.