先看现象,再看源码

在父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

调用栈如下

20190627_130453_469_tx_rollback_parent.html

在子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

调用栈如下

20190627_130515_228_tx_rollback_child.html

子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

调用栈如下

20190627_130534_104_tx_rq_new_rollback_parent.html

子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

调用栈如下

20190627_130556_760_tx_rq_new_rollback_child.html

上述的子事务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。