【问题标题】:MySQL Transaction: SELECT + INSERTMySQL 事务:SELECT + INSERT
【发布时间】:2014-08-15 22:32:06
【问题描述】:

我正在构建 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 ;  

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

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

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

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

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

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

谢谢

【问题讨论】:

  • 你试过FOR UPDATE选项到SELECT吗?这会锁定您读取的所有记录,并且使用该选项的其他事务将无法读取它们,直到您的事务完成。
  • 不是您问题的答案:startend(单独)上的唯一键显然是个坏主意。您可以预订同时开始或结束的不同设备。要么删除这些约束,要么将设备包含在密钥中(尽管这不足以避免重叠保留)。
  • @Barmar 不,我还没有尝试过,但它看起来链接了一个答案。您认为它会如何影响性能,因为我读它会锁定所有记录,因此其他请求将不得不等待。这是解决这个问题的唯一方法吗?
  • @jcaron 是的,我知道这还不够。正如我在问题中所说,我稍后会优化表结构,但谢谢我会记住你的笔记。
  • 我自己没用过,但是我理解的方式是它只锁定被选中的记录,而不是表中的所有记录。因此,选择不相关记录的不同查询将不必等待。但是选择相同或重叠记录的查询将等待,这正是您想要的。

标签: php mysql transactions


【解决方案1】:

您需要的是锁定。事务确实“不是严格需要的”。

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

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

我会推荐阅读这两个以建立对所涉及事物的概念:

一个更好解释的例子

这可能不是那么优雅,但只是一个示例,说明如何在没有事务的情况下完成所有操作(甚至没有 UNIQUE 约束)。 需要做的是使用以下组合的 INSERT + SELECT statemet 并在其执行后检查受影响的行数。 如果受影响的行数为 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 操作的情况下获得的 Optimistic Locking 的示例。

正如它所写的那样,它的问题是slot 表中必须至少有一行才能工作(否则 SELECT 子句将始终返回一个空记录集,在这种情况下不会插入任何内容即使没有碰撞。有两种可能让它真正工作:

  • 在表格中插入一个虚拟行,可能是过去的日期
  • rewrite 所以主 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 代码之间的交互(数据交换)很少。此外,实际上没有“真正的”锁定。

...或使用悲观锁定

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

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 他们说 “MySQL 不允许 INSERT ... SELECT 语句的并发插入” 所以它不应该需要悲观锁,但无论如何这可能是一个不错的选择,如果您认为这将在 MySQL 的未来版本中发生变化。

我是“乐观”这不会改变;-)

【讨论】:

  • 哇,这太棒了。我想这将在启用自动提交的情况下工作。但是如果我使用事务呢?两个线程将同时运行这些插入并提交。他们不会仍然选择相同的旧状态并插入可能发生冲突的插槽吗?还是我没明白? :) 我还阅读了有关隔离级别的信息,但即使已提交读取,我也不确定它是否会安全工作。谢谢
  • “并发”实际上并不存在。我的意思是当代码块不是原子的并且可以交错执行更多实例时,我们说“并发”。这里dev.mysql.com/doc/refman/5.0/en/insert-select.html 他们说“MySQL 不允许对 INSERT ... SELECT 语句进行并发插入”,因此它在数据库引擎级别是原子的。当然,它需要您检查代码是否成功或失败检查受影响记录的数量返回值,因为这就是 Optimistic Lock 的工作原理。
猜你喜欢
  • 1970-01-01
  • 2011-03-18
  • 1970-01-01
  • 2010-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-25
  • 2011-08-30
相关资源
最近更新 更多