某天,你和你的同桌一起打电话问老师什么是爱情,老师应该先接谁的电话呢?双十一时,同一个产品有一万个人同时点击购买,而库存只有 1 个,应该卖给谁呢?……诸如此类的问题,我们称之为并发。
对于数据库而言,并发就是同一条数据有多个修改操作同时进行,那么这个时候数据库应该怎么处理并发呢?按照上述的例子,电话如果同时打进来,老师只有一个一个地接电话回答,MySQL 数据库也是一样,多个同时的修改操作,每次也只能允许其中的一个修改操作生效,其他的操作要么拒绝、要么等待。
那么数据库中处理并发的办法是什么呢?下面我们一起来讨论一下。
MySQL 中处理并发常用的方式是使用 MySQL 中的锁机制。面对各种复杂的场景,MySQL 按照锁的粒度(也就是锁影响的范围)将其内部的锁机制分成了全局锁、表级锁以及行锁等锁机制。
- 全局锁,影响的是所有的数据表;
- 表级锁,影响的是某部分数据表;
- 行级锁,影响的是某一行数据。
由于 MySQL 数据库的锁机制过于复杂,这一讲中我们只介绍全局锁和表级锁,行级锁我们下一讲再介绍。
全局锁
全局锁,见名知义,就是将数据库全局上锁,SQL 语句为: flush tables with read lock;
。通常情况下,全局锁一般用于全局数据迁移、全局系统备份等场景。当加上全局锁之后,MySQL 数据库的全局将会处于只读状态,所有的修改操作(表数据的增删改、创建表等修改操作)都不能正常执行,具体操作如下:
-- 增加数据库全局锁,简称:FTWRL
mysql> flush tables with read lock;
Query OK, 0 rows affected (0.00 sec)
-- 测试插入数据失败
mysql> insert into t1 values (1);
ERROR 1223 (HY000): Can't execute the query because you have a conflicting read lock
加上全局锁之后,整个数据库将无法修改任何数据,换句话说就是一旦数据库加上了全局锁,也就意味着所有的业务将处于不可用状态,这听上去感觉是不是很可怕?但是,我们在玩王者荣耀的时候,是不是经常会遇到在某个时间段内因系统维护而不能进入游戏的情况呢?这个就是系统处于维护状态,很有可能王者荣耀的维护者们要对王者荣耀中的数据库进行数据迁移或者其他的维护。具体见下图:
如图所示,数据库开始数据迁移,恰好这个时候有玩家购买了一款新皮肤,此时是不能直接操作新数据库的(因为数据还没有迁移完成,新的数据库还存在数据缺失,不能使用),而后很有可能这条购买皮肤的数据未被迁移至新的数据库,以至于数据库迁移完成之后,该用户很可能会找不到自己刚购买的这款新皮肤。这个时候,玩家一定会很疑惑,为什么我购买的新皮肤会无缘无故地消失呢?此举也会给玩家造成很不好的用户体验。
但是,如果这个时候使用 MySQL 数据库的全局锁,禁止玩家操作购买皮肤,等系统数据迁移完成,释放全局锁之后再恢复交易就不会出现这样的情况。
-- 释放全局锁
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
-- 测试修改数据
mysql> insert into t1 values (2);
Query OK, 1 row affected (0.01 sec)
-- 查看数据
mysql> select * from t1;
+------+
| id |
+------+
| 1 |
| 2 |
+------+
2 rows in set (0.00 sec)
这就是全局锁的典型使用场景。
那么全局锁既然是为了让数据库处于只读状态,那么为什么不设置全局只读(set global readonly = true
)呢?其原因是 set global readonly = true
主要是用来设置 MySQL 数据库的从节点的,以至于整个从节点处于只读状态,但是只要数据库不重启,将会全局持久生效;而 FTWRL 设置之后,客户端只要断开就会立即释放全局锁,所以不建议使用 set global readonly = true
充当全局锁。
有时候我们只需要操作某部分数据表的数据,如果我们仍然使用全局锁的话,就会影响其他数据表的操作了,所以这个时候我们一般采用表级锁来实现这个功能。
表级锁
在 MySQL 中,表级锁可分为两种,一个是表锁,另一个是元数据锁(MDL)。
1. 表锁
表锁跟全局锁类似,不同的是表锁是用来处理某一部分表的并发的。并且表锁只在当前的连接中生效,当当前的连接断开之后,表级锁将自动释放。
在 MySQL 数据库中设置表锁的 SQL 语句为: lock tables [tableName] read/write;
,设置完表锁之后,有两种方式可以释放表锁,第一种是使用 unlock tables;
释放锁,第二种是当客户端断开之后,锁自动释放。
加上表锁有两种情况,分别是 read 表锁和 write 表锁。设置 read 表锁之后,同一个连接中该表只读不可写,其他表无法读写;其他连接针对该表只读不可写,而对其他数据表可读可写。设置 write 表锁之后,同一个连接该表可读可写,其他表不可读不可写;其他连接该表不可读不可写,其他表可读可写。
设置 read 表级锁之后,同一个连接该表只读不可写,其他表无法读写。示例如下:
— 增加表级锁 mysql> lock tables t1 read; Query OK, 0 rows affected (0.00 sec)
— 可以读取 mysql> select * from t1; +———+ | id | +———+ | 1 | | 2 | | 3 | | 4 | +———+ 4 rows in set (0.00 sec)
— 当添加只读表级锁之后,整个数据表不可写 mysql> insert into t1 values (5); ERROR 1099 (HY000): Table ‘t1’ was locked with a READ lock and can’’t be updated
— 也无法插入其他表 mysql> insert into t2 values (6); ERROR 1100 (HY000): Table ‘t2’ was not locked with LOCK TABLES
— 也无法读取其他数据表 mysql> select * from t2; ERROR 1100 (HY000): Table ‘t2’ was not locked with LOCK TABLES
— 释放表级锁 mysql> unlock tables; Query OK, 0 rows affected (0.00 sec)
设置 read 表级锁之后,其他连接该表只读不可写,其他表可读可写。具体展示如下:
— 该表其他客户端可读 mysql> select * from t1; +———+ | id | +———+ | 1 | | 2 | | 3 | | 4 | +———+ 6 rows in set (0.00 sec)
— 该表其他客户端不可写 mysql> insert into t1 values (5); ^C^C — query aborted ERROR 1317 (70100): Query execution was interrupted
— 其他表其他客户端可写 mysql> insert into t2 values (6); Query OK, 1 row affected (0.01 sec)
— 其他表其他客户端可读 mysql> select * from t2; +———+ | id | +———+ | 1 | +———+ 3 rows in set (0.00 sec)
设置 write 表级锁之后,该表可读可写,其他表不可读不可写。具体展示如下:
— 将数据表t1设置表锁 mysql> lock tables t1 write; Query OK, 0 rows affected (0.00 sec)
— 读取本表 mysql> select * from t1; +———+ | id | +———+ | 1 | | 2 | | 3 | | 4 | +———+ 5 rows in set (0.00 sec)
— 读取其他表 mysql> select * from t2; ERROR 1100 (HY000): Table ‘t2’ was not locked with LOCK TABLES
— 插入本表可以 mysql> insert into t1 values (5); Query OK, 1 row affected (0.00 sec)
— 插入其他表不行 mysql> insert into t2 values (5); ERROR 1100 (HY000): Table ‘t2’ was not locked with LOCK TABLES
— 释放表级锁 mysql> unlock tables; Query OK, 0 rows affected (0.00 sec)
设置 write 表级锁之后,其他客户端该表不可读不可写,其他表可读可写。具体如下:
— 其他客户端当前表不可读 mysql> select * from t1; ^C^C — query aborted ERROR 1317 (70100): Query execution was interrupted
— 其他客户端当前表不可写 mysql> insert into t1 values (6); ^C^C — query aborted ERROR 1317 (70100): Query execution was interrupted
— 其他客户端其他表可读 mysql> select * from t2; +———+ | id | +———+ | 1 | | 6 | | 6 | | 6 | +———+ 4 rows in set (0.00 sec)
— 其他客户端其他表可写 mysql> insert into t2 values (6); Query OK, 1 row affected (0.01 sec)
2. 元数据锁
元数据锁是另一种表级锁,又称为字典锁。在高频写入的数据表中,删除一个字段,这个时候很可能导致查询的字段不一致而失败,为了防止这种情况的发生,MySQL 为我们提供了一个元数据锁。元数据锁在操作一个事务(InnoDB 存储引擎的各种操作自动加事务)时是自动加上的,主要用来防止数据表数据改动时的数据安全问题出现。其具体操作展示为如下。
我们启动一个事务读取一条数据,此时不要提交或者回滚以至于元数据锁不会立即释放:
-- 启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- 查询一条数据
mysql> select * from t1;
+------+
| id |
+------+
| 1 |
| 2 |
| 3 |
+------+
6 rows in set (0.00 sec)
在另外一个客户端中修改该表的表结构,此时就会因为上述操作中的元数据锁未被释放导致修改表结构失败:
-- 修改表结构
mysql> alter table t1 add name varchar(100);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
最终,无法修改表结构。其原因是:当一个事务先查询表数据且还未提交之时,会自动给该表加一个元数据锁,以至于另外一个客户端在修改该表结构时无法修改成功。
而当提交第一个客户端中的事务之后,随即也就会释放元数据锁:
-- 提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
上述操作中释放了事务之后,元数据锁也随即释放,所以其他客户端此时可以修改表结构:
-- 修改表结构
mysql> alter table t1 add name varchar(100);
Query OK, 0 rows affected (0.07 sec)
Records: 0 Duplicates: 0 Warnings: 0
也就是说,只有当前面一个事务释放了元数据锁之后,其他客户端才可以修改表结构。
在一个使用频率不是很高的表中,修改表结构可能还没有什么大的阻碍。但是当在一个操作频率非常高的数据表中,因为有元数据锁的存在,很可能会导致修改表结构失败。这个时候建议多重试几次,如果还是不行,可以适当地使用 Innodb_lock_wait_timeout 设置 SQL 的执行超时时间,看是否能够抢到元数据锁。切记不要长时间执行,导致阻塞。
总结
全局锁主要用于需要同时操作整个数据库中的所有数据的情况。一旦加上了全局锁之后,整个数据将处于全局只读状态,也就意味着主库无法写,从库无法通过 Binlog 实现主从同步,所以非必需场景下,慎用全局锁。
表级锁主要分为两个,分别是表锁与元数据锁。增加了表锁,该表的操作将会受到限制。而增加表锁的方式有自动和手动两种:手动增加上文有案例,这里不再赘述;当有查询数据未使用到索引时,全表更新以及多表事务级联时就会自动加表锁,所以在使用 SQL 时一定要避免操作大量数据以及跨表事务。在实际的应用中,我不太建议你使用表锁,一般只有在不支持行锁的存储引擎中才会使用表锁。
在数据库备份(数据库迁移还是需要用到全局锁)的情况下,由于表锁和全局锁不是很灵活,使用起来影响的范围太大,所以一般也不是很建议使用。对于数据备份和数据迁移时,我比较建议你使用 MySQL 自带的数据备份工具 mysqldump,因为 mysqldump 中携带了--single-transaction
参数,这个参数在数据备份之前,会启动一个全局的事务,进而不会影响其他的数据操作。
此时你一定也对行锁很好奇,下一讲我们来好好聊聊行锁,不见不散。