Mysql 如何在记录付款和运行余额的rails模型中避免竞争条件?

Mysql 如何在记录付款和运行余额的rails模型中避免竞争条件?,mysql,ruby-on-rails,sidekiq,rails-activejob,Mysql,Ruby On Rails,Sidekiq,Rails Activejob,我有一个简单的模型,Payments,它有两个字段amount和running\u balance。创建新的付款记录时,我们会查找其以前付款的运行余额,例如上次运行余额,并将上次运行余额+金额保存为当前付款的运行余额 下面是我们实现支付模型的三次失败尝试。为简单起见,假设以前的付款始终存在,并且随着付款的创建,ids不断增加 尝试1: class Payments < ActiveRecord::Base before_validation :calculate_running_b

我有一个简单的模型,
Payments
,它有两个字段
amount
running\u balance
。创建新的
付款
记录时,我们会查找其以前付款的
运行余额
,例如
上次运行余额
,并将
上次运行余额+金额
保存为当前付款的
运行余额

下面是我们实现
支付
模型的三次失败尝试。为简单起见,假设以前的付款始终存在,并且随着付款的创建,
id
s不断增加

尝试1:

class Payments < ActiveRecord::Base
    before_validation :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.last
        self.running_balance = p.running_balance + amount
    end
end
class付款
尝试2:

class Payments < ActiveRecord::Base
    after_create :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.where("id < ?", id).last
        update!(running_balance: p.running_balance + amount)
    end
end
class付款
计划3:

class Payments < ActiveRecord::Base
    after_commit :calculate_running_balance
    private
    def calculate_running_balance
        p = Payment.where("id < ?", id).last
        update!(running_balance: p.running_balance + amount)
    end
end
class付款
当我们在后台使用
sidekiq
创建付款时,这些实现可能会在系统中造成竞争条件。假设最后一笔付款是
付款1
。当同时创建两个新付款时,例如
payment 2
payment 3
,他们的
流动余额
可能基于
付款1
的流动余额进行计算,因为可能是在
付款3
计算其流动余额时,
付款2
尚未保存到数据库中


特别是,我对一个避免运行条件的修复感兴趣。我还热衷于研究实现类似支付系统的其他rails应用程序。

更新:这是第一个版本,有关实际工作方法,请参见以下内容:

如果在使用计算上次余额时锁定上次付款,则可以取消竞争条件。为此,您始终需要使用事务块包装创建付款

class付款
获取最后一笔
付款的第一次查询
还将在包装该记录的交易期间锁定该记录(并延迟进一步查询),即直到交易完全提交并创建新记录为止。 如果另一个查询同时也试图读取锁定的最后一笔付款,它将不得不等到第一笔交易完成。因此,如果您在创建付款时在sidekiq中使用事务,您应该是安全的

有关更多信息,请参阅上面的链接指南

更新:这并不容易,这种方法可能导致死锁

经过一些广泛的测试,问题似乎更加复杂。如果我们只锁定“最后一次”付款记录(Rails将其转换为
SELECT*FROM payments ORDER BY id DESC LIMIT 1
),那么我们可能会陷入死锁

在这里,我将介绍导致死锁的测试,下面将进一步介绍实际工作的方法

在下面的所有测试中,我都使用MySQL中的一个简单InnoDB表。我创建了最简单的
payments
表,其中只有
amount
列添加了Rails中的第一行和附带的模型,如下所示:

#sql控制台
创建表付款(id整数主键自动递增,金额整数)引擎=InnoDB;
插入付款(金额)值(100);
#app/models/payments.rb
类付款
现在,让我们打开两个Rails控制台,启动一个长时间运行的事务,在第一个控制台会话中使用最后一个记录锁和新行插入,在第二个控制台会话中使用另一个最后一行锁:

