InnoDB行锁定-如何实现


13

我一直在四处逛逛,阅读mysql站点,但仍然看不到它是如何工作的。

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

方案

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime) 

选择状态为待定的项目,然后将其更新为工作状态。使用排他性写入来确保同一物品不会被拾取两次。

所以;

"SELECT id FROM `items` WHERE `status`='pending' LIMIT 1 FOR WRITE"

从结果中获取ID

"UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `id`=<selected id>

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

Answers:


26

您想要的是在事务上下文中进行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)    

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

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

回到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)

但是现在,我们希望对该行具有独占访问权限,因此我们在该行上调用FOR UPDATE。注意,现在我们获得了该行的最新版本,该版本在此事务之外的session2中进行了更新。那不是REPEATABLE READ,那是READ COMMITTED

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)

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

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

但是我们仍然可以从中选择

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)

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

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;

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

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

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;

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

UPDATE `items` SET `status`='working', `updated`=NOW() WHERE `status`='pending' LIMIT 1;

希望这有道理。


3
谢谢。当两个线程以“ SELECT ID FROM itemsWHERE status='pending'LIMIT 1 FOR UPDATE;” 进入两个线程时,似乎无法解决我的问题。并且他们都看到同一行,那么一个将锁定另一行。我希望不知何故,将能够绕过锁定的行和GOTO这是悬而未决..下一个项目
WIZZARD

1
数据库的本质是它们返回一致的数据。如果在值更新之前执行两次该查询,您将获得相同的结果。我知道,没有“除非行被锁定,否则请给我第一个与该查询匹配的值” SQL扩展。这听起来令人怀疑,就像您在关系数据库之上实现队列一样。是这样吗
亚伦·布朗

亚伦; 是的,这就是我想要做的。我一直在考虑使用Gearman之类的工具-但这真是个破门。您还有其他想法吗?
Wizzard '03

我认为您应该阅读以下内容:engineyard.com/blog/2011/…-对于消息队列,根据您选择的客户端语言,有很多队列。ActiveMQ,Resque(Ruby + Redis),ZeroMQ,RabbitMQ等
亚伦·布朗

如何使会话2在读取之前阻塞,直到提交会话1中的更新?
CMCDragonkai,2015年

2

如果您使用的是InnoDB存储引擎,则它将使用行级锁定。结合多版本控制,可以实现良好的查询并发性,因为给定的表可以由不同的客户端同时读取和修改。行级并发属性如下:

不同的客户端可以同时读取相同的行。

不同的客户端可以同时修改不同的行。

不同的客户端不能同时修改同一行。如果一个事务修改了一行,则其他事务将无法修改同一行,直到第一个事务完成。除非其他事务使用READ UNCOMMITTED隔离级别,否则它们也不能读取已修改的行。也就是说,他们将看到原始的未修改的行。

基本上,您不必指定显式锁定InnoDB会处理iteslf,尽管在某些情况下,您可能必须提供有关显式锁定的显式锁定详细信息,如下所示:

以下列表描述了可用的锁类型及其作用:

锁定表格以供阅读。READ锁为读取查询(例如SELECT)锁定表,以从表中检索数据。它不允许写操作(例如INSERT,DELETE或UPDATE)修改表,即使是持有锁的客户端也是如此。当一个表被锁定以进行读取时,其他客户端可以同时从该表读取,但是没有客户端可以写入该表。想要写入已读取锁定的表的客户端必须等待,直到当前从该表读取的所有客户端都已完成并释放了锁。

锁定要写入的表。WRITE锁是排他锁。仅当不使用表时才可以获取它。一旦获取,只有持有写锁的客户端才能从表中读取或写入表。其他客户端无法读取或写入。没有其他客户端可以锁定表以进行读取或写入。

读取本地

锁定表以进行读取,但允许并发插入。并发插入是“读者块作家”原则的例外。它仅适用于MyISAM表。如果MyISAM表的中间没有由删除或更新的记录引起的空缺,则插入总是在表的末尾进行。在这种情况下,正在从表中读取的客户端可以使用READ LOCAL锁将其锁定,以允许其他客户端在具有读取锁的客户端从表中读取时将其插入到表中。如果MyISAM表上确实有孔,则可以使用OPTIMIZE TABLE对表进行碎片整理来删除它们。


感谢您的回答。当我有这张表和100个客户检查未决项目时,我遇到了很多冲突-2-3个客户得到了相同的未决行。表锁定很慢。
Wizzard

0

另一种选择是添加一个存储上次成功锁定时间的列,然后其他任何想要锁定该行的时间都需要等待,直到它被清除或5分钟(或任何其他时间)过去。

就像是...

Schema

id (int)
name (varchar50)
status (enum 'pending', 'working', 'complete')
created (datetime)
updated (datetime)
lastlock (int)

lastlock是一个int,因为它存储unix时间戳,以便比较(也许更快)。

//抱歉,我没有检查它们是否正常运行,但是如果没有,它们应该足够接近。

UPDATE items 
  SET lastlock = UNIX_TIMESTAMP() 
WHERE 
  lastlock = 0
  OR (UNIX_TIMESTAMP() - lastlock) > 360;

然后检查以查看更新了多少行,因为不能同时通过两个进程来更新行,如果您更新了该行,您将获得锁。假设您使用的是PHP,则将使用mysql_affected_rows(),如果返回的值为1,则可以成功锁定它。

然后,您可以在完成所需的操作后将lastlock更新为0,或者可以懒惰并等待5分钟,直到下一次尝试锁定成功为止。

编辑:您可能需要做一些工作来检查它在夏季时间更改前后是否按预期工作,因为时钟会回到一个小时,也许会使检查无效。您需要确保Unix时间戳采用UTC格式-无论如何它们都可能是UTC格式的。


-1

或者,您可以对记录字段进行分段以允许并行写入并绕过行锁定(分段的JSON对样式)。因此,如果复合读取记录的一个字段是整数/实数,则可能会有该字段的1-8片段(8个写入记录/行有效)。然后,在每次写入到单独的读取查找后,将片段循环加总。这样最多可以并行8个并发用户。

因为您仅使用每个片段创建部分总计,所以不会发生冲突和真正的并行更新(即,您写锁定每个片段而不是整个统一读取记录)。显然,这仅适用于数字字段。依靠数学修改来存储结果的东西。

因此,每个统一读取记录的每个统一读取字段有多个写入片段。这些数字片段还适用于ECC,加密和块级传输/存储。写入片段越多,对饱和数据的并行/并行写入访问速度就越大。

当大量玩家都开始使用Area of​​ Effect技能互相打击时,MMORPG遭受了严重的打击。这些多个播放器都需要同时完全并行地写入/更新其他所有播放器,从而在统一的播放器记录上造成写入行锁定风暴。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.