ActiveRecord的嵌套事务
先看现象,再看源码
在父transaction作rollback,两个都rollback
[2] pry(main)> binding.trace_tree(htmp: 'tx_rollback_parent'){ Post.transaction{ Post.create(title: 'b'); Post.transaction{ Post.create(title: 'c') }; raise ActiveRecord::Rollback } }
(26.5ms) BEGIN
SQL (40.3ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:04:54.258657"], ["title", "b"], ["updated_at", "2019-06-27 13:04:54.258657"]]
SQL (41.7ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:04:57.019647"], ["title", "c"], ["updated_at", "2019-06-27 13:04:57.019647"]]
(27.0ms) ROLLBACK
=> nil
调用栈如下
在子transaction作rollback,两个都commit
[3] pry(main)> binding.trace_tree(htmp: 'tx_rollback_child'){ Post.transaction{ Post.create(title: 'b'); Post.transaction{ Post.create(title: 'c'); raise ActiveRecord::Rollback } } }
(25.1ms) BEGIN
SQL (38.4ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:05:15.824843"], ["title", "b"], ["updated_at", "2019-06-27 13:05:15.824843"]]
SQL (39.7ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:05:18.605931"], ["title", "c"], ["updated_at", "2019-06-27 13:05:18.605931"]]
(25.2ms) COMMIT
=> nil
调用栈如下
子transaction有requires_new,在父transaction作rollback,两个都rollback
[4] pry(main)> binding.trace_tree(htmp: 'tx_rq_new_rollback_parent'){ Post.transaction{ Post.create(title: 'b'); Post.transaction(requires_new: true){ Post.create(title: 'c') }; raise ActiveRecord::Rollback } }
(25.5ms) BEGIN
SQL (41.8ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:05:34.815014"], ["title", "b"], ["updated_at", "2019-06-27 13:05:34.815014"]]
(27.1ms) SAVEPOINT active_record_1
SQL (42.6ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:05:37.624946"], ["title", "c"], ["updated_at", "2019-06-27 13:05:37.624946"]]
(26.8ms) RELEASE SAVEPOINT active_record_1
(26.9ms) ROLLBACK
=> nil
调用栈如下
子transaction有requires_new,在子transaction作rollback,只有子transaction会rollback,而父transaction会commit
[5] pry(main)> binding.trace_tree(htmp: 'tx_rq_new_rollback_child'){ Post.transaction{ Post.create(title: 'b'); Post.transaction(requires_new: true){ Post.create(title: 'c'); raise ActiveRecord::Rollback } } }
(26.3ms) BEGIN
SQL (38.3ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:05:57.555988"], ["title", "b"], ["updated_at", "2019-06-27 13:05:57.555988"]]
(27.3ms) SAVEPOINT active_record_1
SQL (42.3ms) INSERT INTO "posts" ("created_at", "title", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2019-06-27 13:06:00.295987"], ["title", "c"], ["updated_at", "2019-06-27 13:06:00.295987"]]
(29.6ms) ROLLBACK TO SAVEPOINT active_record_1
(26.0ms) COMMIT
=> nil
调用栈如下
上述的子事务rollback而不影响父事务是如何实现的呢?
在未调用过transaction的情况下(current_transaction为joinable?等于false的伪事务ClosedTransaction),或者requires_new为false、nil的情况下,transaction都会走within_new_transaction,其他情况会直接yield
而因为我们调用的transaction方法,其实是Thread.current中持有的connection的transaction方法,所以直接yield的时候,尽管是transaction{ transaction{} },但表现起来这两个block是串行而非嵌套的
当利用requires_new开启子事务时,父与子block的CRUD才是嵌套运行在不同的within_new_transaction之中的。这时,子事务的ActiveRecord::Rollback或其他Exception,会在本层捕获,然后调用rollback_transaction,给数据库发rollback命令。之后再把异常往上抛给transaction方法,transaction方法会忽略ActiveRecord::Rollback,这就使得父事务不会因子事务的ActiveRecord::Rollback而rollback,但其他类型的异常还是会导致父事务rollback
# activerecord-4.1.4/lib/active_record/connection_adapters/abstract/database_statements.rb
module ActiveRecord
module ConnectionAdapters
module DatabaseStatements
def transaction(options = {})
options.assert_valid_keys :requires_new, :joinable, :isolation
if !options[:requires_new] && current_transaction.joinable?
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
yield
else
within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
def within_new_transaction(options = {}) #:nodoc:
transaction = begin_transaction(options)
yield
rescue Exception => error
rollback_transaction if transaction
raise
ensure
begin
commit_transaction unless error
rescue Exception
rollback_transaction
raise
end
end
begin_transaction调用的是当前transaction实例的begin方法,如果当前@transaction是顶级的伪事务ClosedTransaction,则会开一个RealTransaction,否则检查是否finishing?(finishing?可通过掉rollback和commit置为true,一般不会手动调用,而是让事务block的rescue和ensure来调),不是finishing?则会开一个SavepointTransaction,并设置其parent为当前@transaction。当begin方法返回时@transaction会赋值为新的transaction实例,源码如下
# activerecord-4.1.4/lib/active_record/connection_adapters/abstract/database_statements.rb
def begin_transaction(options = {})
@transaction = @transaction.begin(options)
end
def commit_transaction
@transaction = @transaction.commit
end
def rollback_transaction
@transaction = @transaction.rollback
end
(在rails 5中,父子结构是使用栈实现的,栈顶是子,子事务运行完,则会pop掉)
此外,SavepointTransaction的初始化过程中还会调用connection.create_savepoint,底层运作是让具体数据库adapter发送创建savepoint的语句,如下流程

rollback的实现同理,要分开RealTransaction和SavepointTransaction,SavepointTransaction发的是rollback savepoint命令,RealTransaction发rollback
源码如下
# activerecord-4.1.4/lib/active_record/connection_adapters/abstract/transaction.rb
module ActiveRecord
module ConnectionAdapters
class ClosedTransaction < Transaction
def begin(options = {})
RealTransaction.new(connection, self, options)
end
end
class OpenTransaction < Transaction
def begin(options = {})
if finishing?
parent.begin
else
SavepointTransaction.new(connection, self, options)
end
end
def rollback
@finishing = true
perform_rollback
parent
end
end
class RealTransaction < OpenTransaction
def perform_rollback
connection.rollback_db_transaction
rollback_records
end
end
class SavepointTransaction < OpenTransaction194
def perform_rollback
connection.rollback_to_savepoint
rollback_records
end
end
end
end
那为什么非require_new的子transaction里raise ActiveRecord::Rollback,对父子都没有影响呢?
还是看回transaction和within_new_transaction的源码。没有require_new时,子transaction是的rollback实际是
transaction do
within_new_transaction do
transaction do
raise ActiveRecord::Rollback
end
end
end
内层transaction并没有自己的within_new_transaction,所以没有rescue调用rollback_transaction方法,而且,transaction方法本来就会吃掉ActiveRecord::Rollback,所以,也不会让父事务感知,于是没有任何rollback,这其实也不构成父子事务。当然其他异常还是会导致整个事务rollback的。
使用技巧
如果确定当前事务会被嵌套使用,而且希望所有嵌套在外的事务都会因为内层的错误而rollback,那就raise ActiveRecord::Rollback以外的异常。
如果希望只rollback内层的事务,则使用requires_new和raise ActiveRecord::Rollback。