在之前的文章有多次提到,MySQL在数据更新和性能优化上会用到锁机制。我们在实际的应用中也经常会遇到锁相关的问题,即使很多时候我们并没有人为的为数据库添加锁,但还是会出现死锁的问题,这是因为在我们操作数据时MySQL隐式的帮我们加了锁。这篇文章主要讲一下MySQL锁的类型和锁与锁之间的关联。
锁类型
按照操作类型划分:读锁、写锁。
1. 读锁
读锁也叫共享锁(S 锁),它是一种读共享写阻塞的锁,当对表里的某一行数据添加S锁时,其他事务对其写时会阻塞,但可以对它进行读操作。同时其他事务可以对该数据添加S锁,但不能添加X锁。因此S锁和S锁可兼容,S锁和X锁不能兼容。
2. 写锁
写锁也叫排他锁(X 锁),它是一种对任何锁都阻塞的锁,当对表里的某行数据添加X锁时,其他事务时不允许再对它添加S锁和X锁。
3. 意向锁
意向锁是表级锁,又分为意向读锁(IS 锁)和意向写锁(IX 锁),它是基于在InnoDB存储引擎下的一种锁,意向锁无法手工添加,是由存储引擎自动添加的。
当事务操作需要获取数据行的锁时,首先需要先获取表对应的意向锁。当需要对表添加表级的锁时,会判断是否存在意向锁,而不用每一行判断是否有行锁,这样可以大大提高数据库的性能。
意向锁之间是相互兼容的,它们相互之间不会阻塞对方获取锁。
按照颗粒度划分:表锁、行锁、间隙锁
1. 表锁
针对表操作的锁,MyISAM存储引擎下目前只支持表锁。对表进行加锁,加锁速度快、锁的颗粒大、不会出现死锁。但是容易出现锁冲突,性能要低,容易造成阻塞,并发度低。
2. 行锁
针对数据行操作的锁,InnoDB存储引擎下支持表锁和行锁。加锁的速度慢需要查找到对应的数据行,锁颗粒度小,容易出现死锁,并发度高。当需要对数据进行检索操作时最好能够使用索引,避免全表扫描,如果是全表扫描那么行锁即变为表锁。
3. 页锁
针对数据页操作的锁,是基于BerkeleyDB存储引擎的锁,锁定的颗粒度介于行锁与表锁之间,所以它的并发度和资源的开销也是介于行锁与表锁之间。
4. 间隙锁
当检索条件为范围检索,并对检索的结果进行加锁,InnoDB会对当前检索出来已有的数据加锁,并对检索范围内不存在记录的间隙也加锁,这就叫间隙锁。
举例:
user表:
id | name | age |
1 | 张三 | 18 |
2 | 李四 | 30 |
1. 当使用精确检索时
事务1:
select * from user where age=18 and age=30 for update;
事务2:
insert into user(name,age) values('王五',20);
这种情况下事务1精确检索,只会对这两行数据进行加锁,因此事务2可正常执行。
2. 当使用范围检索时
事务1:
select * from user where age>=18 and age<=30 for update;
事务2:
insert into user(name,age) values('王五',20);
事务1使用范围检索,InnoDB会对这两行数据进行加锁同时也会对18到30之间的间隙也加锁,因此事务2将会阻塞不可执行。
间隙锁的引入是为了解决在RR隔离级别的幻读问题。
锁之间兼容性
共享锁 | 排他锁 | 意向共享锁 | 意向排他锁 | |
共享锁 | 兼容 | 互斥 | 兼容 | 互斥 |
排他锁 | 互斥 | 互斥 | 互斥 | 互斥 |
意向共享锁 | 兼容 | 互斥 | 兼容 | 兼容 |
意向排他锁 | 互斥 | 互斥 | 兼容 | 兼容 |
从以上表格中可以看出排他锁与任何锁都不兼容,共享锁只与共享锁兼容,意向锁之间都兼容。
死锁
死锁是指多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将一直处于等待状态。
可以通过以下语句查看锁的状态,最终找到相应产生死锁相应的进程,人工kill该进程释放锁的状态。
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
总结
MySQL中存在不同颗粒度的锁,从小到大有,行锁、间隙锁、页锁、表锁,锁的颗粒度越大消耗的资源越小,但并发度越低。不同的存储引擎对锁的实现也不一样。InnoDB支持行锁、间隙锁和表锁,MyISAM只支持表锁,BerkeleyDB支持页锁和表锁。
锁的添加是需要消耗资源的,如果锁添加的越多性能就会相应的降低,同时也会出现锁冲突或者是死锁的情况,所以在实际的应用中根据实际情况加锁,不要盲目的添加从而造成系统性能降低甚至崩溃。