为什么 MySQL 可以同时处理多个更新而 PostgreSQL 不能?

SDR*_*yes 4 mysql innodb postgresql deadlock isolation-level

假设您有一个具有以下定义的表:

CREATE TABLE public.positions
(
   id serial,
   latitude numeric(18,12),
   longitude numeric(18,12),
   updated_at timestamp without time zone
)
Run Code Online (Sandbox Code Playgroud)

您在此表中有 50,000 行。现在出于测试目的,您将运行如下更新:

update positions
set  updated_at = now()
where latitude between 234.12 and 235.00;
Run Code Online (Sandbox Code Playgroud)

该语句将从 50,000 行(在此特定数据集中)更新 1,000 行。

如果您在 30 个不同的线程中运行这样的查询*,MySQL innodb 将成功,而 PostgreSQL 将因大量死锁而失败。

为什么?


* 我正在比较最新版本的 MySQL innodb 与 Postgres,这是一个并发更新案例。生产案例:想象有 5000 只库存不断更新,最新价格不断可用。

Rol*_*DBA 11

历史课

2013 年 3 月 13 日,Uber 已从 MySQL 切换到 PostgrsSQL

出人意料的是,这段恋情并没有持续多久。

2016 年 6 月 26 日,Uber 已经从 PostgreSQL 切换到 MySQL

为什么是关于脸???

您的实际问题

UPDATE在 PostgreSQL 中运行一个令人惊讶的微管理。

PostgreSQL 中有两个 DDL 系统标识符必须正确理解

  • ctid:行版本在其表中的物理位置。请注意,尽管 ctid 可用于非常快速地定位行版本,但如果行ctid被更新或移动,则行会发生变化VACUUM FULL。因此ctid作为长期行标识符是无用的。OID,或者更好的是用户定义的序列号,应该用于标识逻辑行。
  • oid:一行的对象标识符(对象 ID)。仅当表是使用 WITH OIDS 创建的,或者当时设置了 default_with_oids 配置变量时,才会出现此列。此列是 typeoid(与列同名);有关该类型的更多信息,请参阅有关对象标识符类型的 PostgreSQL 文档

关于 ctid,请注意执行和UPDATE (从物理角度来看DELETE/ INSERT)或VACUUM FULL将更改行的 ctid。这对于具有许多索引的表来说不是好兆头。为什么 ?Uber 的数据工程师最近发现了这一事实(2016 年 7 月 26 日)。

请注意下图(优步工程提供)

索引ctid管理

PostgreSQL 不会将 aPRIMARY KEY与非唯一索引紧密耦合。由于索引引用了一行ctid,一个简单的UPDATE(即使在非索引列上)将更改ctid,导致需要重写ctid表中引用更改行的每个索引中的 。这不是什么新鲜事。PostgreSQL 一直是这样设计的

因此,执行UPDATE1000 行将执行 1000DELETEs和 1000 INSERTs。如前所述,附加到表的每个索引都必须将其新行ctid值写入 BTREE 索引条目,以替换旧行ctid值。由于您在表上没有索引,因此您只会遇到DELETEsINSERTs产生您所看到的死锁。

补充信息

有人曾经发布过关于这个问题以及如何解决这个问题的讨论。其他人利用暴露在其 WHERE 子句中可用的系统标识符:

默认事务隔离级别是READ COMMITTED. 移动索引将在此隔离级别下提供更好的数据一致性,但其代价是不断重写ctid索引中的值以及删除和插入全新行的类似 MongoDB 的行为只是为了一个简单的UPDATE. 虽然其他事务隔离级别是可能的(REPEATABLE READREAD UNCOMMITTEDSERIALIZABLE),但它可能需要一些针对 ctid 和 oid 值的专门 SQL 来维护任何给定事务中所需的数据视图。这种专门的查询可能是有用的SELECTs,但将是相当不值得信任的UPDATEs,并DELETEs因为还有一个VACUUM运行的守护程序和的可能性ctid过时将是巨大的。在这种情况下,它会通过明智的做法是参考排在查询oid不如说citd

关于 INNODB ???

InnoDB 不会像 PostgreSQL 那样跳过箍来执行 UPDATE。InnoDB 不会强迫您编写专门的 SQL 命令来利用 rowid-type 信息来充实 UPDATE。

InnoDB 的默认隔离级别是REPEATABLE READ. 这比READ COMMITTEDInnoDB rowids 永远不会改变的事实更容易维护。

为了让 InnoDB 具有相同的不良行为,您必须运行REPLACE 命令(这是一个机械 DELETE 和 INSERT)。PostgreSQL 会自动执行此操作,您对此无能为力(除非您有足够的勇气获取源代码并修复或改进该UPDATE过程)。

  • 我不确定优步在这里的相关性是什么。他们的问题是与写放大无关的问题,不幸的是他们不是向社区提出的问题。在处理低级堆处理时,您对 update = delete+insert 的简化也不完全准确,因为执行 delete+insert 对触发器、FK、唯一索引处理、并发更新等具有不同的结果。你是对的,InnoDB 的索引组织表使更新更简单。他们在其他地方也有成本,就像其他一切一样,但他们在这里显然是赢家。 (2认同)

