MySQL 事务:SELECT + INSERT

Wal*_*r47 3 php mysql transactions

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

我称这些为预留时间间隔槽。插槽存储在 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 ;  
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

现在您知道并发运行时可能会发生什么。我是交易和 mysql 的新手,但我试图对其进行测试,我有理由相信在这种情况下仅进行交易是不够的,但我不确定。

所以我的问题是:如何在一笔交易中正确选择、检查冲突和存储预订。

谢谢

Die*_*aro 5

你需要的是锁定。交易确实“不是严格需要的”。

您可以在“悲观锁定”和“乐观锁定”之间进行选择。关于这两种可能性中的哪一种取决于您的决定,并且必须基本上考虑以下因素进行评估:

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

我会建议阅读这两篇文章以建立对所涉及事物的概念:

一个更好解释的例子

这可能不是那么优雅,但只是一个示例,说明如何在没有事务(甚至没有 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);
Run Code Online (Sandbox Code Playgroud)

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

正如它所写的那样,它存在的问题是slot表中至少需要有一行才能工作(否则 SELECT 子句将始终返回一个空记录集,在这种情况下,如果没有冲突,则不会插入任何内容. 有两种可能使其实际工作:

  • 在表中插入一个虚拟行,可能是过去的日期
  • 重写,所以主 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);
    
    Run Code Online (Sandbox Code Playgroud)

这里遵循一系列说明,如果您只是简单地复制/粘贴,则会显示实际操作中的想法。我假设您将 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`;
Run Code Online (Sandbox Code Playgroud)

这显然是乐观锁的一个极端例子,但最终非常有效,因为所有这些都只用一条 SQL 指令完成,并且数据库服务器和 php 代码之间的交互(数据交换)很少。此外,实际上没有“真正的”锁定。

...或悲观锁定

相同的代码可以成为一个很好的悲观锁定实现,只需围绕显式表锁定/解锁指令:

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;
Run Code Online (Sandbox Code Playgroud)

当然,在这种情况下(悲观锁定),SELECT 和 INSERT 可以分开,并且在它们之间执行一些 php 代码。然而,这段代码的执行速度仍然非常快(没有与 php 的数据交换,没有中间的 php 代码),因此悲观锁的持续时间尽可能短。保持悲观锁尽可能短是避免应用程序变慢的关键点。

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

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

“乐观”认为这不会改变;-)