• 关系数据库事务中的对象锁定


    概述、锁粒度和死锁

    最近,我们有一些关于数据库事务的文章,有关强制执行四个 ACID 属性(原子性、一致性、隔离性、持久性)。在今天的文章中,我们将研究关系数据库(RDBMS)用于强制执行 ACID 属性的另一种机制,即对象锁定。具体来说,我们将了解它是什么,它在 RDBMS 事务中扮演什么角色,以及锁定可能导致的一些副作用。虽然数据库对象锁定可能是一个相当技术性和复杂的主题,但我们会以浅白的用语解释它并尽可能简单易明。

    什么是对象锁定?

    简单地说,对象锁定是一种防止同时访问数据库中数据的方法,以避免数据不一致。为了说明对象锁定的工作原理,假设两个银行柜员试图为两笔不同的交易更新同一个银行帐户。两个柜员都检索(即复制)账户记录。柜员 A 申请并保存一笔交易。柜员 B 将不同的交易应用到自己保存的副本,并保存结果,这一笔交易会覆盖柜员 A 输入的交易。现在记录不再反映第一笔交易,就好像它从未发生过一样!

    修复方法是在任何用户修改记录时锁定该记录,使其他用户不能同时更改它。这可以防止记录被错误地覆盖,但一次只允许处理一条记录,从而锁定需要在同一实例中编辑记录的其他用户。因此,任何试图检索相同记录进行编辑的人都会因为锁定而被拒绝写入访问(取决于具体的实现,他们可能仍然能够以只读状态查看记录)。一旦记录被保存(或取消编辑),锁定就会被释放。通过防止保存记录以覆盖其他更改,数据完整性(ACID 中的 I)就得以保持。

    锁定粒度

    上面的示例演示了一个记录级锁定的实例。现在想象一下,如果上面的两个银行柜员为两个不同的客户提供服务,但他们的帐户都在同一个账本中。在这种情况下,整个账本(或者,一个或多个数据库表)将需要被锁定以进行编辑。可以想象,锁定整个表会导致大量不必要的等待。如果柜员可以从账本中移除一页,其中包含当前客户的帐户(可能还有一些其他帐户),那么可以同时为多个客户提供服务,前提是每个客户的帐户都位于与其他客户不同的页上。如果两个客户在同一页上都有帐户,则一次只能服务一个客户。这类似于数据库中的页级锁定。

    有四种类型的锁定。它们的粒度越来越细:

    • 数据库锁定
    • 表锁定
    • 页锁定
    • 行锁定

    锁定粒度和死锁

    粒度锁定的使用使用可能导致称为“死锁”的情况。当使用增量锁定(锁定一个实体,然后锁定一个或多个附加实体)时,可能会发生死锁。例如,我和我的妻子经常在我们的个人帐户之间转账。如果我们每个人都要求柜员获取我们的个人帐户信息,以便我们可以将一些钱转入另一方的帐户,那么这两个帐户基本上会被锁定。然后,当我们的柜员试图将钱转入彼此的帐户时,他们都会发现另一个帐户“正在使用”,迫使他们等待帐户被释放。不知不觉中,两个柜员一直在等对方,直到对方放弃退回帐户,双方都无法完成交易!值得庆幸的是,有人已经设计了各种技术来规避此类问题。这些将在下一部分中讨论。

    悲观与乐观锁定

    关系数据库系统(RDBMS)在修改(例如,更新或删除)表记录时采用各种锁定策略来强制实施事务 ACID 属性。有时,当两个并发事务无法进行时,可能会发生死锁,因为每个事务都在等待对方释放锁定。在本系列的第 1 部分中,我们知道了什么是关系数据库中的对象锁定、不同类型的锁定和死锁。在今天的后续文章中,我们将比较悲观锁定和乐观锁定的优缺点。

    悲观锁定

    当使用悲观锁定,资源在事务中第一次访问时开始一直被锁定,直到事务完成,在此期间其他事务无法访问它。在大多数事务只是读取而不更新资源的情况下,排他锁可能会导致更多的锁争用(死锁)。回想一下第 1 部分中的银行示例,一旦在交易中访问该帐户,它就会被锁定。任何在其他交易中使用该帐户的尝试都会导致其他进程被延迟,直到帐户锁定被释放,或者该进程交易将被取消并回滚到之前的状态。

    乐观锁定

    当使用乐观锁定,资源在第一次被事务访问时实际上并没有被锁定。相反,资源的初始状态会被持久化。其他事务仍然能够访问该资源,这使得冲突更改的可能性成为已知风险。在提交事务时,当持久化存储中的资源即将更新时,资源的状态会再次从存储中读取,并与在事务中首次访问资源时保存的状态进行比较。如果两种状态不同,则意味着进行了冲突更新,因此事务将会回滚。在我们的银行示例中,帐户的金额将在首次访问时保存。如果交易更改了帐户金额,则将在金额即将更新之前再次从存储中读取该金额。如果交易开始后金额发生了变化,交易本身就会失败,否则新的金额将保持不变并被保存。

    在悲观锁定和乐观锁定之间做出决定

    既然我们已经介绍了两种类型的锁定是什么,问题就变成了应该使用哪种。在大多数情况下,乐观锁定更有效并提供更高的性能。与此同时,悲观锁定提供了更好的数据完整性,但是,锁定的管理更难,因为遇到死锁的可能性更大。在悲观锁定和乐观锁定之间进行选择时,请考虑以下三个准则:

    • 如果有大量更新并且用户尝试同时更新数据的机会相对较高,则悲观锁定很有用。
    • 悲观锁定更适合包含经常更新的小型表的应用程序。在这些“热点”的情况下,冲突的可能性很大,以至于乐观锁定浪费了回滚冲突事务的气力。
    • 当发生冲突的可能性非常低时,乐观锁定很有用,即有很多记录但用户相对较少,或者很少更新而主要是读取操作。

    避免死锁或将死锁减至最少

    在关系数据库系统(RDBMS)中,死锁是两个并发事务无法进行的情况,因为每个事务都在等待另一个释放锁定。在本系列的第 1 部分中,我们知道了什么是关系数据库中的对象锁定、不同类型的锁定和死锁。然后,在第 2 部分中,我们比较了悲观锁定和乐观锁定的优缺点。在本篇文章中,我们将探讨导致死锁的几个原因,以及避免死锁或至少将死锁减至最少的策略。

    低效的查询

    死锁在某种程度上是不可避免的,但只要两个事务的其中一个及时结束,死锁就会很少发生并且不会导致灾难。事实证明,阻塞问题的最常见来源之一是冗长且低效的 SQL 语句,这些语句会导致数据库在运行时“挂起”。这些可以通过两个步骤来补救:

    • 优化性能不佳的 SQL 语句,以便在最短的时间内释放锁。
    • 在同一会话中执行任何长时间运行的 SQL 语句之前,标识是否可以释放锁。

    例如,如果由于执行 DELETE 语句而获取锁,并且紧随其后的是执行完整表扫描的 SELECT 语句,你应该确定是否可以在它们之间执行 COMMIT 语句。这应该有助于更早地释放锁。

    嵌套事务

    阻塞问题的另一个常见原因是睡眠会话已经失去了对事务嵌套级别的跟踪。例如,如果应用程序取消 SQL 语句或超时但未发出 COMMIT 或 ROLLBACK 语句,则资源可能会无限期地保持锁定状态。处理此问题的一些方法包括:

    • 在出现任何应用程序错误后,在应用程序的错误处理程序中提交 IF@@TRANCOUNT > 0 ROLLBACK TRAN 语句。
    • 在启动事务的任何存储过程中包括 SET XACT_ABORT ON 语句,特别是如果它们在错误后没有清理。通过这样做,如果发生运行时错误,任何打开的事务都将中止并将控制权返回给客户端。
    • 如果连接池由打开连接并在将连接返回到池之前运行一些查询的应用程序使用,那么你可能需要考虑暂时禁用连接池。通过这样做,数据库服务器连接被物理注销,导致服务器回滚任何打开的事务。

    取得部分结果

    一个鲜为人知的死锁来源是不会一次性取得所有结果行的应用程序。这是一个问题,因为当查询已发送到服务器时,应用程序必须能够取得所有结果行以完成查询。如果没有发生这种情况,表会被锁定,这会导致其他用户阻塞。因此,请尝试对你的应用程序进行编码,以便它们取得所需的全部行,而不是将其分散到多次迭代中。

    往期回顾

    Navicat 被投毒了 | 真相来了!

    盗版引发设备瘫痪

    Navicat 16.1 为OceanBase 社区版

    Navicat 成为信通院数据库创新实验室成员

    Navicat 学术伙伴计划 - 免费教育版申请

    Navicat 技术智库 - 实战演练与各类热门问题解答

    免费试用攻略 | Navciat 16 数据库管理工具

  • 相关阅读:
    C语言:结构体——关于内存字节对齐图文详解
    Spark和Hadoop作业之间的区别
    docker save与docker export的区别
    Springboot毕设项目购物网站用户评论分析系统6ik2l(java+VUE+Mybatis+Maven+Mysql)
    基于php+mysql的房屋销售管理系统
    【Java 进阶篇】JDBC插入数据详解
    线程同步的实现
    react-window构造的虚拟列表使用react-resizable动态调整宽度和使用react-drag-listview拖拽变换列位置的问题
    软件测试|测试方法论—边界值
    Collectors类作用:
  • 原文地址:https://blog.csdn.net/weixin_53935287/article/details/126014274