SQL原子增量和锁定策略 - 这样安全吗?

Ale*_*ing 43 sql locking atomic increment

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

START TRANSACTION;
UPDATE images SET counter=counter+1 WHERE image_id=some_parameter;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

假设特定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
Run Code Online (Sandbox Code Playgroud)

结果:image_id = 15的值'1'不正确,应为2.

我的问题是:

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

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

编辑:以下方案也可能,导致相同的行为.我假设我们处于隔离级别READ_COMMITED或更高级别,因此s2从事务开始获取值,尽管s1已经向计数器写入"1".

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
Run Code Online (Sandbox Code Playgroud)

Qua*_*noi 29

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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

请注意InnoDB,DML查询不会从他们读取的记录中提取更新锁.

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


Con*_*lls 8

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

为了避免这种竞争条件,您需要在读取操作上设置独占锁定.'Serializable'和'Repeatable Read'并发模式将执行此操作,对于单行操作,它们几乎相同.

要使它完全原子化,你必须:

  • 设置适当的事务隔离级别,例如Serializable.通常,您可以从客户端库执行此操作,也可以在SQL中使用explicilty.
  • 开始交易
  • 阅读数据
  • 更新它
  • 提交交易.

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

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

请注意,要在不创建热点的情况下执行此操作,数据库需要对存储过程中的自治(嵌套)事务提供适当的支持.请注意,有时'嵌套'用于引用链接事务或保存点,因此该术语可能有点令人困惑.我编辑过这个来引用自治交易.

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

IIRC Oracle支持自治事务,但直到最近才有DB/2,而SQL Server则没有.我不知道InnoDB是否支持他们,但 Gray和Reuter在很长一段时间内对他们实施起来有多困难.在实践中,我猜它很可能不会.因人而异.