MySQL InnoDB мертвая блокировка на SELECT с эксклюзивной блокировкой (ДЛЯ ОБНОВЛЕНИЯ) - PullRequest
6 голосов
/ 25 марта 2011

Я делаю это, чтобы убедиться, что только один экземпляр этого процесса запущен (псевдокод php / mysql innodb):

START TRANSACTION
$rpid = SELECT `value` FROM locks WHERE name = "lock_name" FOR UPDATE
$pid = posix_getpid();
if($rpid > 0){
  $isRunning = posix_kill($rpid, 0);
  if(!$isRunning){ // isRunning
    INSERT INTO locks values('lock_name', $pid) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)
  }else{
    ROLLBACK
    echo "Allready running...\n";
    exit();
  }
}else{ // if rpid == 0 -
  INSERT INTO locks values('lock_name', $pid) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)
}
COMMIT

...............

//free the pid
INSERT INTO locks values('lock_name', 0) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)

Блокировки таблицы содержат следующие поля:

id - primary, autoinc
name - varchar(64) unique key
description - text
value - text

IПолагаю, что время от START TRANSACTIN до COMMIT / ROLLBACK действительно составляет миллисекунды - времени не хватает даже на то, чтобы получить тайм-аут.Как можно получить тупик с этим кодом?Я не использую другие таблицы в этой транзакции.Похоже, что тупик невозможен.Если одновременно запускаются 2 процесса, то первый, получивший блокировку в этом ряду, будет запущен, а другой будет ожидать снятия блокировки.Если блокировка не снята в течение 1 минуты, то ошибка «тайм-аут», а не тупик.

Ответы [ 3 ]

7 голосов
/ 25 марта 2011

SELECT FOR UPDATE получает намеренную монопольную блокировку таблицы до получения монопольной блокировки записи.

Следовательно, в этом сценарии:

X1: SELECT FOR UPDATE -- holds IX, holds X on 'lock_name'
X2: SELECT FOR UPDATE -- holds IX, waits for X on 'lock_name'
X1: INSERT -- holds IX, waits for X for the gap on `id`

возникает тупик, поскольку обе транзакции удерживают блокировку IX на таблице и ожидают блокировку X для записей.

Собственно, этот самый сценарий описан в MySQL руководстве по блокировке .

Чтобы обойти это, вам нужно избавиться от всех индексов, кроме того, который вы ищете, то есть lock_name.

Просто бросьте первичный ключ на id.

0 голосов
/ 28 марта 2011

Только что понял благодаря ответу Quassnoi ...

Я могу сделать:

$myPid = posix_getpid();
$gotIt = false;
while(true){
  START TRANSACTION;
  $pid = SELECT ... FOR UPDATE; // read pid and get lock on it
  if(mysql_num_rows($result) == 0){
    ROLLBACK;// release lock to avoid deadlock
    INSERT IGNORE INTO locks VALUES('lockname', $myPid);
  }else{
    //pid existed, no insert is needed
    break;
  }
}

if($pid != $myPid){ //we did not insert that
  if($pid>0 && isRunning($pid)){
    ROLLBACK;
    echo 'another process is running';
    exit;
  }{
    // no other process is running - write $myPid in db
    UPDATE locks SET value = $myPid WHERE name = 'lockname'; // update is safe
    COMMIT;
  }
}else{
  ROLLBACK; // release lock
}
0 голосов
/ 25 марта 2011

Не видя реального кода PHP, трудно быть уверенным - но возможно ли, что вы фактически не используете одно и то же соединение с базой данных между запуском SELECT и INSERT?

Обычно я предпочитаю не использовать транзакции, если я могу избежать этого; Ваша проблема может быть решена путем создания одного запроса к базе данных в соответствии с

insert into locks
select ('lockname', $pid)
from locks
where name not in
(select name from locks)

Получив доступ к затронутым строкам, вы можете увидеть, запущен ли уже процесс ...

...