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

Posted

技术标签:

【中文标题】SQL 原子增量和锁定策略 - 这安全吗?【英文标题】:SQL atomic increment and locking strategies - is this safe? 【发布时间】:2011-04-18 19:11:35 【问题描述】:

我有一个关于 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。

我的问题是:

    这种情况可能吗? 如果是,那么事务隔离级别是否重要? 是否有冲突解决程序可以将此类冲突检测为错误? 是否可以使用任何特殊语法来避免问题(例如比较和交换 (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

【问题讨论】:

mysql ***.com/questions/4358732/… ||女士***.com/questions/193257/… 【参考方案1】:

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 查询不会从它们读取的记录中解除更新锁。

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

【讨论】:

感谢您的精彩解释。我认为这里的关键短语是“提交:锁定释放”。这意味着所有想要更新行的事务都需要等待持有锁的事务完成,从而有效地序列化所有争用该行的事务。您知道这将如何在 Postgres 等多版本并发数据库中工作吗?由于它使用多个版本,我假设它会让事务独立进行并尝试合并结果。还是采用相同的策略? 如 ConcernedOfTunbridgeWells 在他的回答中所建议的那样,s1 在开始时放置更新锁是否需要最低事务隔离级别? @disown:在PostgreSQL 使用的MVCC 中根本没有锁定的概念。相反,使用事务标识符作为标记来存储记录的多个版本。仅在尝试修改处于边缘状态的版本时才会发生锁定。 @disown: DML 查询总是放置UPDATE 锁来读取记录,与事务隔离级别无关(即使它是READ UNCOMMITTED)。我现在说的是SQL Server @Quassnoi 更新语句是否需要在事务中才能避免竞争条件?还是无论如何都安全? (单个更新语句,无事务)【参考方案2】:

如果锁定没有正确完成,那么肯定有可能获得这种类型的竞争条件,并且默认锁定模式(读取提交)确实允许它。在这种模式下,读取只在记录上放置一个共享锁,因此它们都可以看到 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。

【讨论】:

使用单个 UPDATE 查询,无论使用哪种事务隔离级别,都无法在任何支持事务的主要系统中获得这种竞争条件。 如果我正确理解了这个问题,隔离是不够的,SERIALIZABLE 可能是,但可能不是快照隔离(en.wikipedia.org/wiki/Snapshot_isolation),因为我不确定“冲突”的定义这个案例。两者是冲突还是不冲突?并且即使在更新期间对行有排他锁,如果 s2:s update 介于 s1:s update 和 commit 之间怎么办?那么 s2 会读到什么?可以说是 0。在这种情况下,我能看到确保正确行为的唯一方法是让 s1 保持排他锁直到其提交之后。 @Quassnoi:另一个事务在更新之后和提交之前读取了什么?如果你有例如 READ_COMMITED,让另一个事务看到值“1”是错误的(会偷看另一个事务)。 “0”是其他并发事务应该能够看到的唯一其他逻辑值。所以要么这种竞争条件应该能够发生(并可能导致故障),要么数据库需要以某种方式序列化事务。 无论隔离级别如何,更新查询都会锁定记录,但在此期间您将无法检索数据。也就是说,您看不到显示页数的含义。 @Concerned:问题中没有提到这种愿望。无论如何,UPDATE 支持RETURNING 子句在SQL Server 2005 及以上。

以上是关于SQL 原子增量和锁定策略 - 这安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

WIN7系统不知为何没办法修改账户锁定时间

设置帐户锁定策略 若用户连续有3次无效登陆则该帐户自动锁定20分钟 怎么弄

如何设置windows的账户锁定时间

我们可以用两个或多个无锁容器原子地做一些事情而不锁定两者吗?

ACID底层实现原理一致性非锁定读(MVCC的原理)BufferPool缓存机制重做日志刷盘策略隔离级别

ACID底层实现原理一致性非锁定读(MVCC的原理)BufferPool缓存机制重做日志刷盘策略隔离级别