6 种 .NET 中的锁定方法(悲观锁定和乐观锁定)






4.85/5 (105投票s)
6 种 .NET 中的锁定方法(悲观锁定和乐观锁定)
6 种 .NET 中的锁定方法(悲观锁定和乐观锁定)
引言 |
引言
本文讨论了 6 种 .NET 中的锁定方法。它首先讨论并发问题,然后讨论 3 种乐观锁定方法。由于乐观锁定无法从根本上解决并发问题,因此它引入了悲观锁定。然后,它继续解释隔离级别如何帮助我们实现悲观锁定。每种隔离级别都通过示例演示来使概念更清晰。
为什么我们需要锁定?
在多用户环境中,多个用户可能会同时更新同一条记录,导致用户之间产生混乱。这个问题被称为并发。
我们如何解决并发问题?
可以通过实施适当的“锁定策略”来解决并发问题。锁定可防止在其他资源已在对其执行操作时对该资源执行操作。
并发会导致哪些类型的混乱?
并发会导致 4 种主要问题,下表显示了它们的详细信息。
问题 | 简要说明 | 解释 |
脏读 | “脏读”发生在当一个事务正在读取一个记录,而该记录是另一个事务未完成工作的一部分时。 | • 用户 A 和用户 B 看到的值是“5”。 • 用户 B 将值“5”更改为“2”。 • 用户 A 仍然看到值是“5”……发生了脏读。 |
不可重复读 | 如果在每次读取数据时都得到不同的值,那么这就是“不可重复读”问题。 | • 用户 A 看到的值是“5”。 • 用户 B 将值“5”更改为“2”。 • 用户 A 刷新后看到值是“5”,他感到惊讶……发生了不可重复读。 |
幻行 | 如果“UPDATE”和“DELETE”SQL 语句不影响数据,则可能出现“幻行”问题。 | • 用户 A 将所有值“5”更新为“2”。 • 用户 B 插入一条值为“2”的新记录。 • 用户 A 选择所有值为“2”的记录,如果所有值都已更改,他仍然惊讶地发现值为“2”的记录……幻行已插入。 |
丢失更新 | “丢失更新”是指一个成功写入数据库的更新被另一个事务的更新覆盖的情况。 | • 用户 A 将所有值从“5”更新为“2”。 • 用户 B 进来将所有“2”值更新为“5”。 • 用户 A 丢失了他所有的更新。 |
那么,我们如何解决上述问题?
通过使用乐观或悲观锁定,接下来的文章将讨论相同的内容。
什么是乐观锁定?
顾名思义,“乐观”它假设多个事务可以协同工作而不会相互影响。换句话说,在进行乐观锁定期间不强制执行任何锁定。事务只是验证没有其他事务修改了数据。如果发生修改,则回滚事务。
乐观锁定是如何工作的?
您可以通过多种方式实现乐观锁定,但实现乐观锁定的基本原理保持不变。这是一个 5 步过程,如下所示:
• 记录当前时间戳。
• 开始更改值。
• 在更新之前,通过检查旧时间戳和新时间戳来检查是否有人修改了值。
• 如果不相等,则回滚,否则提交。
我们可以通过哪些不同的解决方案来实现乐观锁定?
我们在 .NET 中实现乐观锁定主要有 3 种方法:
• **数据集:**- 数据集默认实现乐观锁定。它们在更新前会检查旧值和新值。
• **时间戳数据类型:** - 在表中创建时间戳数据类型,在更新时检查旧时间戳是否等于新时间戳。
• **检查旧值和新值:** - 获取值,进行更改,并在进行最终更新时检查数据库中的旧值和当前值是否相等。如果不相等,则回滚,否则提交值。
解决方案一:数据集
正如前一部分所述,数据集本身就处理乐观并发。下面是一个简单的快照,我们在 Adapter 的 update 函数上设置了调试断点,然后从 SQL Server 中更改了值。当我们取消断点运行“update”函数时,它抛出了“Concurrency”异常错误,如下所示。
如果在后端运行分析器,您可以看到它会执行 update 语句,检查当前值和旧值是否相同。
exec sp_executesql N'UPDATE [tbl_items] SET [AuthorName] = @p1 WHERE (([Id] = @p2) AND ((@p3 = 1 AND [ItemName] IS NULL) OR ([ItemName] = @p4)) AND ((@p5 = 1 AND [Type] IS NULL) OR ([Type] = @p6)) AND ((@p7 = 1 AND [AuthorName] IS NULL) OR ([AuthorName] = @p8)) AND ((@p9 = 1 AND [Vendor] IS NULL) OR ([Vendor] = @p10)))',N'@p1 nvarchar(11),@p2 int,@p3 int,@p4 nvarchar(4),@p5 int,@p6 int,@p7 int,@p8 nvarchar(18),@p9 int,@p10 nvarchar(2)',@p1=N'this is new',@p2=2,@p3=0,@p4=N'1001',@p5=0,@p6=3,@p7=0,@p8=N'This is Old Author',@p9=0,@p10=N'kk'
在此场景中,我们试图将字段值“AuthorName”更改为“This is new”,但在更新时,它会与
旧值“This is old author”进行比较。下面是上述 SQL 的简化代码片段,显示了与旧值的比较。
,@p8=N'This is Old Author'
解决方案二:使用时间戳数据类型
另一种进行乐观锁定方法是使用 SQL Server 的“TimeStamp”数据类型。时间戳会自动生成
每次更新 SQL Server 数据时都会生成一个唯一的二进制数字。时间戳数据类型用于为记录更新添加版本。
为了实现乐观锁定,我们首先获取旧的“TimeStamp”值,当我们尝试更新时,我们检查旧时间戳
是否等于当前时间戳,如下面的代码片段所示。
update tbl_items set itemname=@itemname where CurrentTimestamp=@OldTimeStamp
然后我们检查是否发生了任何更新,如果没有发生更新,我们使用 SQL Server 的“raiserror”语句引发一个严重的错误“16”
如下面的代码片段所示。
if(@@rowcount=0) begin raiserror('Hello some else changed the value',16,10) end
如果发生任何并发冲突,当您调用“ExecuteNonQuery”到客户端时,您应该会看到错误传播,如下
面的图所示。
解决方案三:检查旧值和新值
很多时候我们想只针对某些字段检查并发,而忽略标识符等字段。对于这类场景
我们可以检查更新字段的旧值和新值,如下面的代码片段所示。
update tbl_items set itemname=@itemname where itemname=@OldItemNameValue
但是,使用乐观锁定似乎并没有真正解决并发问题?
是的,你说得对。通过使用乐观锁定,您只能检测到并发问题。要从根本上解决并发问题
本身,我们需要使用悲观锁定。乐观锁定就像预防,而悲观锁定实际上是治疗。
什么是悲观锁定?
悲观锁定假定会发生并发/冲突问题,因此会对记录加锁,然后更新数据。
我们如何进行悲观锁定?
我们可以通过在 SQL Server 存储过程、ADO.NET 级别指定“IsolationLevel”或使用 Transaction Scope 对象来执行悲观锁定。
通过悲观锁定可以获取哪些类型的锁?
您可以获取 4 种锁:共享锁、排他锁、更新锁和意向锁。前两种是实际锁,而后两种
是混合锁和标记。
何时使用? | 允许读取 | 允许写入 | |
共享锁 | 当您只想读取并且不希望其他事务进行更新时。 | 是 | 否 |
排他锁 | 当您想修改数据,并且不希望任何人读取事务,也不希望任何人更新时。 | 否 | 否 |
更新锁 | 这是一个混合锁。当您想要执行一个在实际更新发生之前需要经过多个阶段的更新操作时,可以使用此锁。它首先在读取阶段以共享锁开始,然后在实际更新时获取排他锁。 | ||
读取阶段 | 是 | 否 | |
操作阶段 | 是 | 否 | |
更新阶段 | 否 | 否 | |
意向锁(请求锁) | 意向锁用于锁定层次结构。当您想锁定层次结构中的资源时,可以使用此锁。例如,表上的共享意向锁意味着在表中的页面和行上放置了共享锁。 | 不适用 | 不适用 |
架构锁 | 当您更改表结构时。 | 否 | 否 |
批量更新锁 | 执行批量更新时使用 | 表级别 否 | 表级别 否 |
更新锁令人困惑,您能详细解释一下吗?
其他锁都比较直接,更新锁由于其混合性质而令人困惑。很多时候,在更新之前我们会读取记录。因此,在读取时锁是共享的,而在实际更新时,我们希望拥有排他锁。更新锁更多的是瞬时锁。
那么,有哪些不同类型的隔离级别,何时应该使用它们?
有 4 种事务隔离级别,下表简单显示了何时使用它们以及它们放置了什么锁。
隔离级别 | 读取 | 更新 | Insert |
未提交读 | 读取尚未提交的数据。 | 允许 | 允许 |
已提交读(默认) | 读取已提交的数据。 | 允许 | 允许 |
可重复读 | 读取已提交的数据。 | 不允许 | 允许 |
串行化 | 读取已提交的数据。 | 不允许 | 不允许 |
我们如何指定隔离级别?
隔离级别是 RDBMS 软件的功能,换句话说,它们本质上属于 SQL Server,而不属于 Ado.NET、EF 或 LINQ。说了这么多,您始终可以从这些组件中的任何一个设置事务隔离级别。
中间层
在中间层,您可以使用 Transaction Scope 对象指定隔离级别。
TransactionOptions TransOpt = New TransactionOptions(); TransOpt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted; using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, TransOptions)) { }
ADO.NET
您也可以使用 ADO.NET 中的“SqlTransaction”对象指定事务隔离级别。
SqlTransaction objtransaction = objConnection.BeginTransaction(System.Data.IsolationLevel.Serializable);
SQL Server
您也可以使用 TSQL 中的“SET TRANSACATION ISOLATION LEVEL”指定隔离级别,如下面的代码片段所示。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
哪个事务隔离级别解决了并发中的哪些问题?
下面是一个图表,显示了哪个事务隔离级别解决了哪些并发问题。
已提交读(S) | 可重复读(I) | 可串行化 | 未提交读 | |
脏读 | 解决 | 解决 | 解决 | X |
丢失更新 | X | 解决 | 解决 | X |
不可重复读 | X | 解决 | 解决 | X |
幻行 | X | X | 解决 | X |
解决方案 4:我们可以看看已提交读如何解决脏读吗?
关于已提交读的一些重要要点:
• 它是 SQL Server 的默认事务隔离级别。
• 它只读取已提交的数据。换句话说,任何未提交的数据都不会被读取,直到提交发生。下图对此进行了更详细的解释。您可以看到更新
如果您想实际看到上述内容,请执行以下操作:
• 打开 2 个查询窗口,执行一个更新事务,但不要提交。
• 在第二个窗口中尝试执行 select 查询,它将显示一个被阻塞的查询,如下面的图所示。
那么,未提交读是已提交读的对立面吗?
是的,未提交读是已提交读的对立面。当您将事务隔离级别设置为未提交读时,也会读取未提交的数据。
关于已提交读的一些重要要点:
• 可以看到未提交的数据,因此可能发生脏读。
• 不持有锁。
• 在锁定不重要,而并发和吞吐量更重要时有用。
如果您想测试相同的内容,请执行下面的 SQL 语句,该语句执行更新并回滚。回滚会在 20 秒延迟后发生。在此期间,如果您执行 select 查询,您将获得未提交的数据,20 秒后您将看到旧数据,这已提交的数据已被回滚。
set transaction isolation level read uncommitted Begin Tran Update customer set CustomerName='Changed' where CustomerCode='1001' WAITFOR DELAY '000:00:20' rollback tran
set transaction isolation level read uncommitted select * from Customer where CustomerCode='1001'
解决方案 5:我们可以看看可重复读如何解决丢失更新和不可重复读吗?
通过将隔离级别设置为可重复读,没有人可以读取和更新数据。关于可重复读隔离级别的要点如下:
• 当为 select 查询设置可重复事务隔离级别时,只读取已提交的数据。
• 当您使用可重复读选择一条记录时,其他事务无法更新该记录,但可以进行选择。
• 如果在 update 查询中设置了可重复事务,直到事务完成,任何人都无法读取或更新该数据。
• 当 select 和 update 查询设置为可重复读时,其他事务可以插入新记录。换句话说,可能发生幻行。
如果您想测试此隔离级别,请执行以下语法,然后尝试执行 select 和 update 查询,它们将被阻塞,50 秒后您应该会看到数据。
set transaction isolation level repeatable read Begin Tran Update customer set CustomerName='Changed' where CustomerCode='1001' WAITFOR DELAY '000:00:50' rollback tran
如果您在可重复读模式下执行以下 select 查询,您将在 50 秒内无法更新,直到事务完成。
set transaction isolation level repeatable read begin tran select * from Customer where CustomerCode='1001' WAITFOR DELAY '000:00:50' commit tran
一个重要的注意事项是,您可以添加客户代码为 1001 的新记录,换句话说,可能发生幻行。
解决方案 6:可串行化隔离级别如何解决幻行问题?
这是最高级别的隔离级别;在此级别下,其他事务无法插入、更新、删除或选择记录。
关于可串行化事务的一些要点:
• 当隔离级别为可串行化时,任何其他事务都不能插入、更新、删除或选择。
• 阻塞很多,但所有并发问题都得到解决。
set transaction isolation level serializable begin tran select * from Customer where CustomerCode='1001' WAITFOR DELAY '000:00:50' commit tran
在什么情况下我们应该使用乐观锁定和悲观锁定?
工作...
什么是死锁,共享锁如何避免死锁?
仍在工作...
什么是锁提示?
仍在工作...
如需进一步阅读,请观看以下面试准备视频和分步视频系列。