在通用SQL中同时检索(选择)或创建(插入)新行而不会发生冲突

时间:2011-03-08 00:57:55

标签: mysql sql oracle postgresql

我有一个系统,它有一个复杂的主键用于连接外部系统,还有一个快速,小的不透明主键供内部使用。例如:外部键可能是复合值 - 类似于(给定名称(varchar),系列名称(varchar),邮政编码(char)),内部键是整数(“客户ID”)。

当我收到外部密钥的传入请求时,我需要查找内部密钥 - 这里是棘手的部分 - 如果我还没有一个内部密钥给定的外部ID。

显然,如果我一次只有一个客户端与数据库通信,这很好。如果我找不到值,请SELECT customer_id FROM customers WHERE given_name = 'foo' AND ...,然后INSERT INTO customers VALUES (...)。但是,如果有可能同时从外部系统进入许多请求,并且许多可能同时到达前所未闻所未闻的客户,则存在竞争条件,其中多个客户端可能尝试INSERT新行。

如果我要修改现有的行,那很容易;在执行SELECT FOR UPDATE之前,首先只需UPDATE获取适当的行级锁定。但在这种情况下,我没有可以锁定的行,因为该行还不存在!

到目前为止,我已经提出了几个解决方案,但每个解决方案都有一些非常重要的问题:

  1. INSERT上捕获错误,从顶部重新尝试整个交易。如果交易涉及十几个客户,这是一个问题,特别是如果传入的数据可能每次都以不同的顺序讨论相同的客户。可能会陷入相互递归的死锁循环,每次冲突都发生在不同的客户身上。您可以通过重试尝试之间的指数等待时间来缓解此问题,但这是处理冲突的缓慢而昂贵的方法。此外,这使得应用程序代码变得非常复杂,因为所有内容都需要重新启动。
  2. 使用保存点。在SELECT之前启动保存点,捕获INSERT上的错误,然后再次回滚到保存点和SELECT。保存点不是完全可移植的,它们的语义和功能在数据库之间略有不同;我注意到的最大的区别是,有时它们似乎是窝,有时它们没有,所以如果我能避免它们会很好。这只是一个模糊的印象 - 它是不准确的吗?保存点是标准化的,还是至少实际上是一致的?此外,保存点使得很难在相同的事务上并行执行操作,因为您可能无法确切知道您将回滚多少工作,尽管我意识到我可能只需要与此同在。
  3. 使用LOCK语句(oracle mysql postgres)获取一些全局锁,例如表级锁。这显然会减慢这些操作并导致很多锁争用,所以我宁愿避免它。
  4. 获取更细粒度但特定于数据库的锁。我只熟悉Postgres's way of doing this,这在其他数据库中肯定不受支持(函数甚至以“pg_”开头),所以这又是一个可移植性问题。此外,postgres的这种方式需要我将键转换为一对整数,它可能不适合。是否有更好的方法来获取假设对象的锁?
  5. 在我看来,这必须是数据库的常见并发问题,但我没有设法找到它的大量资源;可能只是因为我不知道规范的措辞。是否可以在任何标记数据库中使用一些简单的额外语法来执行此操作?

3 个答案:

答案 0 :(得分:3)

我不清楚为什么你不能使用INSERT IGNORE,它会无误地运行,你可以检查是否发生了插入(修改过的记录)。如果插入“失败”,那么您知道密钥已经存在并且您可以执行SELECT。您可以先执行INSERT,然后执行SELECT。

或者,如果您使用的是MySQL,请使用支持事务的InnoDB。这样可以更容易回滚。

答案 1 :(得分:1)

在主要的多客户交易之前和之外,以自动提交模式执行每个客户的“查找或可能创建”操作。

答案 2 :(得分:0)

WRT生成一个不透明的主键,有许多选项,例如,使用guid或(至少使用Oracle)一个序列表。 WRT确保外部密钥是唯一的,对列应用唯一约束。如果插入因密钥存在而失败,请重新尝试获取。您可以使用不存在或不存在的插入。使用存储过程可以减少往返次数并提高性能。