如何正确使用事务和锁以确保数据库完整性?

时间:2016-11-22 19:03:41

标签: mysql database concurrency transactions locking

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

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

这是我到目前为止提出的结帐程序:

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

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

  1. 尝试同时预订同一项目的其他用户将被正确处理。他的交易T2会等到T1完成吗?
  2. 使用PayPal或Stripe付款可能需要一些时间。这不会成为性能方面的问题吗?
  3. 项目可用性将始终正确显示(项目应该可用,直到结帐成功)。这些只读选项是否应使用shared lock
  4. MySql可能会自行回滚事务吗?通常最好自动重试或显示错误消息并让用户再试一次吗?
  5. 如果我在SELECT ... FOR UPDATE表上items,我认为已经足够了。这样,双击和其他用户引起的请求都必须等到事务完成。他们会等待,因为他们也使用FOR UPDATE。与此同时,vanilla SELECT只会在事务发生前看到db的快照,但没有延迟,对吗?
  6. 如果我在JOIN中使用SELECT ... FOR UPDATE,两个表中的记录都会被锁定吗?
  7. 我对Willem Renzema的 SELECT ... FOR UPDATE 部分的回复感到有些困惑。什么时候变得重要?你能提供任何例子吗?
  8. 以下是我读过的一些资源: How to deal with concurrent updates in databases?MySQL: Transactions vs Locking TablesDo database transactions prevent race conditions?Isolation (database systems)InnoDB Locking and Transaction ModelA beginner’s guide to database locking and the lost update phenomena

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

2 个答案:

答案 0 :(得分:4)

  
      
  1. 开始交易
  2.   
  3. 选择购物车中的商品(使用更新锁定)
  4.   

到目前为止,这至少会阻止用户在多个会话中进行结账(多次尝试签出同一张卡 - 很好地处理双击。)

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

你如何检查?使用标准SELECTSELECT ... FOR UPDATE?基于第5步,我猜你正在检查项目上的保留列,或类似的东西。

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

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

根据您提供的信息,如果您未在步骤3中使用SELECT ... FOR UPDATE,则最终可能会有多人购买相同的商品。

建议的解决方案

  1. 开始交易
  2. SELECT ... FOR UPDATE cart-item表。
  3. 这将锁定从运行中双击。你在这里选择的应该是某种“购物车订购”栏目。如果这样做,第二个事务将在此处暂停并等待第一个事务完成,然后读取第一个保存到数据库的结果。

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

    1. SELECT ... FOR UPDATE您记录项目是否已被保留的表格。
    2. 这将锁定其他购物车/用户无法阅读这些商品。

      根据结果,如果未保留项目,请继续:

      1. UPDATE ...步骤3中的表格,将项目标记为已保留。您还需要其他INSERTUPDATE

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

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

      4. 提交交易

      5. 确保您不会在步骤5和7之间执行任何可能失败的操作(例如发送电子邮件),否则如果事务被回滚,您可能最终会在没有记录的情况下进行付款。< / p>

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

        交易中的付款与否

        这是主观的。通常,您希望尽快关闭事务,以避免多个人被锁定,无法立即与数据库交互。

        然而,在这种情况下,你确实希望他们等待。这只是一个问题。

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

        SELECT ...对不存在的行进行更新

        如果你的表设计涉及在早先需要SELECT ... FOR UPDATE之前插入行,请注意警告:如果一行不存在,那么该事务不会导致其他事务等待,如果它们也{ {1}}同一个不存在的行。

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

答案 1 :(得分:2)

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

是。当活动事务对记录保持FOR UPDATE锁定时,使用任何锁定(SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE)的其他事务中的语句将被暂停直到活动事务提交或&#34;锁定等待超时&#34;超过了。

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

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

第3。项目可用性将始终正确显示(项目应该可用,直到结帐成功)。这些只读选项是否应使用shared lock

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

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

<强> 4。 MySql可能会自行回滚事务吗?通常最好自动重试或显示错误消息并让用户再试一次?

有可能。当&#34;锁定等待超时&#34;时,可以回滚交易。超过或发生死锁时。在这种情况下,自动重试是个好主意 默认情况下,挂起的语句在50秒后失败。

<强> 5。如果我在SELECT ... FOR UPDATE表上items,我想它已经足够了。这样,双击和其他用户引起的请求都必须等到事务完成。他们会等待因为他们也使用FOR UPDATE。与此同时,vanilla SELECT只会在交易前看到db的快照,但没有延迟,对吧?

是的,SELECT ... FOR UPDATE表上的items就足够了 是的,这些选择等待,因为FOR UPDATE是一个独占锁 是的,简单的SELECT只会获取交易开始前的价值,这将立即发生。

<强> 6。如果我在JOIN中使用SELECT ... FOR UPDATE,那么两个表中的记录都会被锁定吗?

是的,SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETE会锁定所有阅读记录,因此无论我们JOIN是什么。请参阅MySql Docs

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

  

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