MySql优化 — 锁

小龙 694 2022-06-27

简介

数据库的锁是为了处理并发:并发就是同一条数据有多个修改操作同时进行。面对各种复杂的场景,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

加上全局锁之后,整个数据库将无法修改任何数据,换句话说就是一旦数据库加上了全局锁,也就意味着所有的业务将处于不可用状态。

全局锁既然是为了让数据库处于只读状态,那么为什么不设置全局只读(set global readonly = true)呢?其原因是 set global readonly = true 主要是用来设置 MySQL 数据库的从节点的,以至于整个从节点处于只读状态,但是只要数据库不重启,将会全局持久生效;而 FTWRL 设置之后,客户端只要断开就会立即释放全局锁,所以不建议使用 set global readonly = true 充当全局锁。

表级锁

表锁跟全局锁类似,不同的是表锁是用来处理某一部分表的并发的。并且表锁只在当前的连接中生效,当当前的连接断开之后,表级锁将自动释放。

在 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)

元数据锁

元数据锁是另一种表级锁,又称为字典锁。在高频写入的数据表中,删除一个字段,这个时候很可能导致查询的字段不一致而失败,为了防止这种情况的发生,MySQL 为我们提供了一个元数据锁。元数据锁在操作一个事务(InnoDB 存储引擎的各种操作自动加事务)时是自动加上的,主要用来防止数据表数据改动时的数据安全问题出现。其具体操作展示为如下。

我们启动一个事务读取一条数据,此时不要提交或者回滚以至于元数据锁不会立即释,在另外一个客户端中修改该表的表结构,此时就会因为上述操作中的元数据锁未被释放导致修改表结构失败;最终,无法修改表结构。其原因是:当一个事务先查询表数据且还未提交之时,会自动给该表加一个元数据锁,以至于另外一个客户端在修改该表结构时无法修改成功。

而当提交第一个客户端中的事务之后,随即也就会释放元数据锁,上述操作中释放了事务之后,元数据锁也随即释放,所以其他客户端此时可以修改表结构;也就是说,只有当前面一个事务释放了元数据锁之后,其他客户端才可以修改表结构。

在一个使用频率不是很高的表中,修改表结构可能还没有什么大的阻碍。但是当在一个操作频率非常高的数据表中,因为有元数据锁的存在,很可能会导致修改表结构失败。这个时候建议多重试几次,如果还是不行,可以适当地使用 Innodb_lock_wait_timeout 设置 SQL 的执行超时时间,看是否能够抢到元数据锁。切记不要长时间执行,导致阻塞。

行级锁

全局锁和表级锁有一个缺点就是锁住的数据太多。例如,当需要修改第一条数据时,只需要锁住第一条数据即可,不需要锁住所有的数据。为了解决这个问题,MySQL 数据又提供了一个行级锁。

行级锁是需要存储引擎支持的。在 MySQL 数据库中,MyISAM 存储引擎是不支持行级锁的,支持行锁的是 InnoDB 存储引擎,这也是 MySQL 数据库后来选择 InnoDB 存储引擎为默认存储引擎的原因之一。

行级锁的功能是跟表级锁类似,都是用来锁定数据的,用来防止并发导致数据修改失败这种情况的出现。所不同的是表锁是锁定整个表的数据,而行级锁是操作一行、锁定一行,并且行级锁也是 MySQL 中粒度最小的锁

当操作某个数据表中的数据使用表级锁时,整个数据表中所有的数据都会被锁定而无法操作,也就是同一时间只能有一个修改操作,这是非常影响数据库并发的,不适合访问量大的场景。而行级锁是操作一行、锁定一行,所以在最大程度上减少了数据表中多个操作之间的冲突,进而提升了数据库数据更新的并发。

行级锁就是将修改一行、锁住一行。但这里有一点我们需要注意:行级锁是产生于一个事务之中的,当事务提交或者回滚之后,行级锁立即自动释放。

在 MySQL 数据库中行级锁主要有两种,分别是:共享锁排他锁

共享锁(Shared Lock)

共享锁又称为读锁,也可以简称为:S 锁。添加共享锁的语句为:lock in share mode。当多个事务同一时间修改同一条数据时,共享锁只允许其中的一个事务修改数据,读数据则不限制。

通常情况下,共享锁的使用场景是保证数据库中数据与数据之间的关系。举个例子:我们在购物时,总是会让我们添加地址;在填写地址之时,首先会让我们选择省份,其次是市,最后是详细的地址。在这个场景中,如果我们需要添加浦东新区的话,就意味着上海市这个上级选项必须存在。假设,我们添加浦东新区时,恰好其他人把上海市删除;那此时,我们添加的浦东新区就失去了它的意义。

共享锁的主要功能是:在某个事务中某条数据只要加上了共享锁,那么对于其他的事务来说该条数据将可读但是无法修改。

排他锁(Exclusive Lock)

排他锁又称为写锁,也可以简称为:X 锁。在某个事务中给某些数据添加了排他锁,那么这部分数据将无法添加其他锁机制。添加排他锁的语句是:for update

排他锁和共享锁一样,是需要在事务中开启,当事务提交或者回滚之后将自动释放。另外,还需要注意的是,在一个事务中一个更新的操作会自动添加排他锁

举个例子:618店铺在做秒杀活动,恰好这个时候商品的库存只有一个了,而现在还有两个人在抢购。假设我们不考虑并发(同时修改同一条数据就称为并发)的情况下,两个人同时抢购成功,就会把库存从 1 修改成 -1 ,这时就出现了超卖的情况。为了防止这种情况,在扣减库存时可以采用 MySQL 数据库为我们提供的锁机制。如果采用表锁,将会将整个表中的所有数据全部锁定,其他商品的无法无法购买,很大程度上影响购买的速度。所以,MySQL 又为我们提供了一个行级锁,购买的是那件商品就锁住该商品的数据,这样做既保证安全又降低了复杂度。

行级锁中的死锁(Dead Lock)现象

死锁,并不是 MySQL 数据库提供的一种锁机制,而是在使用 MySQL 数据库锁机制的过程中出现的因争夺锁资源而导致一直处于等待阻塞状态的一种错误现象。表锁是不可能出现死锁现象的,死锁只产生于行锁之间

造成了事务交叉,进而就会造成死锁现象

在实际应用过程中,当需要多个锁时,要尽可能地把那些可能引发锁冲突的锁进行拆分或者往后放,进而最大限度地避免锁冲突

行锁升级

在一些情况下行级锁会升级成为表级锁:

  • 当未命中索引时,行级锁会升级成表级锁。
  • 当更新所有数据时,会从行级锁升级成表级锁。

总结

全局锁主要用于需要同时操作整个数据中所有数据的情况。一旦加上了全局锁之后,整个数据将处于全局只读状态,也就意味着主库无法写,从库无法通过 Binlog实现主从同步,所以非必需场景下,慎用全局锁。

表锁主要分为两个,分表是表锁和元数据锁 ,加了表锁该表的操作将会受到限制。当有查询数据未使用到索引时全表更新以及多表事务级联时就会自动加表锁,所以在使用 SQL 时一定要避免操作大量数据以及跨表事务。在实际的应用中,我不太建议你使用表锁,一般只有在不支持行锁的存储引擎中才会使用表锁。

行锁:所有某一行数据,是粒度细的锁技术,行级锁分为两种分别是共享锁和排他锁。

  • 共享锁:主要是用来解决数据一致性问题的。加上共享锁之后,该条数据在其他事务中只读但不可以修改。
  • 排他锁:就像它的名字一样,排斥其他的锁机制,也就是说加上排他锁之后,将无法再添加任何其他锁。

# 优化