【问题标题】:Optimistic or pessimistic locking in auction/bank like application (Rails/MySQL)拍卖/银行类应用程序(Rails/MySQL)中的乐观或悲观锁定
【发布时间】:2011-12-30 04:20:24
【问题描述】:

我正在使用 Rails 3.1 和 MySQL 5.1 设计一个类似拍卖的 Web 应用程序。用户将有帐户余额,因此重要的是,如果有人没有足够的资金就不要竞标拍卖物品。

显然,我会将拍卖的“获胜”打包到交易中,如下所示:

交易1:

ActiveRecord::Base.transaction do
    a = Account.where(:id=>session[:user_id]).first
    # now comes a long part of code with various calculations and other table updates, i.e. time pases
    a.balance -= the_price_of_the_item
    a.save!
end

顺便说一句,我目前正在使用乐观锁定,因此我所有的表都有 lock_version 列。

在执行此类交易时,用户可以通过另一个输入进行其他出价,因此每当他们出价时,一段代码会检查当前可用余额是否足够

这里还是一样:

事务 2:

ActiveRecord::Base.transaction do
    a = Account.where(:id=>session[:user_id]).first
    raise ActiveRecord::Rollback if a.balance < the_price_of_the_bid + Bids.get_total_bid_value_for_user(session[:user_id])
    # now process the bid saving
end

显然,我需要确保这两个交易不重叠,否则交易 2 可能正在读取余额,而交易 1 正在处理中,我最终得到一个负账户余额(出价被保存,然后交易 1 提交,则用户可能用他不再拥有的资金出价)。

需要注意的一点是,事务 2 并没有对 Account 进行任何更改,它只是读取了 account。我想这归结为一个问题:How to prevent any reads for selected SELECT statements while running transaction 1.

如何让事务 2 等待事务 1 完成?是否可以使用乐观锁定和可用的 MySQL 事务隔离级别之一,还是我需要在这里使用悲观锁定?如果悲观锁定是唯一的答案将添加 a.锁! 读完两笔交易中每一笔的账户记录就够了吗?

设计标准当然是

  • 我正在寻找性能最高的解决方案,即使它意味着更多 编码。
  • 数据一致性至关重要

【问题讨论】:

  • 只是想了解您的数据。假设某人的余额为 10。他们可以创建 2 个每个 10 的出价(此时我们不知道是否会获胜)?如果他们不能,您如何跟踪?
  • 不,他们不能,否则我冒着他们赢得两个投标的风险,这将导致他们的账户余额为 -10。跟踪在事务 2 中完成,它检查可用余额并且只有用户的账户上有足够的钱,然后他的出价将被接受并保存。
  • 所以该代码也会检查当前是否有任何未结出价? (或者账户余额是否反映了这一点)
  • 余额仅跟踪实际余额。在行之后:raise ActiveRecord::Rollback if a.balance &lt; the_price_of_the_bid 将有进一步的代码,通过读取投标表(作为回报可能包括正在交易 1 中处理的投标)来检查现有的公开投标。

标签: mysql ruby-on-rails ruby-on-rails-3.1


【解决方案1】:

我现在花了将近 10 个小时不停地阅读各种帖子和文档,以及使用 Rails 控制台试错,我想总结一下我的发现:

乐观锁定:无法满足我的要求,只有在我实际保存帐户余额记录时才会启动锁定。但是出价不会更新账户记录,因此它不会触发乐观锁定,除非我在账户记录中维护一个字段来跟踪我当前为所有未结出价承诺的资金,因此将在出价时更新账户记录(我不想这样做,因为每次保存出价时都需要再次更新数据库)。

所以我只剩下悲观锁定了。为简单起见,我决定锁定用户记录,因此我的事务 1 的代码更改为:

交易1:

ActiveRecord::Base.transaction do     
    u = User.find(session[:user_id],:lock=>true)
    a = Account.where(:id=>session[:user_id]).first 
    a.balance -= the_price_of_the_item
    ... some more code here ...
    a.save!      
end      

和事务 2:

ActiveRecord::Base.transaction do
    u = User.find(session[:user_id],:lock=>true)
    raise ActiveRecord::Rollback if a.balance < the_price_of_the_bid + Bids.get_total_bid_value_for_user(session[:user_id])          
    # now process the bid saving
    ....
end

此外,我决定将 MySQL 事务隔离级别设置为 SERIALIZABLE。

【讨论】:

    【解决方案2】:

    我按以下方式解释您的问题。

    1. 帐户有余额
    2. 一个帐户有很多出价
    3. 出价具有价值
    4. 既未中标也未输的投标是“开标”
    5. 赢得大奖后,其价值将从帐户余额中扣除。
    6. 只有在“未结”出价的总和小于余额时才能出价。

    因此,您已经确定了重要的交易。

    1. 出价
    2. 中标

    我就是这样做的。

    1.

    account = Account.find_by_id(session[:user_id])
    # maybe do some stuff here
    
    transaction do
      account.lock!
    
      bid_amount = account.bids.open.sum(:value)
      if bid_amount + this_value > account.balance
        raise "you're broke, mate"
      end
    
      account.bid.create!(:value => this_value)
    end
    

    我们在帐户上持有一个行锁定一小段时间, 但假设您使用的是正确的数据库,这应该没问题。

    假设第一个是正确的,那么接下来就容易多了

    2.

    class Bid
      def win!
        transaction do
          account.lock!
          account.decrement(:balance, self.value)
          account.save!
          close!
        end
      end
    end
    

    值得注意的是,如果您进行了 SQL 更新 SET balance = balance - ?,则不需要对第二个进行锁定。

    但一般来说,锁定代码的最小部分。

    实际上,您的锁定时间不应超过 100 毫秒。

    【讨论】:

    • 我认为第 1 部分中存在“脏读”的风险:您在交易开始之前读取了账户余额,因此如果另一个进程更新 account = Account.find_by_id(session [:user_id]) 和交易做account.lock!那么如果bid_amount + this_value > account.balance 将不正确。换句话说,如果第 2 部分在第 1 部分的查找和锁定之间执行,则第 1 部分正在使用错误的帐户余额。不是吗?
    • 我相信lock!会重新加载,因此当时会重新加载余额。
    猜你喜欢
    • 2010-09-12
    • 1970-01-01
    • 1970-01-01
    • 2018-10-22
    • 1970-01-01
    • 2013-09-16
    • 1970-01-01
    • 2022-07-08
    • 1970-01-01
    相关资源
    最近更新 更多