MySQL:事务与锁定表

时间:2010-11-19 16:00:16

标签: sql mysql transactions locking

我对事务与锁定表有点混淆,以确保数据库完整性,并确保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会更好吗?

6 个答案:

答案 0 :(得分:151)

锁定表会阻止其他数据库用户影响您锁定的行/表。但是锁定本身并不能确保你的逻辑以一致的状态出现。

想想银行系统。当您在线支付账单时,至少有两个账户受到交易的影响:您的账户,从中获取资金。接收方的账户,资金转入其中。银行的账户,他们将愉快地存入交易所收取的所有服务费。鉴于(如众所周知的那样)银行非常愚蠢,让我们说他们的系统是这样的:

$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美元可以神奇地添加回你的帐户。

最后,归结为:

锁定会阻止其他人干扰您正在处理的任何数据库记录。交易使任何“后期”错误不会干扰您已完成的“早期”事情。两者都不能保证事情最终成功。但他们在一起。

明天的教训:僵局的喜悦。

答案 1 :(得分:14)

正如您所说,您希望在事务中使用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

答案 2 :(得分:6)

尝试IF NOT EXISTS ...然后执行INSERT时出现类似问题,当多个线程更新同一个表时,这会导致竞争条件。

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

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

答案 3 :(得分:3)

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

交易

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

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

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

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

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

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

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

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

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

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

锁类型

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

锁定资源范围

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

举个例子,SQL-Server 不同隔离级别的默认共享锁行为: enter image description here

死锁

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

快照(数据版本)

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

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

  2. 交易开始:为交易开始时的语句提供数据副本。

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

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

<块引用>

悲观并发控制:

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

乐观并发控制:

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

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

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

  1. SQL-Server - Transaction Locking and Row Versioning Guide
  2. PostgreSQL - Transaction Isolation
  3. PostgreSQL - Explicit Locking
  4. MySQL - Consistent Nonlocking Reads
  5. MySQL - Locking
  6. Understanding Isolation Levels (Video)

答案 4 :(得分:2)

你对锁和&amp;交易。它们是RMDB中的两个不同的东西。当事务关注数据隔离时,Lock会阻止并发操作。查看this伟大的文章,了解澄清和一些优雅的解决方案。

答案 5 :(得分:1)

我会用

START TRANSACTION WITH CONSISTENT SNAPSHOT;

开始,和

COMMIT;

以。结束。

如果你的存储引擎支持交易(即InnoDB),你在中间做的任何事情都会与数据库的其他用户隔离