锁定和解锁 MySQL 中的表

在本文中,您将了解如何使用 MySQL LOCK TABLESUNLOCK TABLES 语句获取表锁和释放表锁。

假设在一个银行系统中有这样的逻辑:

当用户 A 从自己的银行账户取出 500 元时,用户 A 的余额为 原余额 减去 500。当另一个用户 B 给用户转入 500 元时,用户 A 的余额为 原余额 加上 500。

如果这两个操作同时发生,则可能导致用户 A 的余额是错误的。

MySQL 的锁就是为了解决这种并发问题的。MySQL 支持三种类型的锁:表级锁、行级锁和页面所。

MySQL 允许您在会话中显式地获取表锁,以防止其他会话在您需要独占访问表的期间修改表。

锁的操作是在当前会话中进行的。一个会话只能为自己获取锁,并只能释放自己的锁。

MySQL 提供了 LOCK TABLESUNLOCK TABLES 语句用于显式地的获取表锁和释放表锁。

MySQL 表锁语法

LOCK TABLES 语法

要为当前会话显式地获取表锁,请按照以下语法使用 LOCK TABLES 语句:

LOCK TABLES
    table_name [READ | WRITE]
    [, table_name [READ | WRITE], ...];

这里:

  • table_name 是您要获取锁的表名。
  • READWRITE 是锁类型。 READ 锁用于共享读取表, WRITE 锁用于排斥的读写表。
  • 您可以在一个语句中获取多个表的锁。多个表锁之间使用逗号分隔。
  • LOCK TABLES 语句在获取新的表锁之前会隐式的释放当前会话持有的所有的表锁。
  • 您可以使用 LOCK TABLE 代替 LOCK TABLES

UNLOCK TABLES 语法

要释放当前会话获取的所有的表锁,请使用以下语句:

UNLOCK TABLES;

锁类型

表锁支持 READWRITE 两种类型的锁。 READ 锁用于共享读取表, WRITE 锁用于排斥的读写表,他们的特点如下:

READ

  • 持有表锁的会话只能读取表,但不能写入表。

  • 多个会话可以同时获取一个表的 READ 锁。

  • 其他会话无需显式获取 READ 锁即可读取该表,但是不能写入表。其他会话的写操作会一直等待知道读锁被释放。

WRITE

  • 持有锁的会话可以读写表。

  • 只有持有锁的会话才能访问该表。在释放锁之前,没有其他会话可以访问它。

  • 持有 WRITE 锁时,其他会话对表的锁请求会阻塞。

如果您没有显式地的释放表锁,当会话结束后,无论是 READ 锁还是 WRITE 锁,都会被 MySQL 释放掉。

MySQL 表锁实例

这个实例演示了如何在 MySQL 中获取表锁( READWRITE 锁)和释放表锁。

我们使用以下语句在 testdb 数据库中 创建一个 test_lock 以实践我们的实例:

CREATE TABLE `test_lock` (
  `id` int NOT NULL AUTO_INCREMENT,
  `txt` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
);

如果您没有 testdb 数据库,请先使用如下语句创建数据库并选择数据库:

CREATE DATABASE testdb;
use testdb;

READ 锁实例

