postgresql中的锁定和选择更新的一致性

时间:2011-06-27 18:56:25

标签: python sql postgresql

我有一个可以支持一定数量的并发操作的应用程序。这由postgres中的“槽”表表示。当节点联机时,它们会在表中插入多行,每个插槽一行。当作业声称插槽时,它们会更新表格中的一行,声称其中一个插槽,并在完成后再次释放。

插槽表如下所示:

CREATE TABLE slots (
    id INT8 PRIMARY KEY DEFAULT nextval('slots_seq'),
    node_name TEXT NOT NULL,
    job_name TEXT
);

在任何时候它都有一些半固定数量的行,每行都可能填充或不填充作业名称。

当一个新作业想要启动时,它会运行这些查询以获取它应该运行的节点的名称:

BEGIN;
LOCK TABLE slots IN ACCESS EXCLUSIVE MODE;
SELECT id, node_name
    FROM slots
    WHERE job_name IS NULL
    LIMIT 1
    FOR UPDATE;

(从光标读出node_name和id)

UPDATE slots
    SET job_name = %(job_name)s
    WHERE id = %(slot_id)s;
COMMIT;

这通常能够声明行而不会丢失任何更新,但具有更高的并发级别,只有少量行将被声明,而许多SELECT ... FOR UPDATE和UPDATE查询已被执行。最终结果是我们最终运行的工作量远远超过它们的插槽数。

我是否发生锁定错误?有没有更好的方法来解决这个问题?什么东西不使用表锁?

事务级别SERIALIZABLE不会删除它,只会填充少量行。

我正在使用postgresql版本8.4。

3 个答案:

答案 0 :(得分:2)

好吧,我在perl中编写了一个程序来模拟正在发生的事情,因为我不认为你说的是​​可能的。确实在运行我的模拟后,即使我关闭了锁定也没有任何问题(因为SELECT … FOR UPDATEUPDATE应该进行必要的锁定)。

我在PG 8.3和PG 9.0上运行了它,它在两个位置都运行良好。

我恳请您尝试该程序和/或尝试使用python版本来获得一个非常紧凑的测试用例,您可以与该课程共享。如果它确实有效,你可以调查它们之间的差异,如果它不起作用,你就会有其他人可以玩的东西。

#!/usr/bin/perl
use DBI;
$numchild = 0;
$SIG{CHLD} = sub { if (wait) {$numchild--;} };

sub worker($)
{
  my ($i) = @_;
  my ($job);

  my $dbh = DBI->connect("dbi:Pg:host=localhost",undef,undef,{'RaiseError'=>0, 'AutoCommit'=>0});

  my ($x) = 0;
  while(++$x)
  {
#    $dbh->do("lock table slots in access exclusive mode;") || die "Cannot lock at $i\n";
    my @id = $dbh->selectrow_array("select id from slots where job_name is NULL LIMIT 1 FOR UPDATE;");

    if ($#id < 0)
    {
      $dbh->rollback;
      sleep(.5);
      next;
    }
    $job = "$$-$i-($x)";
    $dbh->do("update slots set job_name='$job' where id=$id[0];") || die "Cannot update at $i\n";
    $dbh->commit || die "Cannot commit\n";
    last;
  }
  if (!$job)
  {
    print STDERR "Could not find slots in 5 attempts for $i $$\n" if ($ENV{'verbose'});
    return;
  }
  else
  {
    print STDERR "Got $job\n" if ($ENV{'verbose'} > 1);
  }
  sleep(rand(5));

#  $dbh->do("lock table slots in access exclusive mode;") || die "Cannot lock at $i\n";
  $dbh->do("update slots set usage=usage+1, job_name = NULL where job_name='$job';") || die "Cannot unlock $job";
  print STDERR "Unlocked $job\n" if ($ENV{'verbose'} > 2);
  $dbh->commit || die "Cannot commit";
}

my $dbh = DBI->connect("dbi:Pg:host=localhost",undef,undef,{'RaiseError'=>0, 'AutoCommit'=>0});

$dbh->do("drop table slots;");
$dbh->commit;
$dbh->do("create table slots (id serial primary key, job_name text, usage int);") || die "Cannot create\n";
$dbh->do("insert into slots values (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0), (DEFAULT,NULL,0);") || die "Cannot insert";
$dbh->commit;

for(my $i=0;$i<200;$i++)
{
  if (!fork)
  {
    worker($i);
    exit(0);
  }

  if (++$numchild > 50)
  {
    sleep(1);
  }
}
while (wait > 0)
{
  $numchild--;
  print "Waiting numchild $numchild\n";
  sleep(1);
}
my $dbh = DBI->connect("dbi:Pg:host=localhost",undef,undef,{'RaiseError'=>0, 'AutoCommit'=>0});
my $slots = $dbh->selectall_arrayref("select * from slots;") || die "Cannot do final select";
my $sum=0;
foreach my $slot (@$slots)
{
  printf("%02d %3d %s\n",$slot->[0], $slot->[2], $slot->[1]);
  $sum += $slot->[2];
}
print "Successfully made $sum entries\n";

答案 1 :(得分:2)

BEGIN; 
LOCK TABLE slots IN ACCESS EXCLUSIVE MODE; 
UPDATE slots SET job_name = '111' WHERE id IN (SELECT id FROM slots WHERE job_name IS NULL LIMIT 1) RETURNING *;
COMMIT;

这似乎适用于Read Committed。它只是sql(与你的代码相同),可以在一次调用中执行(更快)。

@Seth Robertson:没有LOCK TABLE且没有while循环,这是不安全的。

如果同时存在事务A和事务B:A将选择第一行,B将选择第一行。 A将锁定并更新行,B必须等到A提交。然后B将重新检查条件job_name IS NULL。它是假的,B不会更新 - B不会选择下一行,但只会重新检查并返回空结果。

@joegester:SELECT FOR UPDATE不是问题,因为所有表都被锁定了。

也许有另一种方法可以做 - 如果你删除并插入行(在其他表中?)而不是设置NULL。但我不确定如何。

答案 2 :(得分:1)

您可能需要查看advisory locks

尚未测试,但可能会像这样重写您的锁定查询:

BEGIN;
SELECT id, node_name
    FROM slots
    WHERE job_name IS NULL
    AND pg_try_advisory_lock('slots'::regclass::int, id::int)
    LIMIT 1;

或者,因为你首先使用bigint(你需要那么多的ID?!?),例如:

BEGIN;
SELECT id, node_name
    FROM slots
    WHERE job_name IS NULL
    AND pg_try_advisory_lock(hashtext('slots_' || id))
    LIMIT 1;

如果你这样做,要警惕陷阱 - 无论交易成功与否,都需要明确解锁每个会话

hashtext()的情况下也存在碰撞的风险,但如果你正在处理工作,那对你来说没什么大不了的......