如何正确使用事务和锁来保证数据库的完整性?
How to properly use transactions and locks to ensure database integrity?
我开发了一个在线预订系统。为简化起见,假设用户可以预订多个项目,每个项目只能预订一次。商品首先添加到购物车。
应用程序使用 MySql
/ InnoDB
数据库。根据 MySql 文档,默认隔离级别为 Repeatable reads
.
这是我到目前为止想出的结帐程序:
- Begin transaction
- Select items in the shopping cart (with
for update
lock)
Records from cart-item
and items
tables are fetched at this step.
- Check if items haven't been booked by anybody else
Basically check if quantity > 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
完成吗?
- 使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?
- 物品可用性将始终正确显示(物品在结帐成功之前应该可用)。这些只读选择是否应该使用
shared lock
?
- 有没有可能MySql自己回滚事务?通常是自动重试还是显示错误消息让用户重试更好?
- 我想如果我在
items
table 上做 SELECT ... FOR UPDATE
就足够了。这样,双击引起的请求和其他用户都必须等到交易完成。他们会等待,因为他们也使用 FOR UPDATE
。同时,vanilla SELECT
只会在交易前看到数据库的快照,没有延迟,对吧?
- 如果我在
SELECT ... FOR UPDATE
中使用JOIN
,两个table中的记录会被锁定吗?
- 我对 SELECT 有点困惑...更新 Willem Renzema 回答中不存在的行 部分。什么时候可能变得重要?你能举个例子吗?
以下是我阅读过的一些资源:
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)
到目前为止一切顺利,这至少可以防止用户在多个会话中进行结帐(多次尝试结帐同一张卡 - 可以很好地处理双击。)
- 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
table。根据名称,每个 cart/user 都有不同的记录。这意味着其他交易不会被阻止继续进行。
- 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
table.
这将锁定来自 运行ning 的双击。你这里 select 应该是某种 "cart ordered" 列。如果这样做,第二个事务将在这里暂停并等待第一个事务完成,然后读取第一个事务保存到数据库的结果。
如果 cart-item
table 表示已经订购,请务必在此处结束结帐流程。
SELECT ... FOR UPDATE
table 您记录项目是否已被预订的地方。
这将阻止其他 carts/users 读取这些项目。
根据结果,如果项目没有保留,继续:
UPDATE ...
步骤 3 中的 table,将项目标记为保留。也执行您需要的任何其他 INSERT
s 和 UPDATE
s。
付款。如果支付服务显示支付无效,请发出回滚。
付款成功后记录。
提交交易
确保您在第 5 步和第 7 步之间没有做任何可能会失败的事情(例如发送电子邮件),否则您可能最终会在交易被回滚的情况下让他们在没有记录的情况下进行付款.
第 3 步是确保两个(或更多)人不会尝试订购同一商品的重要步骤。如果两个人尝试,第二个人将在处理第一个人时最终拥有他们的网页 "hang"。然后当第一个完成时,第二个将读取 "reserved" 列,并且您可以 return 向用户发送一条消息,表明有人已经购买了该商品。
是否交易支付
这是主观的。通常,您希望尽快关闭事务,以避免多人被同时与数据库交互。
但是,在这种情况下,您实际上确实希望他们等待。只是时间长短的问题。
如果您选择在付款前提交交易,您需要在table、运行付款中间记录您的进度,然后记录结果。请注意,如果付款失败,您将不得不手动撤销您更新的商品预订记录。
SELECT ...更新不存在的行
请注意,如果您的 table 设计涉及在您需要更早的地方插入行 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
)的语句将被暂停直到活动事务提交或超过 "Lock wait timeout"。
2。使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?
这不会有问题,因为这正是必要的。结帐交易应按顺序执行,即。后一次结帐不应在前一次结帐之前开始。
3。项目可用性将始终正确显示(项目应在结帐成功之前可用)。这些只读选择应该使用 shared lock
?
Repeatable reads
隔离级别确保事务所做的更改在事务提交之前不可见。因此,项目可用性将正确显示。在实际付款之前,不会显示任何内容不可用。不需要锁。
SELECT ... LOCK IN SHARE MODE
会导致结账交易等待完成。这可能会减慢结帐速度而不会带来任何回报。
4. MySql 有没有可能自己回滚事务?通常是自动重试还是显示错误消息让用户重试更好?
这是可能的。当超过 "Lock wait timeout" 或发生死锁时,事务可能会回滚。在这种情况下,最好自动重试。
默认情况下,挂起的语句会在 50 秒后失败。
5.我想如果我在 items
table 上做 SELECT ... FOR UPDATE
就足够了。这样,双击引起的请求和其他用户都必须等到交易完成。他们会等待,因为他们也使用 FOR UPDATE
。同时,vanilla SELECT
只会在交易前看到数据库的快照,没有延迟,对吧?
是的,items
table 上的 SELECT ... FOR UPDATE
应该足够了。
是的,这些select等待,因为FOR UPDATE
是独占锁。
是的,简单 SELECT
将像交易开始前一样获取价值,这将立即发生。
6.如果我在SELECT ... FOR UPDATE
中使用JOIN
,两个table中的记录会被锁定吗?
是的,SELECT ... FOR UPDATE
、SELECT ... LOCK IN SHARE MODE
、UPDATE
、DELETE
锁定所有读取的记录,所以我们JOIN
包含的内容。参见 MySql Docs。
有趣的是(至少对我而言)在 SQL 语句的处理过程中扫描的所有内容都会被锁定,无论它是否被选中。例如 WHERE id < 10
也会锁定 id = 10
!
的记录
If you have no indexes suitable for your statement and MySQL must scan the entire table to process the statement, every row of the table becomes locked, which in turn blocks all inserts by other users to the table. It is important to create good indexes so that your queries do not unnecessarily scan many rows.
我开发了一个在线预订系统。为简化起见,假设用户可以预订多个项目,每个项目只能预订一次。商品首先添加到购物车。
应用程序使用 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
完成吗? - 使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?
- 物品可用性将始终正确显示(物品在结帐成功之前应该可用)。这些只读选择是否应该使用
shared lock
? - 有没有可能MySql自己回滚事务?通常是自动重试还是显示错误消息让用户重试更好?
- 我想如果我在
items
table 上做SELECT ... FOR UPDATE
就足够了。这样,双击引起的请求和其他用户都必须等到交易完成。他们会等待,因为他们也使用FOR UPDATE
。同时,vanillaSELECT
只会在交易前看到数据库的快照,没有延迟,对吧? - 如果我在
SELECT ... FOR UPDATE
中使用JOIN
,两个table中的记录会被锁定吗? - 我对 SELECT 有点困惑...更新 Willem Renzema 回答中不存在的行 部分。什么时候可能变得重要?你能举个例子吗?
以下是我阅读过的一些资源: 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)
到目前为止一切顺利,这至少可以防止用户在多个会话中进行结帐(多次尝试结帐同一张卡 - 可以很好地处理双击。)
- 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
table。根据名称,每个 cart/user 都有不同的记录。这意味着其他交易不会被阻止继续进行。
- 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
table.
这将锁定来自 运行ning 的双击。你这里 select 应该是某种 "cart ordered" 列。如果这样做,第二个事务将在这里暂停并等待第一个事务完成,然后读取第一个事务保存到数据库的结果。
如果 cart-item
table 表示已经订购,请务必在此处结束结帐流程。
SELECT ... FOR UPDATE
table 您记录项目是否已被预订的地方。
这将阻止其他 carts/users 读取这些项目。
根据结果,如果项目没有保留,继续:
UPDATE ...
步骤 3 中的 table,将项目标记为保留。也执行您需要的任何其他INSERT
s 和UPDATE
s。付款。如果支付服务显示支付无效,请发出回滚。
付款成功后记录。
提交交易
确保您在第 5 步和第 7 步之间没有做任何可能会失败的事情(例如发送电子邮件),否则您可能最终会在交易被回滚的情况下让他们在没有记录的情况下进行付款.
第 3 步是确保两个(或更多)人不会尝试订购同一商品的重要步骤。如果两个人尝试,第二个人将在处理第一个人时最终拥有他们的网页 "hang"。然后当第一个完成时,第二个将读取 "reserved" 列,并且您可以 return 向用户发送一条消息,表明有人已经购买了该商品。
是否交易支付
这是主观的。通常,您希望尽快关闭事务,以避免多人被同时与数据库交互。
但是,在这种情况下,您实际上确实希望他们等待。只是时间长短的问题。
如果您选择在付款前提交交易,您需要在table、运行付款中间记录您的进度,然后记录结果。请注意,如果付款失败,您将不得不手动撤销您更新的商品预订记录。
SELECT ...更新不存在的行
请注意,如果您的 table 设计涉及在您需要更早的地方插入行 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
)的语句将被暂停直到活动事务提交或超过 "Lock wait timeout"。
2。使用 PayPal 或 Stripe 付款可能需要一些时间。这不会成为性能方面的问题吗?
这不会有问题,因为这正是必要的。结帐交易应按顺序执行,即。后一次结帐不应在前一次结帐之前开始。
3。项目可用性将始终正确显示(项目应在结帐成功之前可用)。这些只读选择应该使用 shared lock
?
Repeatable reads
隔离级别确保事务所做的更改在事务提交之前不可见。因此,项目可用性将正确显示。在实际付款之前,不会显示任何内容不可用。不需要锁。
SELECT ... LOCK IN SHARE MODE
会导致结账交易等待完成。这可能会减慢结帐速度而不会带来任何回报。
4. MySql 有没有可能自己回滚事务?通常是自动重试还是显示错误消息让用户重试更好?
这是可能的。当超过 "Lock wait timeout" 或发生死锁时,事务可能会回滚。在这种情况下,最好自动重试。
默认情况下,挂起的语句会在 50 秒后失败。
5.我想如果我在 items
table 上做 SELECT ... FOR UPDATE
就足够了。这样,双击引起的请求和其他用户都必须等到交易完成。他们会等待,因为他们也使用 FOR UPDATE
。同时,vanilla SELECT
只会在交易前看到数据库的快照,没有延迟,对吧?
是的,items
table 上的 SELECT ... FOR UPDATE
应该足够了。
是的,这些select等待,因为FOR UPDATE
是独占锁。
是的,简单 SELECT
将像交易开始前一样获取价值,这将立即发生。
6.如果我在SELECT ... FOR UPDATE
中使用JOIN
,两个table中的记录会被锁定吗?
是的,SELECT ... FOR UPDATE
、SELECT ... LOCK IN SHARE MODE
、UPDATE
、DELETE
锁定所有读取的记录,所以我们JOIN
包含的内容。参见 MySql Docs。
有趣的是(至少对我而言)在 SQL 语句的处理过程中扫描的所有内容都会被锁定,无论它是否被选中。例如 WHERE id < 10
也会锁定 id = 10
!
If you have no indexes suitable for your statement and MySQL must scan the entire table to process the statement, every row of the table becomes locked, which in turn blocks all inserts by other users to the table. It is important to create good indexes so that your queries do not unnecessarily scan many rows.