我开发了一个在线预订系统。为简化起见,假设用户可以预订多个项目,并且每个项目只能预订一次。商品首先添加到购物车中。
应用程序使用 MySql
/InnoDB
数据库。根据 MySql 文档,默认隔离级别为 Repeatable reads
。
这是到目前为止我想出的结帐程序:
- Begin transaction
- Select items in the shopping cart (with
for update
lock)
Records fromcart-item
anditems
tables are fetched at this step.- Check if items haven't been booked by anybody else
Basically check ifquantity > 0
. It's more complicated in the real application, thus I put it here as a separate step.- Update items, set
quantity = 0
Also perform other essential database manipulations.- Make payment (via external api like PayPal or Stripe)
No user interaction is necessary as payment details can be collected before checkout.- If everything went fine commit transaction or rollback otherwise
- Continue with non-essential logic
Send e-mail etc in case of success, redirect for error.
我不确定这是否足够。我担心是否:
T2
会等到 T1
完成吗? shared lock
? SELECT ... FOR UPDATE
表上做 items
就足够了。这样,双击引起的请求和其他用户都必须等到事务完成。他们会等待,因为他们也使用 FOR UPDATE
。同时,vanilla SELECT
只会在事务之前看到 db 的快照,但没有延迟,对吧? JOIN
中使用 SELECT ... FOR UPDATE
,两个表中的记录都会被锁定吗? 以下是我阅读过的一些资源:
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。
重写我原来的问题,使其更一般。
添加了后续问题。
最佳答案
- Begin transaction
- Select items in shopping cart (with for update lock)
到目前为止一切顺利,这至少可以防止用户在多个 session 中结帐(多次尝试结帐同一张卡 - 处理双击的好方法。)
- Check if items haven't been booked by other user
你怎么检查?使用标准
SELECT
还是使用 SELECT ... FOR UPDATE
?根据第 5 步,我猜您正在检查项目上的保留列或类似内容。这里的问题是步骤 2 中的
SELECT ... FOR UPDATE
不会将 FOR UPDATE
锁应用于其他所有内容。它仅适用于 SELECT
ed:cart-item
表。根据名称,对于每个购物车/用户,这将是不同的记录。这意味着其他交易不会被阻止进行。
- Make payment
- Update items marking them as reserved
- If everything went fine commit transaction, rollback otherwise
根据上述内容,根据您提供的信息,如果您没有在第 3 步中使用
SELECT ... FOR UPDATE
,您最终可能会有多人购买同一件商品。建议的解决方案
SELECT ... FOR UPDATE
cart-item
表。 这将锁定双击运行。您在此处选择的应该是某种“购物车订购”列。如果这样做,第二个事务将在此处暂停并等待第一个事务完成,然后读取第一个保存到数据库中的结果。
如果
cart-item
表显示它已被订购,请确保在此处结束结帐过程。SELECT ... FOR UPDATE
记录已预订项目的表格。 这将锁定其他购物车/用户无法阅读这些项目。
根据结果,如果物品没有保留,继续:
UPDATE ...
步骤 3 中的表,将项目标记为保留。执行您需要的任何其他 INSERT
s 和 UPDATE
s。 确保您没有在第 5 步和第 7 步之间做任何可能会失败的事情(例如发送电子邮件),否则您最终可能会导致他们在没有记录的情况下付款,以防交易被回滚。
第 3 步是确保两个(或更多)人不会尝试订购同一件商品的重要步骤。如果有两个人尝试,第二个人最终会在处理第一个时让他们的网页“挂起”。然后当第一个完成时,第二个将读取“保留”列,您可以向用户返回一条消息,表明有人已经购买了该项目。
交易付款与否
这是主观的。通常,您希望尽快关闭事务,以避免多人同时与数据库交互。
但是,在这种情况下,您实际上确实希望他们等待。只是时间长短的问题。
如果您选择在付款前提交交易,则需要在某个中间表中记录您的进度,运行付款,然后记录结果。请注意,如果付款失败,您必须手动撤消您更新的商品预订记录。
SELECT ... FOR UPDATE 对不存在的行
只是一个警告,如果您的表设计涉及在您需要更早的位置插入行
SELECT ... FOR UPDATE
:如果行不存在,则该事务不会导致其他事务等待,如果它们也 SELECT ... FOR UPDATE
相同的不存在行。因此,请确保始终通过在您知道首先存在的行上执行
SELECT ... FOR UPDATE
来序列化您的请求。然后您可以在可能存在或可能不存在的行上 SELECT ... FOR UPDATE
。 (不要尝试在可能存在或可能不存在的行上只执行 SELECT
,因为您将在事务开始时读取行的状态,而不是在您运行 SELECT
的那一刻。所以, SELECT ... FOR UPDATE
在不存在的行上仍然需要做一些事情才能获得最新的信息,请注意它不会导致其他事务等待。)
关于mysql - 如何正确使用事务和锁来保证数据库的完整性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40749730/