我对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。
我的问题是:
我对一般答案很感兴趣,但如果没有,我对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
答案 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'并发模式将执行此操作,对于单行操作,它们几乎相同。
要使它完全原子化,你必须:
您还可以使用HOLDLOCK(T-SQL)或等效提示强制对读取进行独占锁定,具体取决于您的SQL方言。
单个更新查询将以原子方式执行此操作,但您无法分割操作(可能是读取值并将其返回给客户端),而不确保读取取出独占锁定。 您需要以原子方式获取值才能实现序列,因此更新本身可能并不是您所需要的全部内容。 即使使用原子更新,您仍然有一个竞争条件来读取更新后的值。读取仍然必须在事务中进行(存储它在变量中的内容)并发出阅读期间独家锁定。
请注意,要在不创建热点的情况下执行此操作,数据库需要在存储过程中对autonomous (nested) transactions提供适当的支持。请注意,有时'嵌套'用于引用链接事务或保存点,因此该术语可能有点令人困惑。我编辑过这个来引用自治交易。
如果没有自治事务,您的锁将由父事务继承,可以回滚整个事务。这意味着它们将一直保持到父事务提交为止,这可以将您的序列转变为使用该序列序列化所有事务的热点。尝试使用序列的任何其他内容都将阻塞,直到整个父事务提交为止。
IIRC Oracle支持自治事务,但DB / 2直到最近都没有,而SQL Server则没有。我不知道InnoDB是否支持它们,但是Grey and Reuter在很长一段时间内仍然关注它们的实施难度。在实践中,我猜它很可能不会。 YMMV。