# rails console 1
>> Payment.transaction { p = Payment.lock.last; sleep(10); Payment.create!(amount: (p.amount + 1));  }
D, [2016-03-11T21:26:36.049822 #5313] DEBUG -- :    (0.2ms)  BEGIN
D, [2016-03-11T21:26:36.051103 #5313] DEBUG -- :   Payment Load (0.4ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1 FOR UPDATE
D, [2016-03-11T21:26:46.053693 #5313] DEBUG -- :   SQL (1.0ms)  INSERT INTO `payments` (`amount`) VALUES (101)
D, [2016-03-11T21:26:46.054275 #5313] DEBUG -- :    (0.1ms)  ROLLBACK
ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `payments` (`amount`) VALUES (101)

# meanwhile in rails console 2
>> Payment.transaction { p = Payment.lock.last; }
D, [2016-03-11T21:26:37.483526 #8083] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-11T21:26:46.053303 #8083] DEBUG -- :   Payment Load (8569.0ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1 FOR UPDATE
D, [2016-03-11T21:26:46.053887 #8083] DEBUG -- :    (0.1ms)  COMMIT
=> #<Payment id: 1, amount: 100>
发生死锁时,将重试事务,即找到并使用最新的记录

带有测试的工作解决方案

我使用的另一种方法是锁定表的所有记录。这可以通过锁定
COUNT(*)
子句来实现,并且它似乎可以始终如一地工作:

# rails console 1
>> Payment.transaction { Payment.lock.count; p = Payment.last; sleep(10); Payment.create!(amount: (p.amount + 1));}
D, [2016-03-11T23:36:14.989114 #5313] DEBUG -- :    (0.3ms)  BEGIN
D, [2016-03-11T23:36:14.990391 #5313] DEBUG -- :    (0.4ms)  SELECT COUNT(*) FROM `payments` FOR UPDATE
D, [2016-03-11T23:36:14.991500 #5313] DEBUG -- :   Payment Load (0.3ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1
D, [2016-03-11T23:36:24.993285 #5313] DEBUG -- :   SQL (0.6ms)  INSERT INTO `payments` (`amount`) VALUES (101)
D, [2016-03-11T23:36:24.996483 #5313] DEBUG -- :    (2.8ms)  COMMIT
=> #<Payment id: 2, amount: 101>

# meanwhile in rails console 2
>> Payment.transaction { Payment.lock.count; p = Payment.last; Payment.create!(amount: (p.amount + 1));}
D, [2016-03-11T23:36:16.271053 #8083] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-11T23:36:24.993933 #8083] DEBUG -- :    (8722.4ms)  SELECT COUNT(*) FROM `payments` FOR UPDATE
D, [2016-03-11T23:36:24.994802 #8083] DEBUG -- :   Payment Load (0.2ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1
D, [2016-03-11T23:36:24.995712 #8083] DEBUG -- :   SQL (0.2ms)  INSERT INTO `payments` (`amount`) VALUES (102)
D, [2016-03-11T23:36:25.000668 #8083] DEBUG -- :    (4.3ms)  COMMIT
=> #<Payment id: 3, amount: 102>
#rails控制台1
>>Payment.transaction{Payment.lock.count;p=Payment.last;sleep(10);Payment.create!(金额:(p.amount+1));}
D、 调试--(0.3ms)开始
D、 [2016-03-11T23:36:14.990391#5313]调试--(0.4ms)从“付款”中选择计数(*)进行更新
D、 [2016-03-11T23:36:14.9915005313]调试--:付款加载(0.3ms)按“付款”从“付款”订单中选择“付款”。“id”描述限制1
D、 [2016-03-11T23:36:24.993285#5313]调试--:SQL(0.6ms)插入“付款”(“金额”)值(101)
D、 [2016-03-11T23:36:24.996483#5313]调试--(2.8ms)提交
=> #
#同时,在rails控制台2中
>>Payment.transaction{Payment.lock.count;p=Payment.last;Payment.create!(金额:(p.amount+1));}
D、 [2016-03-11T23:36:16.271053#8083]调试--(0.1ms)开始
D、 [2016-03-11T23:36:24.993933#8083]调试--(8722.4ms)从“付款”中选择计数(*)进行更新
D、 [2016-03-11T23:36:24.994802#8083]调试--:付款加载(0.2ms)按“付款”从“付款”订单中选择“付款”。“id”描述限制1
D、 [2016-03-11T23:36]
# rails console 1
>> Payment.transaction { Payment.lock.count; p = Payment.last; sleep(10); Payment.create!(amount: (p.amount + 1));}
D, [2016-03-11T23:36:14.989114 #5313] DEBUG -- :    (0.3ms)  BEGIN
D, [2016-03-11T23:36:14.990391 #5313] DEBUG -- :    (0.4ms)  SELECT COUNT(*) FROM `payments` FOR UPDATE
D, [2016-03-11T23:36:14.991500 #5313] DEBUG -- :   Payment Load (0.3ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1
D, [2016-03-11T23:36:24.993285 #5313] DEBUG -- :   SQL (0.6ms)  INSERT INTO `payments` (`amount`) VALUES (101)
D, [2016-03-11T23:36:24.996483 #5313] DEBUG -- :    (2.8ms)  COMMIT
=> #<Payment id: 2, amount: 101>

# meanwhile in rails console 2
>> Payment.transaction { Payment.lock.count; p = Payment.last; Payment.create!(amount: (p.amount + 1));}
D, [2016-03-11T23:36:16.271053 #8083] DEBUG -- :    (0.1ms)  BEGIN
D, [2016-03-11T23:36:24.993933 #8083] DEBUG -- :    (8722.4ms)  SELECT COUNT(*) FROM `payments` FOR UPDATE
D, [2016-03-11T23:36:24.994802 #8083] DEBUG -- :   Payment Load (0.2ms)  SELECT  `payments`.* FROM `payments`  ORDER BY `payments`.`id` DESC LIMIT 1
D, [2016-03-11T23:36:24.995712 #8083] DEBUG -- :   SQL (0.2ms)  INSERT INTO `payments` (`amount`) VALUES (102)
D, [2016-03-11T23:36:25.000668 #8083] DEBUG -- :    (4.3ms)  COMMIT
=> #<Payment id: 3, amount: 102>