dez*_*zso 10

Rolando 已经或多或少描述了僵局发生的原因。让我补充一点,PostgreSQL 必须锁定要更新的行,以保持一致性。(now()固定在事务的开始,这意味着两个并发更新将希望将给定的行更新为不同的时间戳,这必须以某种方式解决 - 在行上加锁是 PostgreSQL 解决方案。)

锁以先到先得的方式分配给事务。当两个事务试图以不同的顺序获取同一行上的锁时,就会发生死锁。这样做的方法 - 现在并不奇怪 - 以可预测的顺序请求锁定。这是由

SELECT id -- could be any column, even a constant
FROM position 
WHERE latitude between 234.12 and 235.00
ORDER BY id 
FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

这里的关键部分是ORDER BY idFOR UPDATE。后者需要后续需要的相同类型的必要锁UPDATE

SELECT ... FOR UPDATE否则,该语句可能看起来是多余的(尤其是当您不需要所选数据时),并且这也意味着甚至在实际更改数据之前就已写入磁盘。使用时必须考虑到这一点。

  • 您可能想补充一点,您可以使用可写 CTE 将 `select` 与 `update` 结合起来:`with to_update (select ... ) 更新位置 set updated_at = now() where id in (select id from to_update) `. 特别是对于“昂贵”的 where 子句,这可能比在 `update` 语句中评估 where 条件 _again_ 更快。 (3认同)

a_h*_*ame 10

我认为这与 Postgres 或 MySQL 处理 UPDATE 的方式无关(或者 Uber切换回来的有些没有实际意义的理由

此外,对于给定的设置,Postgres不会为每次更新创建新的行版本,因为没有更改索引值。这种情况称为 HOT(仅堆元组)更新。参见例如此处此处此处(另外:作为对 Uber 评论家的反应,目前正在对此进行优化)

死锁的罪魁祸首总是从不同的线程以不同的顺序获取锁——这与如何获取锁或如何在后台物理完成更改无关。

MySQL/InnoDB 和 Postgres 之间的最大区别在于,MySQL始终使用 Oracle 中所谓的“索引组织表”(或 SQL Server 中的“聚集索引”)。所以表的行实际上存储在索引结构中(这并不总是一个好的选择)。

这反过来意味着行按主键物理“排序”(如果没有主键,MySQL 将为此添加一个不可见的列)并且对没有索引的表的更新将简单地扫描整个表,直到所有找到与 where 子句匹配的行。

由索引组织表强加的“排序”存储意味着(最有可能)总是以相同的顺序为每个线程扫描行 - 这反过来意味着每个线程以相同的顺序获取锁- 从而避免死锁。

Postgres 没有索引组织表(聚集索引),因此获取锁的顺序不是由某些隐式顺序决定的,因此可能会发生死锁。

鉴于 Oracle 中常规表(或 SQL Server 中没有聚集索引的表)的性质,如果 Oracle 或 SQL Server 表现出与 Postgres 相同的行为,我不会感到惊讶。


我试图重现这个并创建了一个小(Java)测试程序。

该表是这样创建的:

create table positions
(
   id integer,
   value_1 numeric(18,12), 
   value_2 numeric(18,12),
   updated_at timestamp
);

insert into positions (id, value_1, value_2)
select i, random() * 50000  + 1, random() * 50000  + 1
from generate_series(1, 50000) i;
Run Code Online (Sandbox Code Playgroud)

所以我在表中有 50.000 行value_1value_2列的范围从 1 到 50.000

Java 程序创建了 50 个线程,将每个线程连接到数据库,一旦所有线程都连接起来,实际更新将并行启动 - 这样做是为了避免第一个线程在最后一个连接之前已经完成)。

update positions 
  set updated_at = current_timestamp
where value_1 between 5000 and 30000;
Run Code Online (Sandbox Code Playgroud)

所以它更新了大约 25.000 行(表的一半)。

无论是在 Postgres 9.5 还是 9.6 中,我都没有陷入僵局。将死锁超时从 1 秒降低到 25 毫秒后,我仍然没有遇到死锁。虽然我确实收到了很多如下所示的锁定警告(如果我启用log_lock_waits

create table positions
(
   id integer,
   value_1 numeric(18,12), 
   value_2 numeric(18,12),
   updated_at timestamp
);

insert into positions (id, value_1, value_2)
select i, random() * 50000  + 1, random() * 50000  + 1
from generate_series(1, 50000) i;
Run Code Online (Sandbox Code Playgroud)

即使在更新后让每个线程休眠 25 毫秒后,但提交之前,我也没有遇到任何死锁。

我还将行数增加到 500.000,更新行数增加到 250.000,仍然没有死锁。

你可以在这里看到测试程序:http : //hastebin.com/usuruqolez.java

  • 我在野外见过更新/更新排序死锁,它可能发生,尽管它远非典型。`UPDATE ... ORDER BY` 会很好。这里接受的答案是荒谬的夸大其词,这并没有改变。 (2认同)