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

时间:2010-09-29 12:15:35

标签: sql locking atomic increment

我对SQL和锁定策略有疑问。举个例子,假设我的网站上有图像的视图计数器。如果我有一个或类似的话来执行以下声明:

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)或显式锁定技术)吗?
  5. 我对一般答案很感兴趣,但如果没有,我对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
    

2 个答案:

答案 0 :(得分: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

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

请注意,在InnoDB中,DML查询不会从他们阅读的记录中解除更新锁定。

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

答案 1 :(得分:8)

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

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

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

  • 设置适当的transaction isolation level,例如Serializable。通常,您可以从客户端库执行此操作,也可以在SQL中执行此操作。
  • 开始交易
  • 阅读数据
  • 更新
  • 提交交易。

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

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

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

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

IIRC Oracle支持自治事务,但DB / 2直到最近都没有,而SQL Server则没有。我不知道InnoDB是否支持它们,但是Grey and Reuter在很长一段时间内仍然关注它们的实施难度。在实践中,我猜它很可能不会。 YMMV。