隐隐约约感觉文档里的这个例子有坑:
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
我觉得是这样:
ActiveRecord::Base.transaction do
david.reload
mary.reload
david.withdrawal(100)
mary.deposit(100)
end
这样,也难保你说的有竞争,你只是减小了这个事件发生的概率。。。#1 楼 @avengerbevis 学习了,多谢
#2 楼 @mahone3297 1、赞同 3、我想存在竞争时事务没问题,事务保证 Isolation 性质。
#1 楼 @avengerbevis increment 和 decrement 应该也存在同样的问题:
mysql 默认 isolation level 已经保证可重读了,一开始 reload 过就没问题
pg 用 transaction isolation: repeatable_read
reload 经常是必须的,就算最外面有 transaction 包着
a = Article.find 1
b = Article.find 1
a.increment!
b.reload # 必须
b.increment!
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
刚试了下,又发现 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
#26 楼 @bhuztez 嗯,ANSI 标准没要求锁,只是大部分数据库都是用锁实现的,而在锁实现的数据库里隔离等级就是这样做的...
http://www.cs.umb.edu/~poneil/iso.pdf 指出锁隔离比 ANSI 的定义更精确.
正确的 repeatable read 是不会发生 lost update (定义见批评的 P4), 但 mysql 就没按照标准做,要在语句手动加 for update
才会锁定记录...