在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?
Posted
技术标签:
【中文标题】在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?【英文标题】:What the sequence of the strings locks in case of SELECT … INNER JOIN … FOR UPDATE and how to avoid deadlock?在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是什么锁定以及如何避免死锁? 【发布时间】:2018-05-05 11:51:13 【问题描述】:角色 mysql 5.7.21-20
— Memory temporary table TQueue
CREATE TEMPORARY TABLE IF NOT EXISTS TQueue (
ID bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
QUEUE_STATUS enum ('ADDED', 'PROCESSED', 'SUCCESS', 'ERROR') NOT NULL DEFAULT 'ADDED',
QUEUE_TIMEOUT datetime NOT NULL,
ACTION enum ('INSERT', 'DELETE', 'UPDATE') NOT NULL,
REPORT_ID tinyint(4) UNSIGNED NOT NULL,
LOGIN int(11) NOT NULL,
`GROUP` char(16) NOT NULL,
ENABLE int(11) NOT NULL,
ENABLE_CHANGE_PASS int(11) NOT NULL,
ENABLE_READONLY int(11) NOT NULL,
ENABLE_OTP int(11) NOT NULL,
PASSWORD_PHONE char(32) NOT NULL,
NAME char(128) NOT NULL,
COUNTRY char(32) NOT NULL,
CITY char(32) NOT NULL,
STATE char(32) NOT NULL,
ZIPCODE char(16) NOT NULL,
ADDRESS char(128) NOT NULL,
LEAD_SOURCE char(32) NOT NULL,
PHONE char(32) NOT NULL,
EMAIL char(48) NOT NULL,
COMMENT char(64) NOT NULL,
ID_DOCUMENT char(32) NOT NULL,
STATUS char(16) NOT NULL,
REGDATE datetime NOT NULL,
LASTDATE datetime NOT NULL,
LEVERAGE int(11) NOT NULL,
AGENT_ACCOUNT int(11) NOT NULL,
TIMESTAMP int(11) NOT NULL,
BALANCE double NOT NULL,
PREVMONTHBALANCE double NOT NULL,
PREVBALANCE double NOT NULL,
CREDIT double NOT NULL,
INTERESTRATE double NOT NULL,
TAXES double NOT NULL,
SEND_REPORTS int(11) NOT NULL,
MQID int(10) UNSIGNED NOT NULL,
USER_COLOR int(11) NOT NULL,
EQUITY double NOT NULL,
MARGIN double NOT NULL,
MARGIN_LEVEL double NOT NULL,
MARGIN_FREE double NOT NULL,
CURRENCY char(16) NOT NULL,
API_DATA blob DEFAULT NULL,
MODIFY_TIME datetime NOT NULL,
PRIMARY KEY (ID),
INDEX IDX_JOIN USING BTREE (LOGIN, REPORT_ID)
)
ENGINE = MEMORY;
CREATE TABLE `MT4_USERS` (
ID bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
REPORT_ID tinyint(4) UNSIGNED NOT NULL,
LOGIN int(11) NOT NULL,
`GROUP` char(16) NOT NULL,
ENABLE int(11) NOT NULL,
ENABLE_CHANGE_PASS int(11) NOT NULL,
ENABLE_READONLY int(11) NOT NULL,
ENABLE_OTP int(11) NOT NULL,
PASSWORD_PHONE char(32) NOT NULL,
NAME char(128) NOT NULL,
COUNTRY char(32) NOT NULL,
CITY char(32) NOT NULL,
STATE char(32) NOT NULL,
ZIPCODE char(16) NOT NULL,
ADDRESS char(128) NOT NULL,
LEAD_SOURCE char(32) NOT NULL,
PHONE char(32) NOT NULL,
EMAIL char(48) NOT NULL,
COMMENT char(64) NOT NULL,
ID_DOCUMENT char(32) NOT NULL,
STATUS char(16) NOT NULL,
REGDATE datetime NOT NULL,
LASTDATE datetime NOT NULL,
LEVERAGE int(11) NOT NULL,
AGENT_ACCOUNT int(11) NOT NULL,
TIMESTAMP int(11) NOT NULL,
BALANCE double NOT NULL,
PREVMONTHBALANCE double NOT NULL,
PREVBALANCE double NOT NULL,
CREDIT double NOT NULL,
INTERESTRATE double NOT NULL,
TAXES double NOT NULL,
SEND_REPORTS int(11) NOT NULL,
MQID int(10) UNSIGNED NOT NULL,
USER_COLOR int(11) NOT NULL,
EQUITY double NOT NULL,
MARGIN double NOT NULL,
MARGIN_LEVEL double NOT NULL,
MARGIN_FREE double NOT NULL,
CURRENCY char(16) NOT NULL,
API_DATA blob DEFAULT NULL,
MODIFY_TIME datetime NOT NULL,
PRIMARY KEY (ID),
UNIQUE KEY IDX_LOGIN_REPORT_ID (`LOGIN`,`REPORT_ID`)
)
ENGINE=InnoDB
ROW_FORMAT=COMPRESSED
从内存表TQueue中的队列中选择数据。
我需要插入表 MT4_USERS。应该更新现有的字符串,所以我在 DUPLICATE KEY UPDATE 上做。
为了确保不会发生死锁,我尝试在 INSERT ON DUPLICATE KEY UPDATE 之前进行排他锁。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
— hold exclusive lock on MT4_USERS table
SELECT
u.*
FROM
TQueue q
INNER JOIN MT4_USERS u USING (LOGIN, REPORT_ID)
FOR UPDATE;
— insert to MT4_USERS form TQueue ON DUPLICATE KEY UPDATE
INSERT MT4_USERS (REPORT_ID, LOGIN, `GROUP`, ENABLE …)
SELECT
REPORT_ID, LOGIN, `GROUP`, ENABLE, ENABLE_CHANGE_PASS …
FROM TQueue
ORDER BY ID ASC
ON DUPLICATE KEY UPDATE
`GROUP` = VALUES(`GROUP`),
ENABLE = VALUES(ENABLE),
ENABLE_CHANGE_PASS = VALUES(ENABLE_CHANGE_PASS),
…
COMMIT;
理想情况下,我想为表 MT4_USERS 的字符串设置排他锁,该锁与表 TQueue 的字符串及其字段(LOGIN、REPORT)一致,以便在不死锁的情况下执行 INSERT ON DUPLICATE KEY UPDATE。
但是我在并行事务中遇到了死锁。它执行相同的操作,但使用表 MT4_TRADES。它还使 LEFT JOIN MT4_USERS。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
— hold exclusive lock on MT4_TRADES table
SELECT
t.*
FROM
TQueue q
INNER JOIN MT4_TRADES t USING (TICKET, REPORT_ID)
FOR UPDATE;
— insert to MT4_TRADES form TQueue ON DUPLICATE KEY UPDATE
INSERT MT4_TRADES (REPORT_ID, TICKET, LOGIN, SYMBOL, …)
SELECT
q.REPORT_ID, q.TICKET, q.LOGIN, q.SYMBOL, …
FROM
TQueue q
LEFT JOIN MT4_USERS mu USING(REPORT_ID, LOGIN)
ORDER BY
q.ID ASC
ON DUPLICATE KEY UPDATE
LOGIN = VALUES(LOGIN)
,SYMBOL = VALUES(SYMBOL)
,DIGITS = VALUES(DIGITS)
,…
COMMIT;
SHOW ENGINE INNODB STATUS
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-05-05 00:24:35 0x7fd53e5a6700
*** (1) TRANSACTION:
TRANSACTION 178168806, ACTIVE 1 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 17 lock struct(s), heap size 1136, 45 row lock(s)
MySQL thread id 17661615, OS thread handle 140555520485120, query id 4591798647 event_scheduler Sending data
SELECT u.* FROM TQueue q INNER JOIN MT4_USERS u USING (LOGIN, REPORT_ID) FOR UPDATE
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 876643 page no 46339 n bits 952 index IDX_LOGIN_REPORT_ID of table `Developer`.`MT4_USERS` trx id 178168806 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 178168783, ACTIVE 3 sec starting index read
mysql tables in use 6, locked 4
8870 lock struct(s), heap size 1138896, 22372 row lock(s), undo log entries 1876
MySQL thread id 17661574, OS thread handle 140553850873600, query id 4591797640 event_scheduler
INSERT MT4_TRADES
(
REPORT_ID, TICKET, LOGIN, SYMBOL, DIGITS, CMD, VOLUME, OPEN_TIME, OPEN_PRICE, SL, TP, CLOSE_TIME,
EXPIRATION, REASON, CONV_RATE1, CONV_RATE2, COMMISSION, COMMISSION_AGENT, SWAPS, CLOSE_PRICE,
PROFIT, TAXES, COMMENT, INTERNAL_ID, MARGIN_RATE, `TIMESTAMP`, MAGIC, GW_VOLUME, GW_OPEN_PRICE,
GW_CLOSE_PRICE, MODIFY_TIME, CURRENCY, DELIMER, RATE, TICKET_STATUS
)
SELECT
q.REPORT_ID, q.TICKET, q.LOGIN, q.SYMBOL, q.DIGITS, q.CMD, q.VOLUME, q.OPEN_TIME, q.OPEN_PRICE, q.SL, q.TP, q.CLOSE_TIME,
q.EXPIRATION, q.REASON, q.CONV_RATE1, q.CONV_RATE2, q.COMMISSION, q.COMMISSION_AGENT, q.SWAPS, q.CLOSE_PRICE,
q.PROFIT, q.TAXES, q.COMMENT, q.INTERNAL_ID, q.MARGIN_RATE, q.`TIMESTAMP`, q.MAGIC, q.GW_VOLUME, q.GW_OPEN_PRICE,
q.GW_CLOSE_PRICE, q.MODIFY_TIME, mu.CURRENCY, mu.DELIMER, IF(mu.CURRENCY = 'USD', 1, SearchOfRate(q.REPORT_ID, mu.CURRENCY, 'USD', q.MODIFY_TIME, -1)), IF(q.`
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 876643 page no 46339 n bits 952 index IDX_LOGIN_REPORT_ID of table `Developer`.`MT4_USERS` trx id 178168783 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 876643 page no 46338 n bits 952 index IDX_LOGIN_REPORT_ID of table `Developer`.`MT4_USERS` trx id 178168783 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (1)
在SELECT ... INNER JOIN ... FOR UPDATE的情况下,字符串的顺序是什么锁以及如何避免死锁?
【问题讨论】:
【参考方案1】:死锁在您的MT4_USERS
表的索引上,我相信您已经注意到了。这就是为什么应用程序的不同部分中的两个查询显然会发生冲突的原因。
这个查询
SELECT u.*
FROM TQueue q
INNER JOIN MT4_USERS u USING (LOGIN, REPORT_ID)
FOR UPDATE;
锁定整个MT4_USERS
表。为什么?你可以只锁定一排吗?这是SELECT ... FOR UPDATE
的常见用例。
或者也许您应该跳过SELECT FOR UPDATE
锁和事务,而只执行INSERT ... ON DUPLICATE KEY UPDATE
操作独立。如果两个不同的操作更新相同的行,那么后面的操作将确定最终值。单个查询给出一致的结果。
(我猜到了一些事情,因为您没有显示死锁中涉及的两个查询。)
【讨论】:
"This query locks your entire MT4_USERS table. Can you lock just one row instead?»
我只需要对 TQueue 表中存在的字符串进行排他锁。我不需要阻止整个表,这就是为什么我在«INSERT ... ON DUPLICATE KEY UPDATE»开始执行之前执行«SELECT ... FOR UPDATE»。为什么说查询 «SELECT ... FOR UPDATE» 会阻塞整个表 MT4_USERS?
«Or maybe you should skip the SELECT FOR UPDATE lock and the transaction, and just do the INSERT ... ON DUPLICATE KEY UPDATE operation standalone.»
如果没有排他锁,两个并行查询«INSERT ... ON DUPLICATE KEY UPDATE»会出现死锁,很明显,我已经遇到过这种情况。这就是为什么我想提前使用查询«SELECT ... FOR UPDATE»来获取排他锁。 TQueue 表的每个事务可能包含 100 到 10000 行。这就是为什么我不能使用简单的“SELECT ... FOR UPDATE”,我必须使用«SELECT...INNER JOIN..FOR UPDATE»。以上是关于在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?的主要内容,如果未能解决你的问题,请参考以下文章
带有 WHERE 条件和 INNER JOIN 的 SELECT 命令
SELECT * FROM people、people_emails、members 或 INNER JOIN [重复]
在 SELECT ... INNER JOIN ... FOR UPDATE 的情况下,字符串的顺序是啥锁定以及如何避免死锁?