系列文章目录
第1章 MySQL系列文章之子查询
第2章 MySQL系列文章之表的操作和约束
第3章 MySQL系列文章之表的视图和存储过程
第4章 MySQL系列文章之逻辑架构
第5章 MySQL系列文章之存储引擎
第6章 MySQL系列文章之索引的数据结构
第7章 MySQL系列文章之索引的创建与设计原则
第8章 MySQL系列文章之索引的性能分析工具的使用
第9章 MySQL系列文章之索引优化与查询优化
第10章 MySQL系列文章之数据库其它调优策略
第11章 MySQL事务和事务日志
第12章 MySQL的锁
第13章 MySQL的多版本并发控制
第14章 MySQL日志和主从复制
第15章 基于Docker的MySQL备份
文章目录
一、锁的概述
为保证数据的一致性,需要对并发操作进行控制,因此产生了锁。
同时锁机制也为实现MySQL的各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素。
1.1、并发问题的两种解决方案
1、读操作利用MVCC(多版本并发控制),写操作用加锁
2、读写操作都用加锁
1.2、锁的分类
![](https://img-blog.csdnimg.cn/fddfbdd1c078412c9e7b5e75f48fbd9b.png)
二、表级锁
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
2.1、表锁的S锁、X锁
共享锁(S锁)用法:
LOCK TABLE table_name [ AS alias_name ] READ
排它锁(X锁)用法:
LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE
解锁用法:
unlock tables;
当前事务上锁后:
![](https://i-blog.csdnimg.cn/blog_migrate/3459b059d958c0455e5dcf89eec2f4db.png)
2.2、意向锁(intention lock)
略
2.3、自增锁(AUTO-INC锁)
可以为表的某个列添加AUTO_INCREMENT 属性。
innodb_autoinc_lock_mode = 0(“传统”锁定模式)
innodb_autoinc_lock_mode = 1(“连续”锁定模式)
innodb_autoinc_lock_mode = 2(“交错”锁定模式)
2.4、元数据锁(MDL锁)
如果一个查询在遍历一张表,另一个线程来改变表结构,肯定会出现问题。所以,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
三、页级锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁,会出现死锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁。
四、行级锁
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。
4.1、记录锁(Record Locks)
共享锁用法(S锁 读锁):
若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A。其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
select ... lock in share mode;
#或 高版本中
select ... for share;
共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
排它锁用法(X 锁 写锁):
若事务T对数据对象A加上X锁,事务T可以读A也可以修改A。其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
select ... for update
排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
4.2、间隙锁(Gap Locks)
gap锁的提出仅仅是为了防止插入幻影记录(幻读)而提出的。 MySQL 在REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC 方案解决,也可以采用加锁方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks 的锁。
Gap Locks 示意图:
![](https://i-blog.csdnimg.cn/blog_migrate/48901c6b4d268a004a40776877f6909d.png)
图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。会阻塞插入操作,直到拥有这个gap锁的事务提交了之后。
Gap Locks例子:
mysql> select * from product_copy;
+----+--------+-------+-----+
| id | name | price | num |
+----+--------+-------+-----+
| 1 | 伊利 | 68 | 1 |
| 2 | 蒙牛 | 88 | 1 |
| 6 | tom | 2788 | 3 |
| 10 | 优衣库 | 488 | 4 |
+----+--------+-------+-----+
其中id为主键 num为普通索引
窗口A:
mysql> BEGIN;
mysql> select * from product_copy where num=3 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 6 | tom | 2788 | 3 |
+----+------+-------+-----+
1 row in set
窗口B:
mysql> insert into product_copy values(5,'kris',1888,2);
这里会等待 直到窗口A commit才会显示下面结果
Query OK, 1 row affected
但是下面是不需要等待的
mysql> update product_copy set price=price+100 where num=1;
Query OK, 2 rows affected
Rows matched: 2 Changed: 2 Warnings: 0
mysql> insert into product_copy values(5,'kris',1888,5);
Query OK, 1 row affected
通过上面的例子可以看出Gap 锁的作用是在1,3的间隙之间加上了锁。而且并不是锁住了表,我更新num=1,5的数据是可以的.可以看出锁住的范围是(1,3]。
4.3、临键锁(Next-Key Locks)
有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁。Next-Key Locks是在存储引擎innodb 、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。next-key锁也包含gap锁
begin;
select * from student where id <=8 and id > 3 for update;
4.4、插入意向锁(Insert Intention Locks)
插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种gap锁。
插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁。如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks ,官方的类型名称为:
LOCK_INSERT_INTENTION ,我们称为插入意向锁。
五、乐观锁和悲观锁
这两种锁是两种看待数据并发的思维方式。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想。
5.1、乐观锁
乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁。但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者CAS机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。
适用场景:读操作多的场景。优点在于程序实现, 不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
-
乐观锁的版本号机制
在表中设计一个版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行
UPDATE … SET version=version+1 WHERE version=version 。
此时如果已经有事务对这条数据进行了更改,修改就不会成功。 -
乐观锁的时间戳机制
时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。
5.2、悲观锁
悲观锁是一种思想,顾名思义很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronized 和ReentrantLock 等独占锁就是悲观锁思想的实现。
适用场景:写操作多的场景。在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。
六、死锁原理
两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
比如:
![](https://i-blog.csdnimg.cn/blog_migrate/ba50ebc238fe7f3c4ba01902d98d6fd0.png)
事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有两种策略:
-
直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
-
发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数innodb_deadlock_detect 设置为on ,表示开启这个逻辑。
参考:
1、MySQL高级特性篇-宋红康
2、https://blog.csdn.net/qq_38238296/article/details/88362999