InnoDB 行锁定 - 如何实现

Wiz*_*ard 15 mysql innodb locking

我一直在环顾四周,阅读 mysql 站点,但仍然无法确切了解它是如何工作的。

我想选择并行锁定写入结果,写入更改并释放锁定。audocommit 已开启。

方案

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 
Run Code Online (Sandbox Code Playgroud)

选择状态为待处理的项目,并将其更新为工作状态。使用独占写入来确保同一项目不会被捡起两次。

所以;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"
Run Code Online (Sandbox Code Playgroud)

从结果中获取 id

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>
Run Code Online (Sandbox Code Playgroud)

我是否需要做任何事情来释放锁,它是否像我上面所做的那样工作?

Aar*_*own 28

你想要的是SELECT ... FOR UPDATE从事务的上下文中。SELECT FOR UPDATE 在选定的行上放置一个排他锁,就像您正在执行 UPDATE 一样。它也隐式运行在 READ COMMITTED 隔离级别,而不管隔离级别显式设置为什么。请注意 SELECT ... FOR UPDATE 对并发性非常不利,只应在绝对必要时使用。当人们剪切和粘贴时,它也有在代码库中成倍增加的趋势。

这是来自 Sakila 数据库的示例会话,它演示了 FOR UPDATE 查询的一些行为。

首先,为了让我们非常清楚,将事务隔离级别设置为 REPEATABLE READ。这通常是不必要的,因为它是 InnoDB 的默认隔离级别:

session1> SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
session1> BEGIN;
session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)    
Run Code Online (Sandbox Code Playgroud)

在另一个会话中,更新此行。琳达结婚并改名:

session2> UPDATE customer SET last_name = 'BROWN' WHERE customer_id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
Run Code Online (Sandbox Code Playgroud)

回到 session1,因为我们在 REPEATABLE READ,Linda 仍然是 LINDA WILLIAMS:

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | WILLIAMS  |
+------------+-----------+
1 row in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

但是现在,我们想要独占访问该行,因此我们对该行调用 FOR UPDATE。请注意,我们现在获得了该行的最新版本,该版本是在此事务之外的 session2 中更新的。这不是可重复读,而是读已提交

session1> SELECT first_name, last_name FROM customer WHERE customer_id = 3 FOR UPDATE;
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| LINDA      | BROWN     |
+------------+-----------+
1 row in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

让我们测试一下 session1 中设置的锁。请注意 session2 无法更新该行。

session2> UPDATE customer SET last_name = 'SMITH' WHERE customer_id = 3;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
Run Code Online (Sandbox Code Playgroud)

但我们仍然可以从中选择

session2> SELECT c.customer_id, c.first_name, c.last_name, a.address_id, a.address FROM customer c JOIN address a USING (address_id) WHERE c.customer_id = 3;
+-------------+------------+-----------+------------+-------------------+
| customer_id | first_name | last_name | address_id | address           |
+-------------+------------+-----------+------------+-------------------+
|           3 | LINDA      | BROWN     |          7 | 692 Joliet Street |
+-------------+------------+-----------+------------+-------------------+
1 row in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

我们仍然可以使用外键关系更新子表

session2> UPDATE address SET address = '5 Main Street' WHERE address_id = 7;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1> COMMIT;
Run Code Online (Sandbox Code Playgroud)

另一个副作用是你大大增加了导致死锁的可能性。

在您的特定情况下,您可能想要:

BEGIN;
SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;
-- do some other stuff
UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

如果“做一些其他的事情”是不必要的,并且您实际上不需要保留有关该行的信息,那么 SELECT FOR UPDATE 是不必要且浪费的,您可以改为只运行更新:

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;
Run Code Online (Sandbox Code Playgroud)

希望这是有道理的。

  • 谢谢。当两个线程以“SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR UPDATE;”进入时,它似乎并没有解决我的问题。并且他们都看到同一行,然后一个将锁定另一个。我希望它能够以某种方式绕过锁定的行并转到下一个待处理的项目。 (3认同)