【问题标题】:MySQL InnoDB dead lock on SELECT with exclusive lock (FOR UPDATE)SELECT 上的 MySQL InnoDB 死锁,带有排他锁(FOR UPDATE)
【发布时间】:2011-03-25 12:24:39
【问题描述】:

我这样做是为了确保该进程的实例只运行一次(伪代码 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

我相信从 START TRANSACTIN 到 COMMIT/ROLLBACK 的时间实际上是毫秒 - 甚至没有足够的时间来获得超时。这段代码怎么可能陷入僵局?我不使用此事务中的其他表。看起来死锁是不可能的。如果 2 个进程同时启动,第一个获得该行锁的进程将继续进行,另一个将等待锁被释放。如果锁没有在 1 分钟内释放,则错误是“超时”,而不是死锁。

【问题讨论】:

    标签: mysql transactions innodb deadlock


    【解决方案1】:

    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 manual on locking 中描述了这种情况。

    要解决此问题,您需要删除除您正在搜索的索引之外的所有索引,即lock_name

    只需将主键放在id 上即可。

    【讨论】:

    • 所以如果我使用 UPDATE 而不是 INSERT,主键不会受到伤害。问题也不在一张桌子上。还有其他表我无法删除主键。还有其他解决方法吗?当您对一行进行 X 锁定时,应该有一种方法可以保存插入一行
    • 我想困难的方法是只有一个唯一索引的表锁,并在 X 锁之前使用它来锁定并插入更复杂的表。但这必须在每个可能死锁的插入之前完成,我希望有其他方法。那么有没有另一种方法可以在具有多个(唯一?)索引的表上安全插入。
    【解决方案2】:

    如果没有看到实际的 PHP 代码,很难确定 - 但您是否可能在运行 SELECT 和 INSERT 之间实际上没有使用相同的数据库连接?

    如果可以避免的话,我通常不喜欢使用交易;您的问题可以通过创建单个数据库查询来解决

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

    通过访问受影响的行,您可以查看进程是否已经在运行...

    【讨论】:

    • 在更新之前无法检查从表中选择的 pid 是否仍在运行。如果 'lockname' 已经在表中,但它已经崩溃(未运行)怎么办?新进程永远不会获得锁。我需要选择&lcok->检查它是否正在运行->如果没有使用新 pid 运行更新
    【解决方案3】:

    感谢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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-10-21
      • 1970-01-01
      • 2020-11-24
      • 2014-03-10
      • 2010-10-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多