MySQL:事务与锁定表

Posted

技术标签:

【中文标题】MySQL:事务与锁定表【英文标题】:MySQL: Transactions vs Locking Tables 【发布时间】:2011-05-12 17:21:54 【问题描述】:

我对事务与锁定表有点混淆,以确保数据库完整性并确保 SELECT 和 UPDATE 保持同步并且没有其他连接干扰它。我需要:

SELECT * FROM table WHERE (...) LIMIT 1

if (condition passes) 
   // Update row I got from the select 
   UPDATE table SET column = "value" WHERE (...)

   ... other logic (including INSERT some data) ...

我需要确保没有其他查询会干扰并执行相同的SELECT(在该连接完成更新行之前读取“旧值”。

我知道我可以默认为 LOCK TABLES table 以确保一次只有 1 个连接在执行此操作,并在完成后将其解锁,但这似乎有点矫枉过正。将其包装在事务中会做同样的事情(确保没有其他连接尝试相同的过程,而另一个仍在处理)?或者SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE 会更好吗?

【问题讨论】:

【参考方案1】:

出于与您在问题中指出的相同原因,我已开始研究同一主题。我对 SO 中给出的答案感到困惑,因为它们是部分答案并且没有提供全局。在我阅读了来自不同 RDMS 提供商的几个文档页面后,这些是我的看法:

交易

语句是数据库命令,主要用于读取和修改数据库中的数据。事务是单个或多个语句执行的范围。它们提供了两件事:

    一种保证事务中的所有语句都正确执行的机制,或者在出现单个错误的情况下,由这些语句修改的任何数据都将恢复到其最后的正确状态(即回滚)。这种机制提供的功能称为原子性。 一种机制,可保证并发读取语句可以查看数据,而不会发生下文所述的部分或全部现象。

脏读:事务读取并发写入的数据 未提交的事务。

不可重复读取:事务重新读取它之前读取的数据 并发现数据已被另一个事务修改(即 自首次读取后提交)。

幻读:事务重新执行查询,返回一组 满足搜索条件并找到行集的行 由于另一个最近提交的条件,满足条件已更改 交易。

序列化异常:成功提交组的结果 事务与所有可能的运行顺序不一致 这些交易一次一项。

这种机制提供的称为隔离,让语句选择事务中不应发生哪些现象的机制称为隔离级别

例如,这是 PostgreSQL 的隔离级别/现象表:

如果任何描述的承诺被数据库系统破坏,更改将回滚并通知调用者。

如何实施这些机制以提供这些保证如下所述。

锁定类型

    独占锁:当在资源上获得独占锁时,无法在该资源上获得其他独占锁。排他锁总是在修改语句(INSERT、UPDATE 或 DELETE)之前获取,并在事务完成后释放。要在修改语句之前显式获取独占锁,您可以使用 FOR UPDATE(PostgreSQL, mysql) 或 UPDLOCK (T-SQL) 等提示。 共享锁:可以在一个资源上获取多个共享锁。但是,共享锁和排他锁不能同时获取一个资源。根据隔离级别的数据库实现,可能会或可能不会在读取语句(SELECT、JOIN)之前获取共享锁。

锁定资源范围

    行:执行语句的单行。 范围:基于语句中给定条件的特定范围(SELECT ... WHERE)。 表格:整个表格。 (主要用于防止批量更新等大语句出现死锁。)

以 SQL-Server 不同隔离级别的默认共享锁行为为例:

死锁

锁定机制的缺点之一是死锁。当一条语句进入等待状态时会发生死锁,因为请求的资源被另一个等待语句持有,而另一个等待语句又在等待另一个等待语句持有的另一个资源。在这种情况下,数据库系统检测到死锁并终止其中一个事务。不小心使用锁会增加死锁的机会,但即使没有人为错误也可能发生。

快照(数据版本)

这是一种隔离机制,可为语句提供在特定时间获取的数据的副本。

    语句开始:为语句执行开始时获取的语句提供数据副本。通过保留这些数据直到事务完成,它还有助于回滚机制。

    事务开始:为事务开始时的语句提供数据副本。

所有这些机制共同提供一致性

当谈到乐观锁和悲观锁时,它们只是对并发问题方法分类的命名。

悲观并发控制:

锁定系统可防止用户以以下方式修改数据 影响其他用户。在用户执行导致 锁定要应用,其他用户无法执行将 与锁发生冲突,直到所有者释放它。这就是所谓的 悲观控制,因为它主要用于环境 数据竞争激烈,保护数据的成本 带锁的成本小于回滚事务的成本,如果 发生并发冲突。

乐观并发控制:

在乐观并发控制中,用户在操作时不会锁定数据 阅读。当用户更新数据时,系统会检查是否有另一个 用户在读取数据后更改了数据。如果另一个用户更新了 数据,引发错误。通常,收到错误的用户 回滚事务并重新开始。这叫乐观 因为它主要用在环境低的地方 数据争用,以及偶尔回滚的成本 事务低于读取时锁定数据的成本。

例如,默认情况下 PostgreSQL 使用快照来确保读取的数据没有更改,如果更改则回滚,这是一种乐观的方法。但是,SQL-Server 默认使用读锁来提供这些承诺。

实施细节可能会根据您选择的数据库系统而有所不同。但是,根据数据库标准,他们需要使用这些机制以一种或另一种方式提供那些声明的事务保证。如果您想了解有关该主题或特定实施细节的更多信息,下面是一些对您有用的链接。

    SQL-Server - Transaction Locking and Row Versioning Guide PostgreSQL - Transaction Isolation PostgreSQL - Explicit Locking MySQL - Consistent Nonlocking Reads MySQL - Locking Understanding Isolation Levels (Video)

【讨论】:

【参考方案2】:

事务概念和锁是不同的。然而,事务使用锁来帮助它遵循 ACID 原则。 如果你想在你读/写的同时防止其他人在同一时间点读/写,你需要一个锁来做到这一点。 如果要确保数据的完整性和一致性,最好使用事务。 我认为在带有锁的事务中混合了隔离级别的概念。 请搜索事务的隔离级别,SERIALIZE 应该是您想要的级别。

【讨论】:

这应该是正确的答案。锁定是为了防止竞争条件,事务是为了更新具有相关数据的多个表。两个完全不同的概念,尽管事务使用锁。【参考方案3】:

您对锁定和事务感到困惑。它们在 RMDB 中是两个不同的东西。锁定防止并发操作,而事务专注于数据隔离。查看 this 的精彩文章以获得澄清和一些优雅的解决方案。

【讨论】:

锁定防止其他人干扰您正在使用的记录简要描述了它的作用,事务防止后来的错误(其他人并行更改的错误)干扰您之前所做的事情(通过允许在有人并行执行某事的情况下回滚)几乎总结了事务......他对这些主题的理解有什么困惑?【参考方案4】:

正如您所说,您希望在事务中使用SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE,因为通常SELECT,无论它们是否在事务中,都不会锁定表。您选择哪一个取决于您是否希望其他事务能够在您的事务进行时读取该行。

http://dev.mysql.com/doc/refman/5.0/en/innodb-locking-reads.html

START TRANSACTION WITH CONSISTENT SNAPSHOT 不会为您解决问题,因为其他事务仍然可以出现并修改该行。这在下面链接的顶部提到。

如果同时有其他会话 更新同一张表 [...] 你可以 看表的状态永远不会 存在于数据库中。

http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html

【讨论】:

【参考方案5】:

锁定表可防止其他数据库用户影响您锁定的行/表。但是锁本身并不能确保你的逻辑处于一致的状态。

想想银行系统。当您在线支付账单时,至少有两个账户会受到交易的影响: 您的账户,用于取款。以及收款人的账户,资金转入该账户。还有银行的账户,他们很乐意将交易中收取的所有服务费存入该账户。鉴于(现在大家都知道)银行非常愚蠢,假设他们的系统是这样工作的:

$balance = "GET BALANCE FROM your ACCOUNT";
if ($balance < $amount_being_paid) 
    charge_huge_overdraft_fees();

$balance = $balance - $amount_being paid;
UPDATE your ACCOUNT SET BALANCE = $balance;

$balance = "GET BALANCE FROM receiver ACCOUNT"
charge_insane_transaction_fee();
$balance = $balance + $amount_being_paid
UPDATE receiver ACCOUNT SET BALANCE = $balance

现在,由于没有锁定和交易,这个系统很容易受到各种竞争条件的影响,其中最大的一种是在您的帐户或收款人的帐户上并行执行多项付款。虽然您的代码已检索到您的余额并正在执行 huge_overdraft_fees() 等等,但其他一些付款完全有可能并行运行相同类型的代码。他们将取回您的余额(例如,100 美元),进行交易(取出您支付的 20 美元,以及他们让您搞砸的 30 美元),现在两个代码路径都有两个不同的余额:80 美元和70 美元。根据哪些最后完成,您最终会在您的帐户中获得这两个余额中的任何一个,而不是您最终应该得到的 50 美元(100 美元 - 20 美元 - 30 美元)。在这种情况下,“银行错误对您有利”。

现在,假设您使用锁。您的账单支付(20 美元)首先进入管道,因此它会获胜并锁定您的帐户记录。现在你有了独家使用权,可以从余额中扣除 20 美元,然后平心静气地把新的余额写回去……你的账户最终如预期的那样有 80 美元。但是...呃...您尝试去更新收款人的帐户,它被锁定了,并且锁定的时间超过了代码允许的时间,使您的交易超时...我们正在与愚蠢的银行打交道,所以没有出现适当的错误处理,代码只是拉出exit(),你的 20 美元就会消失在一股电子中。现在你已经赔了 20 美元,但你还欠收款人 20 美元,你的电话被收回了。

所以...输入交易。你开始一笔交易,你从你的账户中扣除 20 美元,你试图将 20 美元记入收款人的账户中……然后又发生了一些事情。但是这一次,代码可以代替exit(),而是rollback,然后,你的20美元会神奇地添加回你的帐户。

最终归结为:

锁可以防止其他人干扰您正在处理的任何数据库记录。事务可以防止任何“后来的”错误干扰您所做的“早期”的事情。任何一个人都不能保证事情最终会顺利进行。但在一起,他们做到了。

在明天的课程中:死锁的乐趣。

【讨论】:

我也/仍然感到困惑。假设收款人账户开始时有 100 美元,我们正在从我们的账户中添加 20 美元的账单支付。我对事务的理解是,当它们开始时,任何事务中的操作都会看到数据库处于事务开始时的状态。即:在我们更改它之前,接收方帐户有 100 美元。所以...当我们添加 20 美元时,我们实际上设置了 120 美元的余额。但是,如果在我们的交易过程中,有人将收款人账户的金额降至 0 美元,会发生什么?这是否以某种方式阻止?他们会再次神奇地获得 120 美元吗?这就是为什么也需要锁吗? 基本上将事务视为保护代码路径中的事物。锁在“并行”代码路径中保护事物。直到出现死锁...... @MarcB,那么,如果单独使用事务已经保证锁定到位,为什么我们必须明确地进行锁定呢?是否会出现我们必须进行显式锁定的情况,因为仅靠事务是不够的? 这个答案不正确,可能导致错误的结论。此声明:“锁可以防止其他任何人干扰您正在处理的任何数据库记录。事务可以防止任何“以后”的错误干扰您所做的“较早”的事情。单独一个人不能保证事情在结束。但在一起,他们做到了。 - 会让你被解雇,这是非常错误和愚蠢的见文章:en.wikipedia.org/wiki/ACID、en.wikipedia.org/wiki/Isolation_(database_systems) 和 dev.mysql.com/doc/refman/5.1/en/… 所以如果我只是去取款机取20$,(这意味着GET BALANCE,$balance = $balance -20,UPDATE BALANCE),那么一个LOCK就足够了并且没有交易需要,对吧?【参考方案6】:

我在尝试 IF NOT EXISTS ... 然后执行 INSERT 时遇到了类似的问题,当多个线程更新同一个表时会导致竞争条件。

我在这里找到了问题的解决方案:How to write INSERT IF NOT EXISTS queries in standard SQL

我意识到这并不能直接回答您的问题,但是将检查和插入作为单个语句执行的相同原则非常有用;您应该能够对其进行修改以执行更新。

【讨论】:

【参考方案7】:

我会用一个

START TRANSACTION WITH CONSISTENT SNAPSHOT;

开头,还有一个

COMMIT;

结束。

如果您的存储引擎支持事务(即 InnoDB),那么您在两者之间所做的任何事情都与数据库的其他用户隔离。

【讨论】:

除了他选择的表不会被其他会话锁定,除非他特别锁定它(或直到他的 UPDATE 发生),这意味着其他会话可能会出现并在 SELECT 和更新。 在阅读了 MySQL 文档中的 START TRANSACTION WITH CONSISTENT SNAPSHOT 之后,我看不到它实际上在哪里锁定了另一个连接以防止更新同一行。我的理解是它会看到表格在事务开始时开始。因此,如果另一个事务正在进行中,已经获得了一行并且即将更新它,那么第二个事务仍然会在更新之前看到该行。因此,它可能会尝试更新其他事务即将更新的同一行。这是正确的还是我在进行中遗漏了什么? @Ryan 它不做任何锁定;你是对的。锁定(或不锁定)取决于您执行的操作类型(SELECT/UPDATE/DELETE)。 我明白了。它确实提供了您自己的事务读取一致性,但不会阻止其他用户在您之前修改行。

以上是关于MySQL:事务与锁定表的主要内容,如果未能解决你的问题,请参考以下文章

14.Mysql事务控制和锁定

PDO,mysql,事务和表锁定

mysql锁分析

mysql数据库锁定机制

MySQL 储存引擎 MyISAM 和 InnoDB 配置

Django 事务锁定表