【问题标题】:Insert with Subselect - Atomic Operation?插入子选择 - 原子操作?
【发布时间】:2014-12-30 13:49:48
【问题描述】:

我知道,mysql 支持自动增量值,但不支持 依赖 自动增量值。

即如果你有这样的表:

id | element | innerId
1  | a       | 1
2  | a       | 2
3  | b       | 1

而你插入另一个b-element,你需要计算你自己的innerId,(预计插入为“2”)

  • 是否有数据库支持这样的功能?

实现这种行为的最佳方法是什么?我不知道元素的数量,所以我无法为它们创建 dedicated 表,我只能在其中导出一个 id。

(例子很简单)

应该实现的目标是任何元素“类型”(如果数字未知,possibly infitine -1 应该有它自己的、无间隙的 id。

如果我会使用类似的东西

INSERT INTO 
  myTable t1 
   (id,element, innerId)
   VALUES
   (null, 'b', (SELECT COUNT(*) FROM myTable t2 WHERE t2.element = "b") +1)

http://sqlfiddle.com/#!2/2f4543/1

这会在所有情况下返回预期结果吗?我的意思是它可以工作,但是并发呢?带有 SubSelects 的插入仍然是 atomic 还是可能存在一个场景,两个插入将尝试插入相同的 id? (尤其是在事务插入待处理的情况下?)

尝试使用编程语言(即 Java)实现这一点会更好吗?还是更容易在尽可能接近数据库引擎的地方实现这个逻辑?

由于我使用聚合来计算next innerId,我认为使用SELECT...FOR UPDATE 无法避免其他事务有待提交的问题,对吧?

ps.:我可以。只是 bruteforce 插入 - 从每个元素的当前最大值开始 - 在 (element,innerId) 上具有唯一键约束,直到没有foreignKey-violation - 但没有一个 更好的 em> 方法?

根据Make one ID with auto_increment depending on another ID - possible?,可以使用复合主键 - 在我的情况下 - innerId and element。但根据这个仅适用于 MyIsam 的setting MySQL auto_increment to be dependent on two other primary keys(我有 InnoDB)


现在我更加困惑了。我尝试使用 2 个不同的 php 脚本插入数据,使用上面的查询。虽然脚本一“休眠”了 15 秒,以允许我调用脚本二(它应该模拟并发修改) - 结果是正确的使用一个查询时

(ps.:mysql(?!i)-仅用于快速调试的功能)

基础数据:

脚本 1:

mysql_query("START TRANSACTION");
mysql_query("INSERT INTO insertTest (id, element, innerId, fromPage)VALUES(null, 'a', (SELECT MAX(t2.innerID) FROM insertTest t2 WHERE element='a') +1, 'page1')");

sleep(15);

//mysql_query("ROLLBACK;");
mysql_query("COMMIT;");

脚本 2:

//mysql_query("START TRANSACTION");
mysql_query("INSERT INTO insertTest (id, element, innerId, fromPage)VALUES(null, 'a', (SELECT MAX(t2.innerID) FROM insertTest t2 WHERE element='a') +1, 'page2')");
//mysql_query("COMMIT;");

我原以为page2 插入会在page1 插入之前发生,因为它在没有任何事务的情况下运行。但实际上page1的插入是FIRST发生的,导致第二个脚本也延迟了15秒左右……

(忽略AC-Id,玩了一下)

在第一个脚本上使用Rollback 时,第二个脚本仍然延迟 15 秒,然后然后选择正确的innerId

所以

  • 事务处于活动状态时,非事务性插入被阻止。
  • 带有子选择的插入似乎也被阻止了。
  • 所以最后看起来带有子选择的插入是原子操作?不然为什么第二页的SELECT被屏蔽了?

像这样在单独的非事务性语句中使用选择和插入(第 2 页,模拟并发修改):

$nextId = mysql_query("SELECT MAX(t2.innerID) as a FROM insertTest t2 WHERE element='a'");
$nextId = mysql_fetch_array($nextId);
$nextId = $nextId["a"] +1;
mysql_query("INSERT INTO insertTest (id, element, innerId, fromPage)VALUES(null, 'a', $nextId, 'page2')");

导致我试图避免的错误:

那么当每次修改都是一个查询时,为什么它在并发场景中起作用? 带有子选择的插入是原子的吗?

【问题讨论】:

    标签: java mysql transactions atomic


    【解决方案1】:

    从我在这里读到的内容:Atomicity multiple MySQL subqueries in an INSERT/UPDATE query? 您的查询似乎是原子的。我已经用 InnoDB 在我的 MySQL 上对其进行了测试,其中有 4 个不同的程序尝试执行查询 100000 次。之后,我能够在 (element,innerid) 上创建一个组合的唯一键并且它运行良好,所以它似乎没有生成重复。但是我有:

    尝试获取锁时发现死锁

    所以你可能要考虑这个http://dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html

    编辑:看来我可以通过将 SQL 更改为来规避死锁

    INSERT INTO 测试 (id,element,innerId) 值(空,“b”, (SELECT Count(*) FROM test t2 WHERE element = 'b' FOR UPDATE )+1);

    【讨论】:

    • 请看看我的更新。我也玩过并发插入,它似乎像带有子选择的插入是原子的。 - 至少我对这种行为没有任何解释。
    • 我认为从文档和观察中我们可以很确定它是原子的。只有在表中应用读锁时,两个单独语句的情况才有效。
    • 但是,如果嵌套选择的 delay 仅仅是因为一个 - 让我们称之为 bug - 导致外部查询以INSERT - 如果并发插入正好在 INSERT(Sub-)SELECT i> 的“声明 1”? (我不能通过调用两个脚本来模拟这个,因为它会是几毫秒的窗口
    • 我想如果你想 100% 确定没有办法绕过读锁。
    • 不要误会我的意思,我很欣赏您基于观察得出的结论 - 但由于我是发自内心的开发人员,我不会采用 99.99999% 的解决方案 :-)
    【解决方案2】:

    嗯,所有(或几乎)所有数据库都支持根据您的规则计算innerid 的必要功能。它被称为触发器,特别是插入前触发器。

    您的特定版本无法在多用户环境中始终如一地工作。很少(如果有的话)数据库在开始插入时在表上生成 read 锁。这意味着两个非常接近发出的插入语句将为innerid 生成相同的值。

    由于并发性的考虑,您应该在数据库中使用触发器而不是在应用程序端进行此计算。

    您始终可以在需要时计算innerid,而不是在插入值时。这在计算上很昂贵,需要order by(使用变量)或相关子查询。其他数据库支持窗口/分析函数,使这样的计算更容易表达。

    【讨论】:

    • 感谢您的回答。是的,innerID 可以计算出来,基本上是某种类型元素中的行号。问题是,(我讨厌这句话)客户希望让它适用于复合模式布局中的大量元素(n = 5 的限制)。新数据的预期每天大约有数千个插入,因此存储生成的sub-sub-sub-id 会更好——性能方面,当你必须找到元素 125.5685.125.2145.3369 时——因为我可以从中选择 innerId 3369第 5 个 innerID 列(分别为其他列)
    猜你喜欢
    • 2021-08-04
    • 1970-01-01
    • 2014-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多