READ 锁是共享的读锁。让我们通过以下实例了解 READ 锁的特性。

  1. 先开启一个会话,并使用 SHOW PROCESSLIST 语句查看当前会话的 ID:

    SHOW PROCESSLIST;
    
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+
    | Id | User            | Host      | db     | Command | Time   | State                           | Info                                             |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+
    |  5 | event_scheduler | localhost | NULL   | Daemon  | 201156 | Waiting on empty queue          | NULL                                             |
    |  8 | root            | localhost | testdb | Query   |      0 | init                            | show PROCESSLIST                                 |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+

    这里,当前会话的 ID 为 8。

  2. 在当前会话没有获取 READ 锁的情况下向表中插入一个新行,

    INSERT INTO test_lock(txt)
    VALUES('Hello');
    

    这里能正常插入。

  3. 使用下面的 LOCK TABLES 获取 test_lock 表的读锁:

    LOCK TABLES test_lock READ;
    
  4. 让我们在拥有 READ 锁的情况下向表中插入一个新行,

    INSERT INTO test_lock(txt)
    VALUES('Hello World');
    

    此时,MySQL 给出一个错误: “ERROR 1099 (HY000): Table ’test_lock’ was locked with a READ lock and can’t be updated”。

  5. 您可以重新打开一个会话,并在新会话测试读表:

    SELECT * FROM test_lock;
    
    +----+-------------+
    | id | txt         |
    +----+-------------+
    |  1 | Hello       |
    +----+-------------+

    这说明,拥有表的读锁的会话之外的其他会话也可以读表。

  6. 在新的会话中测试写表。请执执行上面的的 INSERT 语句。你会发现,该操作将被挂起,直到表的读锁被释放。

  7. 您可以使用 SHOW PROCESSLIST 语句查看回话是否处于等待的状态:

    SHOW PROCESSLIST;
    
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+
    | Id | User            | Host      | db     | Command | Time   | State                           | Info                                             |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+
    |  5 | event_scheduler | localhost | NULL   | Daemon  | 201156 | Waiting on empty queue          | NULL                                             |
    |  8 | root            | localhost | testdb | Query   |      0 | init                            | show PROCESSLIST                                 |
    |  9 | root            | localhost | testdb | Query   |     16 | Waiting for table metadata lock | INSERT INTO test_lock(txt) VALUES('Hello World') |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+--------------------------------------------------+

    您可以看到, Id 为 9 的会话正在等待锁释放,它的状态为:Waiting for table metadata lock

WRITE 锁实例

WRITE 锁是排他锁,只有持有锁的会话才能访问表,其他会话的对表的操作都会等待锁的释放。让我们通过以下示例了解 WRITE 锁的特性。

  1. 先开启一个会话,并从此会话中获取 test_lockWRITE 锁。

    LOCK TABLES test_lock WRITE;
    
  2. 使用以下语句在 test_lock 表中插入一个新行。

    INSERT INTO test_lock(txt)
    VALUES('Hi');
    

    插入成功。这说明持有表的写锁的会话可以写表。

  3. 使用以下语句从 test_lock 表中查询数据:

    SELECT * FROM test_lock;
    
    +----+-------------+
    | id | txt         |
    +----+-------------+
    |  1 | Hello       |
    |  2 | Hello World |
    |  3 | Hi          |
    +----+-------------+

    查询成功。这说明持有表的写锁的会话可以读表。

  4. 开启另一个会话,并尝试读取数据:

    SELECT * FROM test_lock;
    

    您会发现,该操作处于等待的状体,并且没有返回。

  5. 您可以使用 SHOW PROCESSLIST 语句查看会话的状态

    SHOW PROCESSLIST;
    
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+-------------------------+
    | Id | User            | Host      | db     | Command | Time   | State                           | Info                    |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+-------------------------+
    |  5 | event_scheduler | localhost | NULL   | Daemon  | 202266 | Waiting on empty queue          | NULL                    |
    |  8 | root            | localhost | testdb | Query   |      0 | init                            | SHOW PROCESSLIST        |
    |  9 | root            | localhost | testdb | Query   |     81 | Waiting for table metadata lock | SELECT * FROM test_lock |
    +----+-----------------+-----------+--------+---------+--------+---------------------------------+-------------------------+
  6. 在第一个会话中使用下面的语句释放掉持有的锁:

    UNLOCK TABLES;
    

    您将看到第二个会话中的 SELECT 语句的执行结果如下:

    +----+-------------+
    | id | txt         |
    +----+-------------+
    |  1 | Hello       |
    |  2 | Hello World |
    |  3 | Hi          |
    +----+-------------+

结论

MySQL 允许您在会话中使用 LOCK TABLES 语句显式地获取表锁,以防止其他会话在您需要独占访问表的期间修改表。

MySQL 允许您在会话中使用 UNLOCK TABLES 语句显式地释放表锁。

MySQL 表锁有两种类型:共享的读锁和排他的写锁。