MySQL事务:SELECT + INSERT

时间:2014-08-15 22:32:06

标签: php mysql transactions

我正在使用php和mysql构建Web应用程序 - 预订系统。系统将允许用户在某些设备上预约时间间隔(用户在该设备上工作的时间)。

我将这些保留的时间间隔称为时隙。插槽存储在mysql数据库表中,如下所示:

CREATE TABLE IF NOT EXISTS `slot` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`start` int(11) unsigned DEFAULT NULL,
`end` int(11) unsigned DEFAULT NULL,
`uid` int(11) unsigned DEFAULT NULL,
`group` int(11) unsigned DEFAULT NULL,
`message` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`devices_id` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `start_2` (`start`),
UNIQUE KEY `end_2` (`end`),
KEY `index_foreignkey_slot_devices` (`devices_id`),
KEY `start` (`start`),
KEY `end` (`end`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci AUTO_INCREMENT=6997 ;  

(这个表是由redbean orm自动创建的,我还没有优化它)

因此,当用户创建预订时,会在此表中插入新行。在专栏中 开始,结束我保留每个预约开始和结束的unix时间戳。

要记住的另一件事是应用程序允许不同的用户查看同一设备的不同时间表。例如:用户A有6分钟长的间隔,所以她可以看到空闲插槽(12:00 - 12:06)和免费插槽(12:06 - 12:12),但用户B有4个小间隔长的间隔,所以他还有其他人看到插槽(12:04 - 12:08)。每个用户或一组用户可以具有不同的间隔持续时间。因此,我必须确保当用户A和B都使用这些插槽发送请求时,其中只有一个成功。这让我想到了交易以及我的问题。

我是这样做的: - 开始交易 - 选择当天的所有插槽 - 运行算法,检查所选保留时隙和请求时隙之间的时间冲突 - 如果没有冲突,则在槽表中插入新行,否则向用户发出信号错误 - 提交

现在你知道它同时运行时会发生什么。我是交易和mysql的新手,但我试图测试它,我有理由相信在这种情况下只是在交易中是不够的,但我不确定。

所以我的问题是:如何在一次交易中正确选择,检查碰撞和存储预订。

感谢

1 个答案:

答案 0 :(得分:4)

您需要的是锁定。交易是"并非严格需要"确实

您可以选择"悲观锁定" "乐观锁定"。 关于这两种可能性中的哪一种取决于您并且必须基于以下方式进行评估的决定:

  • 您拥有的并发级别
  • 数据库中必须原子操作的持续时间
  • 整个操作的复杂性

我建议你阅读这两篇文章,以了解所涉及的事情:

更好地解释的例子

这可能不是那么优雅,但只是一个例子,展示了如何在没有事务的情况下完成所有操作(甚至没有UNIQUE约束)。 需要做的是使用以下组合的INSERT + SELECT状态,并在执行后检查受影响的行数。 如果受影响的行数是1,那么它已经成功了(如果它是0)则发生了冲突而另一方已经获胜。

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
FROM `slot`
WHERE NOT EXISTS (
    SELECT `id` FROM `slot`
    WHERE `start` <= @endTime AND `end` >= @startTime
    AND `devices_id` = @deviceId)
GROUP BY (1);

这是在没有事务和单个SQL操作的情况下获得的乐观锁定的示例。

正如它所写的,它有一个问题,即slot表中至少需要有一行才能工作(另外,SELECT子句总会返回一个空的记录集,在这种情况下没有如果没有碰撞,则插入evei。这有两种可能性使其真正起作用:

  • 在表格中插入一个虚拟行,可能包含过去的日期
  • 重写所以主FROM子句引用任何至少有一行或更好的表创建一个小表(可能名为dummy),只有一列,只有一个记录,并重写为以下(请注意,不再需要GROUP BY子句)

    INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
    SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
    FROM `dummy`
    WHERE NOT EXISTS (
        SELECT `id` FROM `slot`
        WHERE `start` <= @endTime AND `end` >= @startTime
        AND `devices_id` = @deviceId);
    

以下是一系列指令,如果您只是简单地复制/粘贴就会显示出这个想法。我假设您将int字段上的日期/时间编码为一个数字,其中连接了日期和时间的数字。

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
VALUES (1008141200, 1008141210, 11, 2, 'Dummy Record', 14)

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
SELECT 1408141206, 1408141210, 11, 2, 'Hello', 14
FROM `slot`
WHERE NOT EXISTS (
    SELECT `id` FROM `slot`
    WHERE `start` <= 1408141210 AND `end` >= 1408141206
    AND `devices_id` = 14)
GROUP BY (1);

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
SELECT 1408141208, 1408141214, 11, 2, 'Hello', 14
FROM `slot`
WHERE NOT EXISTS (
    SELECT `id` FROM `slot`
    WHERE `start` <= 1408141214 AND `end` >= 1408141208
    AND `devices_id` = 14)
GROUP BY (1);

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
SELECT 1408141216, 1408141220, 11, 2, 'Hello', 14
FROM `slot`
WHERE NOT EXISTS (
    SELECT `id` FROM `slot`
    WHERE `start` <= 1408141220 AND `end` >= 1408141216
    AND `devices_id` = 14)
GROUP BY (1);

SELECT * FROM `slot`;

这显然是乐观锁定的一个极端例子,但最终效率非常高,因为所有操作都只使用一条SQL指令,并且数据库服务器和php代码之间的交互较少(数据交换)。此外,实际上没有真正的&#34;锁定。

...或悲观锁定

相同的代码可以成为一个好的Pessimistc Locking实现,只需使用显式的表锁定/解锁指令:

LOCK TABLE slot WRITE, dummy READ;

INSERT INTO `slot` (`start`, `end`, `uid`, `group`, `message`, `devices_id`)
SELECT @startTime, @endTime, @uid, @group, @message, @deviceId
FROM `dummy`
WHERE NOT EXISTS (
    SELECT `id` FROM `slot`
    WHERE `start` <= @endTime AND `end` >= @startTime
    AND `devices_id` = @deviceId);

UNLOCK TABLES;

当然在这种情况下(悲观锁定),SELECT和INSERT可以分开并在其间执行一些php代码。但是这段代码执行起来非常快(没有与php进行数据交换,没有中间的PHP代码),因此悲观锁的持续时间最短。保持悲观锁定尽可能短是一个关键点,以避免减慢应用程序。

无论如何,您需要检查受影响的记录返回值的数量,以便知道它是否成功,因为代码实际上是相同的,因此您以相同的方式获得成功/失败信息。

这里http://dev.mysql.com/doc/refman/5.0/en/insert-select.html他们说&#34; MySQL不允许INSERT ... SELECT语句的并发插入&#34; 所以它不应该需要悲观锁但是无论如何如果你认为这将在未来版本的MySQL中发生变化,这可能是一个不错的选择。

&#34;乐观&#34; 这不会改变; - )