【问题标题】:How to properly use transactions and locks to ensure database integrity?如何正确使用事务和锁来保证数据库的完整性?
【发布时间】:2017-04-06 14:13:51
【问题描述】:

我开发了一个在线预订系统。为简化起见,假设用户可以预订多个项目,并且每个项目只能预订一次。商品首先添加到购物车中。

应用使用MySql / InnoDB 数据库。根据 MySql 文档,默认隔离级别为Repeatable reads

这是我目前想出的结帐程序:

  1. 开始交易
  2. 选择购物车中的商品(带有for update 锁定)
    在这一步从cart-itemitems 表中获取记录。
  3. 检查物品是否未被其他人预订
    基本上检查是否quantity > 0。在实际应用中比较复杂,所以我把它作为一个单独的步骤放在这里。
  4. 更新项目,设置quantity = 0
    还可以执行其他必要的数据库操作。
  5. 付款(通过 PayPal 或 Stripe 等外部 API)
    无需用户交互,因为可以在结帐前收集付款详细信息。
  6. 如果一切正常提交事务或回滚否则
  7. 继续不重要的逻辑
    成功时发送电子邮件等,错误时重定向。

我不确定这是否足够。我担心是否:

  1. 尝试同时预订相同项目的其他用户将得到正确处理。他的交易T2会等到T1完成吗?
  2. 使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?
  3. 项目可用性将始终正确显示(项目应该可用,直到结帐成功)。这些只读选择是否应该使用shared lock
  4. MySql 有可能自己回滚事务吗?通常是自动重试还是显示错误消息让用户再试一次更好?
  5. 我想如果我在items 表上执行SELECT ... FOR UPDATE 就足够了。这样,由双击引起的请求和其他用户都必须等到事务完成。他们会等待,因为他们也使用FOR UPDATE。与此同时,vanilla SELECT 只会在事务之前看到 db 的快照,但不会有任何延迟,对吧?
  6. 如果我在SELECT ... FOR UPDATE中使用JOIN,两个表中的记录会被锁定吗?
  7. 我对Willem Renzema 回答的SELECT ... FOR UPDATE on non-existent rows 部分有点困惑。什么时候会变得重要?你能举个例子吗?

以下是我读过的一些资源: How to deal with concurrent updates in databases?, MySQL: Transactions vs Locking Tables, Do database transactions prevent race conditions?, Isolation (database systems), InnoDB Locking and Transaction Model, A beginner’s guide to database locking and the lost update phenomena.

重写了我原来的问题,使其更笼统。
添加了后续问题。

【问题讨论】:

  • 重要问题:当您SELECT ... FOR UPDATE 时,您是在选择您知道已经存在的行,还是在您计划插入行的位置?
  • @WillemRenzema 我只选择通过cart-item 数据透视表已经存在的行

标签: mysql database concurrency transactions locking


【解决方案1】:
  1. 开始交易
  2. 选择购物车中的商品(带有更新锁)

到目前为止一切都很好,这至少可以防止用户在多个会话中进行结帐(多次尝试结帐同一张卡 - 很好处理双击。)

  1. 检查项目是否未被其他用户预订

你如何检查?使用标准SELECT 还是使用SELECT ... FOR UPDATE?根据第 5 步,我猜您正在检查项目上的保留列,或类似的内容。

这里的问题是第 2 步中的 SELECT ... FOR UPDATE 不会将 FOR UPDATE 锁应用于其他所有内容。它仅适用于SELECTed:cart-item 表。根据名称,这将是每个购物车/用户的不同记录。这意味着不会阻止其他交易继续进行。

  1. 付款
  2. 更新将其标记为保留的项目
  3. 如果一切顺利,提交事务,否则回滚

根据上述情况,根据您提供的信息,如果您在第 3 步中没有使用 SELECT ... FOR UPDATE,您最终可能会有多人购买同一商品。

建议的解决方案

  1. 开始交易
  2. SELECT ... FOR UPDATEcart-item 表。

这将锁定双击运行。您在此处选择的应该是某种“已订购的购物车”列。如果你这样做,第二个事务将在这里暂停并等待第一个事务完成,然后读取第一个保存到数据库的结果。

如果cart-item 表显示已订购,请务必在此处结束结帐流程。

  1. SELECT ... FOR UPDATE 如果项目已被保留,您记录的表。

这将锁定其他购物车/用户无法阅读这些项目。

根据结果,如果项目没有保留,继续:

  1. UPDATE ... 步骤 3 中的表,将项目标记为保留。也可以做任何其他你需要的INSERTs 和UPDATEs。

  2. 付款。如果付款服务说付款无效,则发出回滚。

  3. 记录付款,如果成功。

  4. 提交事务

