文章目录
- 🔥MySQL事务-MySQL中锁的分类
- 🔥MySQL事务-MySQL中的死锁问题
🔥MySQL事务-MySQL中锁的分类
MySQL中锁的分类
从本质上讲,锁是一种协调多个进程或多个线程对某一资源的访问的机制,MySQL使用锁和MVCC机制实现了事务隔离级别。
锁的分类
悲观锁和乐观锁
悲观锁
顾名思义,悲观锁对于数据库中数据的读写持悲观态度, 即在整个数据处理的过程中,它会将相应的数据锁定。在数据库中,悲观锁的实现需要依赖数据库提供的锁机制,以保证对数据库加锁后,其他应用系统无法修改数据库中的数据。
注意:
在悲观锁机制下,读取数据库中的数据时需要加锁,此时不能对这些数据进行修改操作。修改数据库中的数据时也需要加锁,此时不能对这些数据进行读取操作。
乐观锁
悲观锁会极大地降低数据库的性能,特别是对长事务而言,性能的损耗往往是无法承受的。乐观锁则在一定程度上解决了这个问题。
注意⭐:
实现乐观锁的一种常用做法是为数据增加一个版本标识,如果是通过数据库实现,往往会在数据表中增加一个类似version的版本号字段。
读锁和写锁
读锁
读锁又称为共享锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
写锁
写锁又称为排他锁,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
⭐需要注意的是,对同一份数据,如果加了读锁,则可以继续为其加读锁,且多个读锁之间互不影响,但此时不能为数据增加写锁。一旦加了写锁,则不能再增加写锁和读锁。因为读锁具有共享性,而写锁具有排他性。
表锁、行锁和页面锁
表锁
表锁也称为表级锁,就是在整个数据表上对数据进行加锁和释放锁。典型特点是开销比较小,加锁速度快,一般不会出现死锁,锁定的粒度比较大,发生锁冲突的概率最高,并发度最低。
手动增加表锁
mysql> lock table userinfo read;
Query OK, 0 rows affected (0.00 sec)
mysql> lock table userinfo write;
Query OK, 0 rows affected (0.00 sec)
查看数据表上增加的锁
show open tables;
删除表锁
行锁
行锁也称为行级锁,就是在数据行上对数据进行加锁和释放锁。典型特点是开销比较大,加锁速度慢,可能会出现死锁,锁定的粒度最小,发生锁冲突的概率最小,并发度最高。
⭐行锁主要加在索引上,如果对非索引的字段设置条件进行更新,行锁可能会变成表锁。
⭐InnoDB的行锁是针对索引加锁,不是针对记录加锁,并且加锁的索引不能失效,否则行锁可能会变成表锁。
⭐锁定某一行时,可以使用lock in share mode命令来指定共享锁,使用for update命令来指定排他锁,例如下面的SQL语句。
select * from userinfo where id = 1 for update;
页面锁
页面锁也称为页级锁,就是在页面级别对数据进行加锁和释放锁。对数据的加锁开销介于表锁和行锁之间,可能会出现死锁,锁定的粒度大小介于表锁和行锁之间,并发度一般。
间隙锁和临键锁
间隙锁
在MySQL中使用范围查询时,如果请求共享锁或排他锁,InnoDB会给符合条件的已有数据的索引项加锁。如果键值在条件范围内,而这个范围内并不存在记录,则认为此时出现了“间隙(也就是GAP)”。InnoDB存储引擎会对这个“间隙”加锁,而这种加锁机制就是间隙锁(GAP Lock)。
例如,userinfo数据表中存在如下数据。
此时,userinfo数据表中的间隙包括id为(3,15]、(15,20]、
(20,正无穷]的三个区间。如果执行如下命令,将符合条件的用户的账户余额增加100元。
update userinfo set balance = balance + 100 where id > 5
and id <16;
则其他事务无法在(3,20]这个区间内插入或者修改任何数据。这里需要注意的是,间隙锁只有在可重复读事务隔离级别下才会生效。
临键锁
临键锁(Next-Key Lock)是行锁和间隙锁的组合,例如上面例子中的区间(3,20]就可以称为临键锁。
🔥MySQL事务-MySQL中的死锁问题
什么是死锁
死锁是并发系统中常见的问题,同样也会出现在数据库MySQL的并发读写请求场景中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或因为加锁顺序不一致造成循环等待锁资源,就会出现“死锁”。
Deadlock found when trying to get lock...
举例来说 A 事务持有 X1 锁 ,申请 X2 锁,B事务持有 X2 锁,申请X1 锁。A 和 B 事务持有锁并且申请对方持有的锁进入循环等待,就造成了死锁。
出现死锁的几个要素:
⭐两个或者两个以上事务
⭐每个事务都已经持有锁并且申请新的锁
⭐锁资源同时只能被同一个事务持有或者不兼容
⭐事务之间因为持有锁和申请锁导致彼此循环等待
第一步
打开终端A,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | Java | 100.00 |
| 2 | MySQL | 200.00 |
+----+-------+---------+
2 rows in set (0.00 sec)
第二步
打开终端B,登录MySQL,将事务隔离级别设置为可重复读,开启事务后为userfinfo数据表中id为2的数据添加排他锁,如下所示。
mysql> set session transaction isolation level
repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from userinfo where id = 2;
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 2 | MySQL | 200.00 |
+----+-------+---------+
1 row in set (0.00 sec)
第三步
在终端A为userinfo数据表中id为2的数据添加排他锁,如下所示。
⭐此时,线程会一直卡住,因为在等待终端B中id为2的数据释放排他锁。
第四步
在终端B中为userinfo数据表中id为1的数据添加排他锁,如下所示。
mysql> select * from userinfo where id =1 for
update;
ERROR 1213 (40001): Deadlock found when trying
to get lock; try restarting transaction
通过如下命令可以查看死锁的日志信息。
show engine innodb status\G
⭐通过命令行查看LATEST DETECTED DEADLOCK选项相关的信息,可以发现死锁的相关信息,或者通过配置
innodb_print_all_deadlocks(MySQL 5.6.2版本开始提供)参数为ON,将死锁相关信息打印到MySQL错误日志中。
如何避免死锁
⭐合理的设计索引,区分度高的列放到组合索引前面,使业务SQL 尽可能通过索引定位更少的行,减少锁竞争 。
⭐调整业务逻辑 SQL 执行顺序, 避免 update/delete 长时间持有锁的 SQL 在事务前面。
⭐合理设计索引,尽量缩小锁的范围。
⭐尽量减少查询条件的范围,尽量避免间隙锁或缩小间隙锁的范围。
⭐尽量控制事务的大小,减少一次事务锁定的资源数量,缩短锁定资源的时间。
⭐如果一条SQL语句涉及事务加锁操作,则尽量将其放在整个事务的最后执行。
⭐优化 SQL 和表设计,减少同时占用太多资源的情况。比如说,减少连接的表 ,将复杂 SQL 分解 为多个简单的 SQL。