概述
在本文中,我们将讨论数据库交易在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 。每个级别提供不同程度的隔离度,并解决与并发交易有关的特定问题。我们证明了每个隔离水平如何通过实际示例影响交易。通过仔细选择适当的隔离水平,开发人员可以在数据完整性和性能之间取得平衡,以确保交易的行为能够预测并保持数据一致性,即使在多用户环境中也是如此。