确保您在第 5 步和第 7 步之间不要做任何可能失败的事情(例如发送电子邮件),否则您最终可能会在交易回滚的情况下让他们在没有记录的情况下进行付款。

第 3 步是确保两个(或更多)人不会尝试订购同一商品的重要步骤。如果两个人尝试,第二个人最终会在处理第一个时让他们的网页“挂起”。然后当第一个完成时,第二个将读取“已保留”列,您可以向用户返回一条消息,表明有人已经购买了该商品。

是否在交易中付款

这是主观的。通常,您希望尽快关闭事务,以避免多人同时与数据库进行交互。

但是,在这种情况下,您实际上确实希望他们等待。这只是多长时间的问题。

如果您选择在付款前提交交易,您需要在某个中间表中记录您的进度,运行付款,然后记录结果。请注意,如果付款失败,您将不得不手动撤消您更新的项目预订记录。

SELECT ... FOR UPDATE 对不存在的行

提醒一句,如果您的表设计涉及在您需要更早的位置插入行SELECT ... FOR UPDATE:如果行不存在,则该事务不会导致其他事务等待,如果它们也有SELECT ... FOR UPDATE相同的不存在的行。

因此,请确保始终通过在您知道首先存在的行上执行 SELECT ... FOR UPDATE 来序列化您的请求。然后您可以在可能存在或不存在的行上SELECT ... FOR UPDATE。 (不要试图只在可能存在或不存在的行上执行 SELECT,因为您将在事务开始时读取行的状态,而不是在运行 SELECT 时. 所以,在不存在的行上SELECT ... FOR UPDATE 仍然是您需要做的事情才能获得最新的信息,请注意它不会导致其他事务等待。)

【讨论】:

  • 感谢您提供如此详细的回答!你提出了一个很好的观点,我应该避免任何可能在付款和提交之间失败的代码。因此,我更改了第 4 步和第 5 步的顺序。我想我会将付款留在交易中,否则可能会发生User 2 的结帐被拒绝,但一段时间后“已取”项目重新出现,因为User 1从来没有完成付款。我已经更新了我的帖子以阐明步骤并添加了一些后续问题,请看一下!
【解决方案2】:

1.其他尝试同时预订相同项目的用户将被正确处理。他的交易T2会等到T1完成吗?

是的。虽然活动事务在记录上保持FOR UPDATE 锁定,但使用任何锁定(SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE)的其他事务中的语句将被挂起,直到活动事务提交或“锁定等待超时” " 已超过。

2。使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?

这不是问题,因为这正是必要的。结帐交易应按顺序执行,即。后结帐不应在前结帐之前开始。

3.项目可用性将始终正确显示(项目应在结帐成功之前可用)。这些只读选择是否应该使用shared lock

Repeatable reads 隔离级别确保事务所做的更改在该事务提交之前不可见。因此,项目可用性将正确显示。在实际付款之前,不会显示任何内容不可用。不需要锁。

SELECT ... LOCK IN SHARE MODE 会导致结帐事务等待完成。这可能会减慢结账速度而不会带来任何回报。

4. MySql 有可能自己回滚事务吗?通常是自动重试还是显示错误消息让用户重试更好?

这是可能的。超过“锁定等待超时”或发生死锁时,事务可能会回滚。在这种情况下,最好自动重试。
默认情况下,暂停语句在 50 秒后失败。

5.我想如果我在items 桌子上做SELECT ... FOR UPDATE 就足够了。这样,由双击引起的请求和其他用户都必须等到事务完成。他们会等待,因为他们也使用FOR UPDATE。与此同时,vanilla SELECT 只会在事务之前看到 db 的快照,但不会有任何延迟,对吧?

是的,items 表上的SELECT ... FOR UPDATE 应该足够了。
是的,这些选择等待,因为FOR UPDATE 是排他锁。
是的,简单的SELECT 只会像交易开始前一样获取价值,这会立即发生。

6.如果我在SELECT ... FOR UPDATE中使用JOIN,两个表中的记录会被锁定吗?

是的,SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE 锁定所有读取记录,所以无论我们 JOIN 什么都包括在内。见MySql Docs

有趣的是(至少对我而言)在 SQL 语句处理过程中扫描的所有内容都会被锁定,无论它是否被选中。例如WHERE id < 10 也会锁定记录id = 10

如果您没有适合您的语句的索引,并且 MySQL 必须扫描整个表来处理该语句,则表的每一行都会被锁定,这反过来又会阻止其他用户对表的所有插入。创建良好的索引很重要,这样您的查询就不会不必要地扫描许多行。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-09
    • 2017-04-18
    • 1970-01-01
    • 2012-05-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多