在INSERT语句中使用多个SELECT防止竞争条件

时间:2017-02-01 01:53:23

标签: php mysql innodb

我正在编写一个查询,以防止并发查询搞砸了。 我很确定这是一个更好的方法,这就是我在这里的原因。

基本上表格的列如下所示:

id      : INT(10) unsigned auto_incremented
number  : INT(6) unsigned zerofill
date    : DATETIME

当我插入新行时,我需要设置数字值:

  • 如果最后插入的行的日期是今天,那么数字 =最后插入行的数字+ 1
  • 如果最后插入的行的日期是昨天或之前,那么数字 = 1

我想到首先选择最后一行的日期和数字,使用php设置新数字,然后进行插入。

$pdo->beginTransaction();

$stmt = $pdo->prepare("SELECT `number`, `date` FROM `table` ORDER BY `id` DESC LIMIT 1");
$stmt->execute();
$infos = $stmt->fetch(PDO::FETCH_ASSOC);

$date = date("Y-m-d H:i:s");
if (date("Ymd", strtotime($infos['date'])) < date("Ymd", strtotime($date))) {
    $number = 1;
} else {
    $number = $infos['number'] + 1;
}

$stmt2 = $pdo->prepare("INSERT INTO `table` (`number`, `date`) VALUES (:number, :date)");
$stmt2->execute(array(":number" => $number, ":date" => $date));

$pdo->commit();

交易是否足以阻止另一个线程选择相同的号码并因此尝试插入我想要的相同号码?

由于我自己无法回答这个问题,即使使用我们亲爱的朋友Google和SO,我还是认为“我不能让MySQL完成所有的工作吗?”并提出了这个问题:

INSERT INTO `table`(`number`, `date`)
SELECT IF(
    DATE((SELECT `date` FROM `table` ORDER BY `id` DESC LIMIT 1)) < CURDATE(),
    '1',
    (SELECT `number` + 1 FROM `table` ORDER BY `id` DESC LIMIT 1)
), NOW()

这是一种更好的方法吗?

我应该在做任何事之前锁定桌子吗?

我太傻了,不知道应该怎么做?

1 个答案:

答案 0 :(得分:0)

这可能就足够了:

首先,更改表格。没有iddate is a DATE`,&#34;自然&#34; PK。

number INT(6) unsigned zerofill,
date   DATE,
PRIMARY KEY(date, number)

然后交易是

BEGIN;
($not_first, $number) = SELECT date = CURDATE(), number
                     FROM tbl
                     ORDER BY date DESC, number DESC
                     LIMIT 1
                     FOR UPDATE;  -- important
$number = $not_first ? $number+1 : 1;
INSERT INTO tbl (date, number, ...)
    VALUES
    ( CURDATE(), $number, ...);
COMMIT;

但是如果SELECT在午夜之前运行,那么仍然存在错误,但INSERT仅在之后运行。

因此,这可以避免错误,并避免事务,因为该语句应该是原子的:

INSERT INTO tbl (date, number, ...)
    VALUES
    ( CURDATE(),
      IFNULL( ( SELECT MAX(id) FROM tbl WHERE date = CURDATE() ),
                   1 ) AS number,
      ...  -- whatever else you are inserting into `tbl`
    );

无论你如何做,你都必须检查错误 - 另一个连接可能会做同样的事情并导致死锁等。