假设我们在使用 MySQL的应用程序中有以下情况,其中用户可以购买商品,并且每个商品都有一个买家,但商品的价格可能会发生变化。
伪代码:
BEGIN TRANSACTION
seenPrice = SELECT price FROM Item
WHERE id = ABC AND buyer IS NULL
UPDATE Item SET buyer = X
WHERE id = ABC AND buyer IS NULL AND price = seenPrice
COMMIT
UPDATE 语句中的seenPrice
变量和buyer IS NULL AND price = seenPrice
检查用作乐观锁,以确保不会出现任何并发问题。
在多线程环境中,线程 A 和线程 B 同时通过SELECT
语句并说线程 A 先执行UPDATE
语句,然后并发性不是问题,但 A 和 B 是否有可能同时执行UPDATE
语句相同的确切时间?对于上下文,正在使用 Spring Boot 和 Spring Data JPA 开发应用程序。
这些语句可能会同时开始执行,但它们不能同时更新同一行。更新语句必须先获取行上的排他锁才能修改,而锁获取是严格序列化的原子操作,意味着其中一个并发会话总是先获取锁,其他会话会被阻塞.
锁获取始终是原子的,无论 DBMS 或存储引擎如何——如果不是,它作为并发控制机制将完全无用。
锁定机制并不关心您的应用程序是使用乐观还是悲观锁定方法编写的——改变的是锁定的时间。使用悲观锁定,您希望有很多并发活动,并且您希望确保您的事务成功(以防止并发更新为代价):
SELECT ... FOR UPDATE
UPDATE ...
COMMIT
使用乐观锁定,您不会期望太多并发活动,因此您的应用程序无法完成更新的可能性很低,并且您不会长时间保持锁定:
SELECT ...
UPDATE ...
COMMIT
不,A 和 B 不可能同时执行 UPDATE。一个或另一个将首先获得行上的锁,然后另一个将被阻塞,直到获胜者提交。
一旦获胜者提交,条件
buyer IS NULL
将不再为真,因此等待的线程的 UPDATE 不会影响它。我们可以做一个实验来测试一下:
在窗口 1 中:
在窗口 2 中,执行相同操作,开始交易并查看价格。
在窗口 1 中:
在窗口 2 中:
在窗口 1 中:
在窗口 2 中:
注意匹配或更改的零行!因为买方在该行的最新提交版本中不再为空。
由于您
buyer IS NULL
在两个语句中都进行了测试,因此处理将“起作用”。但是,不同的连接可能会潜入SELECT
andUPDATE
和 set之间buyer
。所以,你必须检查一下是否
UPDATE
真的成功了。如果没有,那又是什么?告诉用户“对不起,你浏览了所有的 UI 只是为了让别人偷偷溜进来买你想要的东西。” 啊。相反,
FOR UPDATE
在SELECT
. 这样,您可以更快地发现问题。并且不要编写挂在事务上超过几秒钟的代码。这可能会导致您的应用程序出现各种令人讨厌的崩溃。