MySQL排他锁(FOR UPDATE)正在锁定整个表[重复]

Posted

技术标签:

【中文标题】MySQL排他锁(FOR UPDATE)正在锁定整个表[重复]【英文标题】:MySQL exclusive lock (FOR UPDATE) is locking the whole table [duplicate] 【发布时间】:2018-03-06 14:10:27 【问题描述】:

我有一个 mysql 表(使用 InnoDB 作为存储引擎)来存储用户事务。

CREATE TABLE `transactions` (
  `id` int(11) NOT NULL,
  `correlation_id` char(36) NOT NULL,
  `user_id` char(36) NOT NULL,
  `currency` char(3) NOT NULL,
  `time_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `transaction_amount` double NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE `transactions`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `correlation_id_unique` (`correlation_id`), 
  ADD INDEX (`user_id`);

我在多线程环境中工作并希望确保:

没有两个线程可以同时为同一个用户插入事务 如果线程正在为用户插入事务,则没有其他线程可以读取该用户的事务

我想出了以下解决方案:

    当线程要为用户插入事务时,获取该用户对应行的排他锁

    BEGIN;
    
    -- Acquire an exclusive lock on the rows with user_id=1
    SELECT * FROM transactions WHERE user_id = 1 FOR UPDATE;
    
    -- Insert transactions
    ...
    
    COMMIT;
    

    当一个线程想要读取一个用户余额时(通常是对一个用户的所有事务求和),它首先为这个用户对应的行获取一个共享锁

    SELECT SUM(transaction_amount) 
    FROM transactions 
    WHERE user_id=1 
    LOCK IN SHARE MODE;
    

但是,排他锁似乎锁定了整个表,而不仅仅是 SELECT...FOR UPDATE 语句返回的行。这是一个例子。

线程 1:

mysql> select user_id, transaction_amount from transactions;
+---------+--------------------+
| user_id | transaction_amount |
+---------+--------------------+
| 1       |                 10 |
| 1       |                 -2 |
| 2       |                  5 |
| 2       |                 10 |
+---------+--------------------+
4 rows in set (0.00 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM transactions WHERE user_id = 1 FOR UPDATE;
+----+----------------+---------+----------+---------------------+--------------------+
| id | correlation_id | user_id | currency | time_created        | transaction_amount |
+----+----------------+---------+----------+---------------------+--------------------+
|  1 | 1              | 1       | CHF      | 2018-03-06 09:54:28 |                 10 |
|  2 | 2              | 1       | CHF      | 2018-03-06 09:54:28 |                 -2 |
+----+----------------+---------+----------+---------------------+--------------------+
2 rows in set (0.01 sec)

线程 2:

-- Retrieve transactions of user 2
mysql> SELECT * FROM transactions WHERE user_id = 2 LOCK IN SHARE MODE;

[[Hangs]]

在阅读MySQL's documentation 之后,我预计这会起作用:

选择...锁定共享模式

在读取的任何行上设置共享模式锁。 其他会话可以读取行,但在您的事务提交之前不能修改它们

选择 ... 更新

对于搜索遇到的索引记录,锁定行和任何关联的索引条目,就像您为这些行发出 UPDATE 语句一样。其他事务被阻止更新那些行、执行 SELECT ... LOCK IN SHARE MODE 或读取某些事务隔离级别的数据。

现在,我找到了this topic,说明在我的情况下,user_id 字段应该有一个索引 - 确实如此。

我感觉问题可能是由于请求SELECT * FROM transactions WHERE user_id=1没有使用索引引起的:

EXPLAIN SELECT * FROM transactions WHERE user_id=1 FOR UPDATE;
+----+-------------+--------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table        | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+--------------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | transactions | NULL       | ALL  | user_id       | NULL | NULL    | NULL |    2 |    50.00 | Using where |
+----+-------------+--------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 3 warnings (0.00 sec)

有什么想法吗?

【问题讨论】:

【参考方案1】:

我使用 MySQL 5.6.31 测试了您的表,并在其中填充了 50 万行介于 1 和 1000 之间的随机值。

即使强制索引也无济于事:

mysql> EXPLAIN SELECT * FROM `transactions` force index (user_id) where user_id=1;
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | transactions | ALL  | user_id       | NULL | NULL    | NULL | 520674 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+

但即使没有索引提示,也可以搜索整数字符串:

mysql> EXPLAIN SELECT * FROM `transactions`  where user_id='1';
+----+-------------+--------------+------+---------------+---------+---------+-------+------+-----------------------+
| id | select_type | table        | type | possible_keys | key     | key_len | ref   | rows | Extra                 |
+----+-------------+--------------+------+---------------+---------+---------+-------+------+-----------------------+
|  1 | SIMPLE      | transactions | ref  | user_id       | user_id | 36      | const |    1 | Using index condition |
+----+-------------+--------------+------+---------------+---------+---------+-------+------+-----------------------+

将 varchar 列与二进制整数进行比较似乎破坏了可索引性。

【讨论】:

太棒了,非常感谢!

以上是关于MySQL排他锁(FOR UPDATE)正在锁定整个表[重复]的主要内容,如果未能解决你的问题,请参考以下文章

数据库:Mysql中“select ... for update”排他锁分析

数据库:Mysql中“select ... for update”排他锁分析

MySQL的SELECT ...for update

MySQL的SELECT ...for update

mysql(for update)悲观锁总结与实践

一文搞懂mysql中的共享锁排他锁悲观锁乐观锁以及使用场景