46

我有一个关于 SQL 和锁定策略的问题。例如,假设我有一个网站上的图片的查看计数器。如果我有一个 sproc 或类似的执行以下语句:

START TRANSACTION;
UPDATE images SET counter=counter+1 WHERE image_id=some_parameter;
COMMIT;

假设特定 image_id 的计数器在时间 t0 的值为“0”。如果两个会话更新相同的图像计数器,s1 和 s2,同时在 t0 开始,这两个会话是否有可能都读取值“0”,将其增加到“1”,并且都尝试将计数器更新为“1” ',所以计数器将得到值'1'而不是'2'?

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok

最终结果:image_id=15 的值“1”不正确,应该是 2。

我的问题是:

  1. 这种情况可能吗?
  2. 如果是这样,事务隔离级别是否重要?
  3. 是否有冲突解决程序可以将此类冲突检测为错误?
  4. 是否可以使用任何特殊语法来避免问题(例如比较和交换 (CAS) 或显式锁定技术)?

我对一般答案感兴趣,但如果没有我对 MySql 和 InnoDB 特定的答案感兴趣,因为我正在尝试使用这种技术在 InnoDB 上实现序列。

编辑:以下情况也是可能的,导致相同的行为。我假设我们处于隔离级别 READ_COMMITED 或更高级别,因此尽管 s1 已将“1”写入计数器,但 s2 从事务开始时获取值。

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: read counter for image_id=15, get 0 (since another tx), store in temp2
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok
4

2 回答 2

32

UPDATE查询在它读取的页面或记录上放置一个更新锁。

当决定是否更新记录时,锁要么被解除,要么被提升为排他锁。

这意味着在这种情况下:

s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1

s2将等到s1决定是否写计数器,这种情况实际上是不可能的。

会是这样的:

s1: place an update lock on image_id = 15
s2: try to place an update lock on image_id = 15: QUEUED
s1: read counter for image_id=15, get 0, store in temp1
s1: promote the update lock to the exclusive lock
s1: write counter for image_id=15 to (temp1+1), which is 1 
s1: commit: LOCK RELEASED
s2: place an update lock on image_id = 15
s2: read counter for image_id=15, get 1, store in temp2
s2: write counter for image_id=15 to (temp2+1), which is 2

请注意,在 中InnoDBDML查询不会从它们读取的记录中解除更新锁。

这意味着在全表扫描的情况下,已读取但决定不更新的记录仍将保持锁定状态,直到事务结束,并且无法从另一个事务更新。

于 2010-09-29T12:59:38.397 回答
8

如果锁定没有正确完成,那么肯定有可能获得这种类型的竞争条件,并且默认锁定模式(读取提交)确实允许它。在这种模式下,读取只在记录上放置一个共享锁,因此它们都可以看到 0、递增它并将 1 写入数据库。

为了避免这种竞争情况,您需要在读取操作上设置排他锁。'Serializable' 和 'Repeatable Read' 并发模式可以做到这一点,对于单行上的操作,它们几乎是等价的。

要使其完全原子化,您必须:

  • 设置适当的事务隔离级别,例如 Serializable。通常,您可以从客户端库或 SQL 中的显式执行此操作。
  • 开始交易
  • 读取数据
  • 更新它
  • 提交事务。

您还可以使用 HOLDLOCK (T-SQL) 或等效提示强制对读取进行排他锁定,具体取决于您的 SQL 方言。

单个更新查询将自动执行此操作,但您不能拆分操作(可能是读取值并将其返回给客户端)而不确保读取获取排他锁。 您需要以原子方式获取值以实现序列,因此更新本身可能并不是您所需要的全部。 即使使用原子更新,您仍然有一个竞争条件来读取更新后的值。 读取仍然必须在事务中进行(将它获得的内容存储在变量中)并在读取期间发出排他锁。

请注意,要在不创建热点的情况下执行此操作,您的数据库需要对存储过程中的自治(嵌套)事务具有适当的支持。请注意,有时“嵌套”用于指代链接事务或保存点,因此该术语可能有点混乱。我已对此进行了编辑以引用自主事务。

如果没有自治事务,您的锁将由父事务继承,这可以回滚整个事务。这意味着它们将一直保留到父事务提交,这可以将您的序列变成使用该序列序列化所有事务的热点。任何其他尝试使用该序列的东西都会阻塞,直到整个父事务提交。

IIRC Oracle 支持自治事务,但 DB/2 直到最近才支持,而 SQL Server 不支持。我不知道 InnoDB 是否支持它们,但 Gray 和 Reuter继续讨论它们的实现难度。在实践中,我猜它很可能不会。YMMV。

于 2010-09-29T12:47:13.570 回答