MySQL独占锁(FOR UPDATE)锁定整个表

时间:2018-03-06 14:10:27

标签: mysql multithreading concurrency locking innodb

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

// OS.hpp -------------------------------------------------
// !!!! HERE I WILL NOT INCLUDE APPLICATION.HPP !!!!
// I JUST USE THE APPLICATION DEFINITION TO ACCESS THE 
// STATIC FUNCTION
extern class Application;

class OS {
public:
    OS() {}
    ~OS() {}
    int getInfo() { return Application::get(); }
}
// Middleware.hpp -----------------------------------------
#include "OS.hpp"

class Middleware {
    OS os;
public:
    Middleware() {}
    ~Middleware() {}
    void doSomething() { int a = os.getInfo(); }
}
// Application.hpp ----------------------------------------
#include "Middleware.hpp"

class Application {
    Middleware mw;
public:
    Application() {}
    ~Application() {}
    void run() { mw.doSomething(); }
    static int get() { return 0; } // !!!! this function (static) i would like to access from OS
}

// main.cpp -----------------------------------------------
#include "Application.hpp"

int main() {
    Application app;
    app.run();
}

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

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

我提出了以下解决方案:

  1. 当线程想要为用户插入事务时,获取与该用户对应的行的独占锁

    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`);
    
  2. 当线程想要读取用户余额时(通常通过对用户的所有事务求和),它首先获取与该用户对应的行的共享锁

    BEGIN;
    
    -- Acquire an exclusive lock on the rows with user_id=1
    SELECT * FROM transactions WHERE user_id = 1 FOR UPDATE;
    
    -- Insert transactions
    ...
    
    COMMIT;
    
  3. 但是,似乎独占锁是锁定整个表,而不仅仅是SELECT ... FOR UPDATE语句返回的行。这是一个例子。

    主题1:

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

    主题2:

    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)
    

    阅读MySQL's documentation之后,我原本预计会这样做:

      

    选择...锁定共享模式

         

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

         

    SELECT ... FOR UPDATE

         

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

    现在,我找到this topic,说明在我的情况下,-- Retrieve transactions of user 2 mysql> SELECT * FROM transactions WHERE user_id = 2 LOCK IN SHARE MODE; [[Hangs]] 字段应该有一个索引 - 而且确实如此。

    我觉得问题可能是由请求user_id没有使用索引引起的:

    SELECT * FROM transactions WHERE user_id=1

    有什么想法吗?

1 个答案:

答案 0 :(得分: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列与二进制整数的比较似乎打败了可索引性。