Rails ActiveRecord Transaction 的疑问

fleuria · 发布于 2014年02月18日 · 最后由 luikore 回复于 2014年02月18日 · 2980 次阅读
96

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

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

共收到 29 条回复
96

试一试 increment和decrement

6281

我觉得是这样:

  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初学者,所以不懂。麻烦懂的人解释下。。。
96

#1楼 @avengerbevis 学习了,多谢

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

96

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

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

96

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

2880

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

96

#6楼 @luikore

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

2880

#7楼 @bhuztez

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

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

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

1342

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

96

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

96

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

96

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

96

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

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

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

1342

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

PS:b大神补充的正确

96

#16楼 @bhuztez #17楼 @ywjno

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

96

#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

2880

#19楼 @bhuztez #17楼 @ywjno

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

96

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

2880

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

96

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

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

2880

刚试了下, 又发现 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
96

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

2880

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

162

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

2880

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

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