了解带有简单示例的导轨中的交易隔离水平
#database #ruby #rails #transaction

概述

在本文中,我们将讨论数据库交易在Rails应用程序中的用法,特别关注 Acid Acid 原理之一 - 隔离 - 及其四个水平。我们将探讨这些隔离水平是什么,为什么它们是必要的以及它们旨在解决的问题。此外,我们将使用导轨控制台提供每个隔离级别的直接工作示例。到本文结束时,您将对隔离级别有体面的了解,您将能够自行验证其行为以确保它们的工作方式。

定义

简单地说,交易是一种机制,使您可以执行一组操作,以使他们都执行(提交),或者系统状态似乎根本没有开始执行(回滚)。在铁轨中,要使用交易,您需要将操作写入以下块:

ActiveRecord::Base.transaction { # ... }

介绍

为了更好地理解交易,我们将解释原则是什么以及它们旨在解决的问题。之后,我们将深入研究这些称为孤立的原则之一,我们将解释为什么我们需要它以及隔离所建议的解决问题的策略。此外,我们将提供铁轨示例以更好地理解这个概念。

是一个缩写词,代表原子能一致性隔离持久性< /strong>。这四个原则突出了可能发生的潜在问题以及我们应该意识到的。对于其中两个(一致性 耐用性),我们没有任何控制权,我们应该依靠数据库实现并信任它。对于其他两个( artomicity 隔离),我们可以拥有更多的控制权,我们将更多地关注它们。

原子

第一个原理是原子能。要记住这一点,我们可以考虑原子。你不能拆分原子。原子交易也是如此。你不能只拆分它。它应该全部或一无所有。让我们提供打破这一原则的例子:

# rails c

animal = Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: nil, status: nil>

def check_transaction_atomicity(animal)
  animal.update!(name: 'Cat')
  raise 'Error'
  animal.update!(status: 'created')
end

check_transaction_atomicity(animal)
# RuntimeError: Error

animal.reload
# #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: nil>

您可以看到,我们的数据现在保存在不一致的状态下。名称保存了,但状态不是。这就是为什么我们违反原子原则,因为我们必须全部或全无。

一致性

第二个原理是一致性。一致性意味着数据库必须始终从一个有效状态移至另一个有效状态,以确保数据完成交易完成后符合所有定义的规则和约束。一致性确保事务导致的任何非法状态自动回到先前的有效状态。

如果数据库事务期间不存在一致性,那么即使ID列上存在严格的NOT NULL约束,以下代码也可以成功工作。

animal = Animal.first

def check_transaction_consistency(animal)
  animal.update(id: nil)
end

check_transaction_consistency(animal)

# => ActiveRecord::NotNullViolation: Mysql2::Error: Column 'id' cannot be null

animal.reload.id
# => 1

我们无法控制这一原则。这就是为什么我们应该完全依靠数据库。

隔离

隔离原理不允许干扰交易中的任何数据,直到交易释放为止。可能会发生三个潜在的问题,并破坏隔离,所有这些问题都与交易执行期间的读取数据有关,在这里是:

  • 肮脏的阅读
  • 不可重复阅读
  • 幻影读取

要记住这些问题,您可以考虑CRUD操作(创建,阅读,更新和删除):

  • 肮脏的读取当我们READ data
  • 时可能会出现
  • 当我们UPDATE data
  • 幻影读取。当我们CREATE数据
  • 时可能会出现

让我们讨论一些抽象示例,以深入了解问题。在下一章中,我们将提供一个真实的例子,以更具体地说明这些概念。

肮脏的阅读

当交易读取来自另一项不承诺的交易的数据时,我们将此违规称为肮脏读取。这个问题具有这样的名称,因为它允许我们读取未完成且可能不准确的“肮脏”数据。证明这种违规行为的一个抽象示例如下:

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: nil>

def dirty_read_transaction
  Animal.first.update!(status: 'dirty_read')

  sleep 5

  raise 'Unexpected Error'
end

def try_to_read_data_inside_transaction_during_execution
  sleep 1

  puts Animal.first.status
end

Thread.new { dirty_read_transaction }
Thread.new { try_to_read_data_inside_transaction_during_execution }

# => 'dirty_read'

您可以看到,这里的问题是我们可以访问更新的数据,即使数据未完全投入并可能稍后再回滚。

不可重复的阅读

在交易过程中,将行检索两次,并且行之间的值不同时,就会发生不可重复的读数。此问题具有这样的名称,因为在交易执行结束时,交易中的相同数据可能不相等(“不重复”)。为了证明它,我们可以使用以下片段:

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: 'repeatable_read'>

def non_repeatable_read_transaction
  puts "Current value is: #{Animal.first.status}"

  sleep 5

  puts "Current value is: #{Animal.first.status}"
end

def try_to_update_data_inside_transaction_during_execution
  sleep 1

  Animal.last.update(status: 'non_repeatable_read')
end

Thread.new { non_repeatable_read_transaction }
Thread.new { try_to_update_data_inside_transaction_during_execution }

# => 'Current value is: repeatable_read'
# => 'Current value is: non_repeatable_read'

您可能会注意到,两个读数之间的值不同(“不重复”)。

幻影阅读

当交易过程中,通过另一笔交易添加了新的行以读取正在读取的记录时,就会发生幻影读取。之所以这样的名称,是因为新记录出现在交易执行的中间,例如“幻影”,因为当我们刚开始交易执行时,没有这样的记录。

让我们提供一个打破此原则的例子:


Animal.all
# [
#  #<Animal:0x000000010b7ca748 id: 1, name: "Cat", status: nil>,
#  #<Animal:0x000000010b7ca680 id: 2, name: "Dog", status: nil>
# ]

def phantom_read_transaction
  puts "We have the following animals #{Animal.ids}"

  sleep 5

  puts "We have the following animals #{Animal.ids}"
end

def try_to_create_data_inside_transaction_during_execution
  sleep 1

  Animal.create(name: 'Wolf')
end

Thread.new { phantom_read_transaction }
Thread.new { try_to_create_data_inside_transaction_during_execution }

# => We have the following animals [1, 2]
# => We have the following animals [1, 2, 3]

就是这样。

耐用性

数据库管理的酸方法的最后一个方面是耐用性。

耐用性可确保成功承诺的数据库(交易)的更改即使在系统故障的情况下也将永久生存。这样可以确保数据库中的数据不会通过:

来破坏:

  • 服务中断
  • 崩溃
  • 其他失败案件

让我们来看看以下示例:

# rails c

animal = Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: nil, status: nil>

def check_transaction_durability(animal)
  animal.update!(name: 'Cat')
end

check_transaction_durability(animal)

# power outage

animal.reload.name
# => 'Cat'

如果我们在这里不应用耐久性原则,那么经过一些意外的崩溃,我们将丢失已经投入的数据。我们无法控制这个;这就是为什么我们应该完全依靠数据库。

交易隔离水平

解决了上面在隔离章中提到的问题,数据库提供了4个隔离水平:

  • 读取
  • 阅读订单
  • 可重复阅读
  • 序列化

这些级别中的每个级别都比上一个级别强,并且默认情况下解决了所有以前的问题。

读取不承诺

在此级别上,我们可以回滚交易;每个人都可以在执行过程中更新和读取内部数据。在这个级别上,根本没有隔离。顾名思义,允许此隔离级别读取尚未投入的交易中的数据。让我们看一个示例:

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: nil>

def read_uncommitted_transaction
  Animal.first.update!(status: 'read_uncommitted')

  sleep 5

  raise 'Unexpected Error'
end

def try_to_read_data_inside_transaction_during_execution
  sleep 1

  puts "We have access to the uncommitted value inside the transaction, and the status value is: #{Animal.first.status}"
end

Thread.new { ActiveRecord::Base.transaction(isolation: :read_uncommitted) { read_uncommitted_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :read_uncommitted) { try_to_read_data_inside_transaction_during_execution } }

# => We have access to the uncommitted value inside the transaction, and the status value is: read_uncommitted
# => Error (RuntimeError)

Animal.first.status
# => 'nil'

您可以看到,我们可以访问交易中未承诺的状态,但是在回滚后,最终状态值仍然为零。

读取

第二个隔离级别使我们能够解决肮脏的读问题,但不能解决其他两个(不可重复的读 phantom读取)。正如该隔离级别所暗示的那样,现在我们无法阅读未承诺的数据,而只能读取所承诺的数据。首先,让我们尝试运行相同的示例,但是我们将将隔离级别从:read_uncommitted更改为:read_committed

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: nil>

def read_committed_transaction
  Animal.first.update!(status: 'read_committed')

  sleep 5

  raise 'Unexpected Error'
end

def try_to_read_data_inside_transaction_during_execution
  sleep 1

  puts "We don't have access to uncommitted value inside the transaction, and the status value is still: #{Animal.first.status}"
end

Thread.new { ActiveRecord::Base.transaction(isolation: :read_committed) { read_committed_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :read_committed) { try_to_read_data_inside_transaction_during_execution } }

# => We don't have access to uncommitted value inside the transaction, and the status value is still:
# => Error (RuntimeError)

Animal.first.status
# => 'nil'

您在这里看到,我们无法访问另一笔交易中已更新的值,因为第二笔交易无法从第一个交易中读取数据。我们在这个隔离水平上还有其他问题吗?是的,正如我说的那样,这项交易只能解决肮脏的阅读问题,而 不可重复的读取读取 phantom读取仍然存在。让我们看看:

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: 'initial'>

def read_committed_transaction
  puts "Current value is: #{Animal.first.status}"

  sleep 5

  puts "Current value is: #{Animal.first.status}"
end

def try_to_update_data_inside_transaction_during_execution
  sleep 1

  Animal.first.update!(status: 'non_repeatable_read')
end

Thread.new { ActiveRecord::Base.transaction(isolation: :read_committed) { read_committed_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :read_committed) { try_to_update_data_inside_transaction_during_execution } }

# => 'Current value is: initial'
# => 'Current value is: non_repeatable_read'

您可以看到,第二笔交易正在更改第一个交易,我们会收到不可重复的值。

可重复阅读

第三个隔离级别使我们能够求解肮脏的读 不可重复的读取问题,但仍然无法处理 phantom读取。让我们从上一个示例中运行代码,以确保在此处不会出现不可重复的读取

# rails c

Animal.first
# => #<Animal:0x00000001112ad408 id: 1, name: "Cat", status: 'initial'>

def repeatable_read_transaction
  puts "Current value is: #{Animal.first.status}"

  sleep 5

  puts "Current value is: #{Animal.first.status}"
end

def try_to_update_data_inside_transaction_during_execution
  sleep 1

  Animal.first.update!(status: 'non_repeatable_read')
end

Thread.new { ActiveRecord::Base.transaction(isolation: :repeatable_read) { repeatable_read_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :repeatable_read) { try_to_update_data_inside_transaction_during_execution } }

# => 'Current value is: initial'
# => 'Current value is: initial'

您可以看到,上一个问题在这里消失了,我们有“可重复”的值。

现在,如果我们真的有 Phantom读取问题,让我们看看这里:

# rails c 

Animal.all
# [
#  #<Animal:0x000000010b7ca748 id: 1, name: "Cat", status: nil>,
#  #<Animal:0x000000010b7ca680 id: 2, name: "Dog", status: nil>
# ]

def repeatable_read_transaction
  puts "Inside the current transaction, we have the following animals: #{Animal.ids}"

  sleep 5

  puts "After the time gap, we still have the following animals: #{Animal.ids}"
  Animal.update_all(status: 'phantom_read_triggered')
  puts "After we triggered the update all operation, we have the following animals: #{Animal.ids}"
end

def try_to_create_data_inside_transaction_during_execution
  sleep 1

  Animal.create(name: 'Wolf')
end

Thread.new { ActiveRecord::Base.transaction(isolation: :repeatable_read) { repeatable_read_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :repeatable_read) { try_to_create_data_inside_transaction_during_execution } }

# => Inside the current transaction, we have the following animals [1, 2]
# => After the time gap, we still have the following animals [1, 2]
# => After we triggered the update all operation, we have the following animals [1, 2, 3]

您可以看到,已经添加了一个新的ID,这是没有预期的。让我们尝试使用最新的隔离级别修复它。

可序列化

序列化隔离水平是所有隔离水平中最强的。它提供了最高级别的隔离水平,并确保交易的执行方式相当于顺序运行它们,一个接一个地运行。这意味着不同时执行交易可能会导致异常,例如 dirty读 不可重复的读取 phantom读取。。

让我们使用序列化隔离级别来修复 phantom读取在上一个示例中:

# rails c

Animal.all
# [
#  #<Animal:0x000000010b7ca748 id: 1, name: "Cat", status: nil>,
#  #<Animal:0x000000010b7ca680 id: 2, name: "Dog", status: nil>
# ]

def serializable_transaction
  puts "Inside the current transaction, we have the following animals: #{Animal.ids}"

  sleep 5

  puts "After the time gap, we still have the following animals: #{Animal.ids}"
  Animal.update_all(status: 'phantom_read_fixed')
  puts "After we triggered the update all operation, we have the following animals: #{Animal.ids}"
end

def try_to_create_data_inside_transaction_during_execution
  sleep 1

  Animal.create(name: 'Wolf')
end

Thread.new { ActiveRecord::Base.transaction(isolation: :serializable) { serializable_transaction } }
Thread.new { ActiveRecord::Base.transaction(isolation: :serializable) { try_to_create_data_inside_transaction_during_execution } }

# => Inside the current transaction, we have the following animals [1, 2]
# => After the time gap, we still have the following animals [1, 2]
# => After we triggered the update all operation, we have the following animals [1, 2]

您可以看到,没有幻影记录,所有数据都在交易中正确隔离。

结论

交易隔离级别对于管理数据库中的数据一致性和并发至关重要。我们讨论了四个隔离级别:读取未承诺的读取订单可重复的读 serializable 。每个级别提供不同程度的隔离度,并解决与并发交易有关的特定问题。我们证明了每个隔离水平如何通过实际示例影响交易。通过仔细选择适当的隔离水平,开发人员可以在数据完整性和性能之间取得平衡,以确保交易的行为能够预测并保持数据一致性,即使在多用户环境中也是如此。