我开发了一个在线预订系统。为了简化,假设用户可以预订多个项目,每个项目只能预订一次。物品首先被添加到购物车。
应用程序使用MySql
/ InnoDB
数据库。根据MySql文档,默认隔离级别为Repeatable reads
。
这是我到目前为止提出的结账程序:
- 开始交易
- 选择购物车中的商品(使用
for update
锁) 来自cart-item
和items
表的记录在此步骤中获取。- 检查项目是否未被其他人预订 基本上检查
quantity > 0
。它在实际应用中更复杂,因此我把它作为一个单独的步骤。- 更新项目,设置
quantity = 0
还执行其他重要的数据库操作。- 付款(通过外部API,如PayPal或Stripe) 无需用户交互,因为可以在结账前收集付款细节。
- 如果一切顺利,则提交事务或回滚
- 继续使用非必要的逻辑 如果成功发送电子邮件等,重定向错误。
我不确定这是否足够。我担心是否:
T2
会等到T1
完成吗?shared lock
吗?SELECT ... FOR UPDATE
桌上做items
就够了。这样,双击和其他用户引起的请求都必须等到事务完成。他们会等,因为他们也使用FOR UPDATE
。与此同时,香草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。
重写了我原来的问题,使其更加通用。 添加了后续问题。
- 开始交易
- 选择购物车中的商品(用于更新锁定)
到目前为止,这至少可以防止用户在多个会话中进行结账(多次尝试签出同一张卡 - 很好地处理双击。)
- 检查其他用户是否未预订项目
你怎么检查?使用标准的SELECT
或SELECT ... FOR UPDATE
?基于第5步,我猜你正在检查项目上的保留列,或类似的东西。
这里的问题是步骤2中的SELECT ... FOR UPDATE
不会将FOR UPDATE
锁应用于其他所有内容。它只适用于什么是SELECT
ed:cart-item
表。根据名称,这将是每个购物车/用户的不同记录。这意味着其他交易不会被阻止继续进行。
- 付款
- 更新将其标记为已保留的项目
- 如果一切顺利提交事务,否则回滚
根据上述情况,根据您提供的信息,如果您未在第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
,因为您将在事务开始时读取行的状态,而不是在您运行SELECT
的那一刻。所以,SELECT ... FOR UPDATE
在不存在的行上仍然需要做些什么才能获得最新的信息,只要知道它不会导致其他事务等待。)
1.试图同时预订同一项目的其他用户将被正确处理。他的交易T2
会等到T1
完成吗?
是。当活动事务使FOR UPDATE
锁定记录时,其他使用任何锁定的事务中的语句(SELECT ... FOR UPDATE
,SELECT ... LOCK IN SHARE MODE
,UPDATE
,DELETE
)将被暂停,直到活动事务提交或超过“锁定等待超时”。
2.使用PayPal或Stripe付款可能需要一些时间。这不会成为性能方面的问题吗?
这不会是一个问题,因为这正是必要的。结帐交易应按顺序执行,即。后结账不应该在前完成之前开始。
3.项目可用性将始终正确显示(项目应该可用,直到结帐成功)。这些只读选择应该使用shared lock
吗?
Repeatable reads
隔离级别确保在提交事务之前,事务所做的更改不可见。因此,项目可用性将正确显示。在实际支付之前,什么都不会显示。不需要锁。
SELECT ... LOCK IN SHARE MODE
会导致结账交易等到结束。这可能会减慢结账速度而不给予任何回报。
4. MySql可能自行回滚事务吗?通常更好的方法是自动重试或显示错误消息并让用户再试一次?
有可能的。当超过“锁定等待超时”或发生死锁时,可以回滚事务。在这种情况下,自动重试它是个好主意。 默认情况下,挂起的语句在50秒后失败。
如果我在SELECT ... FOR UPDATE
桌上做items
,我想它已经足够了。这样,双击和其他用户引起的请求都必须等到事务完成。他们会等,因为他们也使用FOR UPDATE
。与此同时,香草SELECT
将在交易前看到db的快照,但没有延迟,对吗?
是的,在SELECT ... FOR UPDATE
桌上的items
应该足够了。
是的,这些选择等待,因为FOR UPDATE
是一个独占锁。
是的,简单的SELECT
只会抓住交易开始前的价值,这将立即发生。
6.如果我在JOIN
中使用SELECT ... FOR UPDATE
,两个表中的记录是否会被锁定?
是的,SELECT ... FOR UPDATE
,SELECT ... LOCK IN SHARE MODE
,UPDATE
,DELETE
锁定所有读取记录,所以无论我们JOIN
包括在内。见MySql Docs。
有趣的是(至少对我来说)在SQL语句处理过程中扫描的所有内容都会被锁定,无论它是否被选中。例如,WHERE id < 10
将用id = 10
锁定记录!
如果没有适合您的语句的索引,并且MySQL必须扫描整个表来处理该语句,则表的每一行都会被锁定,这反过来会阻止其他用户对表的所有插入。创建好的索引非常重要,这样您的查询就不会不必要地扫描很多行。