art*_*hur 1 postgresql concurrency postgresql-9.1 acid except
我正在使用 Postgres DB 为大量计算机/进程实现作业调度。简而言之,每个作业都有其 id,所有调度都通过三个选项卡实现:所有作业、当前正在运行的作业和已完成的作业。
调度的关键功能是 (1) 请求作业和 (2) 通知 DB 已完成的作业。请求作业从作业列表中获取任何 id,它不在运行表中,也不在已完成表中:
insert into piper.jobs_running
select x.fid from (
SELECT fid FROM piper.jobs
except
select fid from piper.jobs_running
except
select fid from piper.jobs_completed
) as x limit 1
returning(fid)
Run Code Online (Sandbox Code Playgroud)
完成作业会将其从运行列表中删除,并将其插入到已完成列表中。因为它不是特定于并发的,所以我省略了 SQL 命令(完成一项工作需要几十分钟到几个小时。)
对我来说,这是一个令人讨厌的惊喜,上面运行完全相同的查询的两个进程(几乎同时请求作业)可能会获得相同的作业 ID (fid)。我要提出的唯一可能的解释是 Postgres 不依赖 ACID 一致性。注释?
附加信息:我将事务设置为可序列化(在postgresql.conf 中 set default_transaction_isolation = 'serializable')。现在,如果隔离未完全填充,DBMS 会使事务失败。是否可以强制 Postgres 自动重新启动它们?
PostgreSQL 默认为READ COMMITTED隔离,看起来您没有使用任何不同的东西。在READ COMMITTED每个语句中都有自己的快照。它无法看到来自其他事务的未提交更改;就好像它们根本没有发生过一样。
现在,想象一下你在三个会话中同时运行它,在一个设置中有三个条目 in jobs、 zero injobs_running和 zero in jobs_completed:
insert into piper.jobs_running
select x.fid from (
SELECT fid FROM piper.jobs
except
select fid from piper.jobs_running
except
select fid from piper.jobs_completed
) as x limit 1
returning(fid)
Run Code Online (Sandbox Code Playgroud)
每次运行都会选择 中的三个作业jobs。因为它们的快照都是在它们中的任何一个提交更改之前拍摄的,甚至在创建未提交的行之前,*所有它们都会在jobs_running和 中找到零行jobs_completed。
所以他们都要求一份工作。可能是相同的工作,因为即使没有ORDER BY,扫描顺序也会相同。
行锁定跨越国界,让您可以在事务之间进行通信以强制排序。所以你可能认为这会解决你的问题,但它不会。
如果您FOR UPDATE对row条目加锁,因此该行被独占锁定,则该锁将一直保持,直到事务提交或回滚。所以你会认为下一个事务会得到一个不同的行,或者如果它试图得到同一行,它会等待锁释放,看到现在有一个条目jobs_running,然后跳过该行。你错了。
将会发生的是您将锁定所有行。一个事务将成功获得所有行的锁。其他事务将执行相同的索引或顺序扫描,通常会尝试以相同的顺序锁定行,在第一次锁定尝试时卡住,并等待第一个事务回滚或提交。如果您不走运,它们可能会开始锁定不同的行集并相互产生死锁,从而导致死锁中止,但通常您只是得不到有用的并发性。
更糟糕的是,第一个事务选择它锁定的行之一,插入一行jobs_running并提交,释放锁。然后另一个事务能够继续,并锁定所有行......但它没有获得数据库状态的新快照(快照在语句开始时拍摄),所以*它看不到你插入一排成jobs_running。因此,它获取相同的作业,将该作业的行插入到 中jobs_running,然后提交。
PostgreSQL 有一个奇怪的特性,大多数数据库都没有,如果一个事务阻塞在一个锁上,它会在第一个事务提交后重新检查所选行是否仍然匹配锁条件。
这就是/sf/ask/807278531/ 中的示例有效的原因- 它依赖于WHERE在回收锁后重新检查子句中的限定符。
锁定的使用强制所有内容串行运行,因此在实践中,您最好使用单个连接来完成工作。
PostgreSQL 中的事务隔离并不是事务并发运行的完美理想,但它们的效果与串行执行是一样的。
现实世界中唯一能够提供完美隔离的数据库是在写入事务第一次访问每个表时独占锁定每个表的数据库,因此在实践中,如果事务对数据库的不同部分进行并发访问,则它只能是并发访问。在需要并发或有用的情况下,没有人愿意使用这样的数据库。
所有现实世界的实现都是妥协。
READ COMMITTED 默认PostgreSQL 的默认值是READ COMMITTED隔离,这是一个明确定义的隔离级别,允许不可重复和幻读,如PostgreSQL 手册中关于事务隔离的讨论。
SERIALIZABLE 隔离您可以在每个事务的基础上或作为每个用户、每个数据库或(不推荐的)全局默认值请求更严格的SERIALIZABLE隔离。这提供了更强大的保证,虽然仍然不完美,但代价是如果它们以串行运行时不会发生的方式交互,则强制回滚事务。
因为您的并发查询将始终尝试获取第一个作业,无论有多少作业,除了一个作业外,所有作业都会因序列化失败而中止。因此,在实践中,您不会获得任何有用的并发性,也可能只有一个连接将工作分发给工作人员。
(请注意,在 PostgreSQL 9.1 之前,SERIALIZABLE提供的保证要弱得多,并且不会检测到大量事务相互依赖的情况。)
SERIALIZABLEPostgreSQL里面没有自动重新运行SERIALIZABLE的交易是中止由于串行化故障。在某些情况下这会非常有用,但在其他情况下这样做会完全错误和危险 - 特别是在涉及通过应用程序读取/修改/写入周期的情况下。目前不支持在序列化失败时自动重新运行事务。预计应用程序将重试。
看起来您要做的是编写一个排队系统。考虑不这样做。编写一个健壮、可靠和正确的排队系统是很困难的,并且已经有一些很好的可供您采用。您必须处理诸如崩溃安全之类的事情,当有人执行任务然后未能完成它时会发生什么,围绕完成的竞争条件就像您放弃完成它的任务处理程序一样,等等。有很多微妙的并发问题。不要尝试DIY。
SKIP LOCKED仍在开发中的 PostgreSQL 9.5 添加了一项功能,使排队变得更加容易。
它让您说,如果当您SELECT ... FOR UPDATE锁定一行时,您应该忽略它并继续查找下一个未锁定的行。这在与 a 结合使用时非常有用,LIMIT因为它可以说“在其他人尚未尝试声明的第一行找到我”。因此,编写安全并发地抢占作业的队列变得非常简单。
在该功能可用之前,我强烈建议您坚持使用单连接队列管理,或使用经过良好测试的任务队列系统。
| 归档时间: |
|
| 查看次数: |
703 次 |
| 最近记录: |