Rails ActiveRecord Transaction 的疑问

fleuria · 2014年02月18日 · 最后由 luikore 回复于 2014年02月18日 · 5869 次阅读

隐隐约约感觉文档里的这个例子有坑:

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

假设 withrawal 与 deposit 的实现是这样:

def withraw
  self.balance -= 10
  self.save!
end

def deposit
  self.balance += 10
  save!
end

那么,david 和 mary 中 balance 字段的值是在进入 transaction 之前被取出的,如有竞态条件,无法保证事务的 Isolation。为此改成这样子心里可能更踏实一点:

ActiveRecord::Base.transaction do
  david.reload
  mary.reload
  david.withdrawal(100)
  mary.deposit(100)
end

但是感觉略丑,有点拿不准 rails 有没有在这方面做特殊处理,或者有没有更简短的 api 呢?或者有没有使用 transaction 的注意事项或者最佳实战?

thanks

试一试 increment 和 decrement

我觉得是这样:

  1. 这个例子应该是想说明 transaction 的使用,所以我觉得没有问题。这个例子刚好说明 transaction 的使用。。。
  2. 你说的问题,我觉得确实存在
  3. 另外,即使你改成 ActiveRecord::Base.transaction do david.reload mary.reload david.withdrawal(100) mary.deposit(100) end 这样,也难保你说的有竞争,你只是减小了这个事件发生的概率。。。
  4. 在 symfony(php)中,是有乐观锁去处理这个事。我觉得,rails 中肯定有。当然,我是 rails 初学者,所以不懂。麻烦懂的人解释下。。。

#1 楼 @avengerbevis 学习了,多谢

#2 楼 @mahone3297 1、赞同 3、我想存在竞争时事务没问题,事务保证 Isolation 性质。

#1 楼 @avengerbevis increment 和 decrement 应该也存在同样的问题:

http://apidock.com/rails/ActiveRecord/Base/increment!

是应该在取数据之前就进入 transaction。另外和 isolation level 的设置也有关系的

mysql 默认 isolation level 已经保证可重读了,一开始 reload 过就没问题 pg 用 transaction isolation: repeatable_read

#6 楼 @luikore

实际上是不建议用 reload 的,而是一开始读之前就进入 transaction,不然可能导致有些在代码里检查的条件被漏过去了

#7 楼 @bhuztez

reload 经常是必须的,就算最外面有 transaction 包着

a = Article.find 1
b = Article.find 1
a.increment!
b.reload # 必须
b.increment!

#9 楼 @luikore 我在前面加个行么

select for update 取出数据后再进行事务的话是否可行?

#11 楼 @ywjno 求详解 _(:з」∠) _ gogole 了下 select for update 语句,感觉不如全在事务里做更直接?

#13 楼 @fleuria select for update 应该也是要在 transaction 里做的,row 锁会在 commit 的时候释放

#14 楼 @bhuztez 赞,决定还是上笨方法了,正确性最重要

#15 楼 @fleuria 感觉还是有点不太对。就你这个例子来说

UPDATE xxx SET x = x + 10UPDATE xxx SET x = 100 可能结果是不一样的,不放心还是加个SELECT FOR UPDATE保险

其实就这个例子,CouchDB 才是最合适的,把所有操作都记录下来,balance 是 map/reduce 出来的

#13 楼 @fleuria 手机回复仓促抱歉了,应该说的是开启事务,然后用 select for update 语句对相关数据进行行级只读锁,然后再进行 update 操作,之后 commit、行级锁自行释放,结束事务。

PS:b 大神补充的正确

#16 楼 @bhuztez #17 楼 @ywjno

使用 transaction 外加在应用程序中计算会有怎样的问题呢?计算有时会更复杂一些

#18 楼 @fleuria

transaction 只能保证你一个 transaction 里的所有改动要么同时成功要么同时失败。

isolation level 以 postgresql 为例

read uncommited 可能返回任何数据库里的记录,包括已经 commit 的和没有 commit 的。

read commited 只可能返回查询时已经 commit 的记录

repeatable read 只返回当前 transaction 开始前已经 commit 的记录

正常情况,修改的操作应该用 transaction,isolation level 应该选 repeatable read

比如,有一种情况是,两个 transaction 同时改同一条记录

1) num = `SELECT id, num FROM xxx WHERE id=1 LIMIT 1`;

2) new_num = num + 1

3) `UPDATE xxx SET num = ? WHERE id = 1` new_num

4) COMMIT

假如一开始 num = 1,有两个 transaction A, B

A: 1
B: 1
A: 2
B: 2
A: 3
B: 3
A: 4
B: 4

最终结果可能是 num = 2

#19 楼 @bhuztez #17 楼 @ywjno

其实 repeatable read 就是锁记录,serializable 就是锁表... 一般情况只要用 repeatable read 就够了

#20 楼 @luikore SQL 标准里的 repeatable read 应该没有锁记录这么严吧?

#22 楼 @bhuztez 从碰到记录开始就锁定,到事务结束才释放,才能保证第二次读取它的结果还是和第一次读取是一致的,这才是 repeatable read... 如果同时进行的两个 repeatable read transaction 都要获取一个记录的锁,其中一个就会弹出 deadlock 错误,你可以重试几次,或者直接就让用户重新提交表单。

#23 楼 @luikore #22 楼 @bhuztez #21 楼 @hooopo #17 楼 @ywjno

干货 get,请收下我对你们的爱 ❤

刚试了下,又发现 mysql innoDB 的又一个大坑,它的 repeatable read 不是按标准做的,它只返回第一次读记录的 snapshot 而不会抛异常,需要改成 serializable ...

下面代码在 pg 就可以正确的抛出一个事务异常,在 mysql 就会只更新一次

require "active_record"
# require "mysql2"
require 'pg'

ActiveRecord::Base.establish_connection \
  database: 'dummy',
  adapter: 'postgresql',
  username: 'postgres',
  password: '',
  pool: 5

class Article < ActiveRecord::Base
end

Article.delete_all
a = Article.new
a.count = 3
a.id = 1
a.save!

t1, t2 = 2.times.map do |i|
  Thread.new do
    Article.transaction isolation: :repeatable_read do
      x = Article.find 1
      x.count += 4
      sleep 2
      x.save!
    end
  end
end

t1.join
t2.join

puts Article.first.count

#25 楼 @luikore 我就感觉标准里 REPEATABLE READ 行锁不是强制的,不信你去查查看

#26 楼 @bhuztez 嗯,ANSI 标准没要求锁,只是大部分数据库都是用锁实现的,而在锁实现的数据库里隔离等级就是这样做的... http://www.cs.umb.edu/~poneil/iso.pdf 指出锁隔离比 ANSI 的定义更精确. 正确的 repeatable read 是不会发生 lost update (定义见批评的 P4), 但 mysql 就没按照标准做,要在语句手动加 for update 才会锁定记录...

#25 楼 @luikore 你这个例子在事务里面只读了一次,没有发生 repeatable read 啊

#28 楼 @quakewang 我觉得 repeatable read 只是个名称...

需要 登录 后方可回复, 如果你还没有账号请 注册新账号