浅谈MySQL中的锁

时间:2022-07-22
本文章向大家介绍浅谈MySQL中的锁,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

根据加锁范围MySQL中的锁可以分为全局锁,表级锁以及行级锁。

全局锁

全局锁是对整个数据库进行加锁的,执行Flush table with read lock对整个数据库加锁,执行之后会使得整个库处于只读状态,数据更新语句,数据定义语句以及更新类事务的提交语句都会被阻塞。使用 unlock tables解锁。

例如对数据库先执行Flush table with read lock,另一进程进行查询插入操作的测试,

学生表如下:

CREATE TABLE `t_student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

执行Flush table with read lock之后,

执行查询语句结果如下:

mysql> select * from t_student;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
+----+--------+------+
1 row in set (0.00 sec)

发现查询时不受影响的。

执行插入语句结果如下:

mysql> insert into t_student(name,age) values('李四',19);

该线程就被阻塞住了直到我们执行解锁语句unlock tables

mysql> insert into t_student(name,age) values('李四',19);
Query OK, 1 row affected (1 min 14.16 sec)

mysql>

全局锁一个典型的使用场景为做全库备份。

但是对于支持事务的引擎(例如Innodb)时,可以在可重复读的隔离级别下开启一个事务,当 mysqldump 使用参数–single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图,后续的备份只需对拿到的视图备份即可。

此外使全库只读还有set global readonly=true的方式,但是不建议使用:

1)、某些系统中会把readonly用于其他逻辑判断,例如判断主库还是备库。

2)、FTWRL加锁后,若客户端发生异常,该锁会自动释放,而set global readonly=true的方式客户端发生异常后,整个库还处于只读状态。

表级锁

表级的锁有两种,一种是表锁一种是元数据锁(meta data lock,MDL)。

表锁的语法:lock tables table_name read/write, 其也可以使用unlock tables进行解锁。

需要注意的是lock tables加锁后除了会限制其他线程的读写外还会限制本线程的读写。

例如:

线程1执行如下sql

lock tables t_student read, t_book write;

之后其他线程只能读t_student,对t_book既不能读又不能写。

当前线程对t_student也只允许读。

另一线程执行查询结果如下,对t_book的查询对t_student的写入都被阻塞住了

mysql> select * from t_student;
+----+--------+------+
| id | name   | age  |
+----+--------+------+
|  1 | 张三   |   18 |
|  2 | 李四   |   19 |
+----+--------+------+
2 rows in set (0.00 sec)

mysql> select * from t_book;

mysql> insert t_student(name,author) values('三国演义','罗贯中');

当前线程对t_student执行写操作结果如下:

mysql> insert into t_student(name,age) value('王五',22);
ERROR 1099 (HY000): Table 't_student' was locked with a READ lock and can't be updated

另一种表级锁为meta data lock(MDL),MDL不需要显式使用,访问表时会被自动加上。对表做增删改查时加MDL读锁,对表做结构变更时加MDL写锁。

读锁之间不互斥:因此可以多个线程并发对一张表进行增啥改查;

读写锁互斥,写锁之间互斥,用来保证变更表结构的安全性。

行级锁

MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁,不支持行锁使得MyISAM的并发控制只能使用表锁,这也是InnoDB替代MyISAM的一个重要原因。

行锁顾名思义,针对数据表的行记录建立的锁,如果线程1更新这一行,线程二也要更新这一行,如此只有等线程一的事务提交后,线程二才能更新

两阶段锁协议:

在InnoDB中,行锁是在需要时添加的,并不是不需要时就立即释放,只有当事务提交时才进行释放。

该图来自MySQL实战45讲

如上图所示事务A执行update t set k = k + 1 where id = 1;时就会对id=1这行记录加行锁,就算这条语句执行完了还不能将行锁释放,此时事务B的更新操作会被阻塞,直到事务A提交了事务。

知道了两阶段锁的这个特性后,对于一条事务中多条跟新语句其会锁多行,我们可以通过改变其顺序(令竞争激烈并发度高的那些可能造成锁冲突的语句往后放)达到提升并发度的效果,例如:

电影院卖票这种场景

开启事务
用户余额减少一张电影票价
影院余额增加一张电影票价
添加一条购买记录
提交事务

经过分析我们得知给影院余额增加一张票价这条语句的跟容易造成锁冲突,例如多个用户同时购票,因此将其放到最后即

开启事务
添加一条购买记录
用户余额减少一张电影票价
影院余额增加一张电影票价
提交事务

保证拿到锁后很快释放。

死锁与死锁检测

例如如下场景

事务1                              事务2
update ...where id = 1            update ...where id = 2
update ...where id = 2            update ...where id = 1
提交事务                            提交事务

事务1先拿到id=1这一行的行锁,事务2先拿到id=2这一行的行锁,此时事务1由于拿不到id = 2这一行的锁,他就阻塞住自己直到事务2释放了id=2的行锁,阻塞阶段其并不会释放自己手里的id=1这一行的锁。事务2同理等事务1释放id=1的行锁,因此就进入了死锁状态。

出现死锁的两种解决方式;

1)、直接进入等待,直到超时,超时参数innodb_lock_wait_timeout默认为50s。

2)、发起死锁检测,发现死锁后立即回滚循环等待链中某一个事务,让其他事务先执行。将参数 innodb_deadlock_detect 设置为 on即可,该参数默认值亦为on。

方式二会导致每个新来阻塞住的线程都会判断其是否出现死锁,对于正常的阻塞亦会执行该过程,该过程是一个O(N)时间复杂度的操作,如此会极大得消耗cpu资源。

对于热点行更新的性能问题的可以对于相同行的更新,在进入引擎之前排队。这样在 InnoDB 内部就不会有大量的死锁检测工作了。