全局锁
全局锁是对整个数据库实例加锁,典型应用场景是做全库逻辑备份。
备份为什么要加锁?因为如果不加锁的话,备份得到的库不是一个逻辑时间点,备份的视图是逻辑不一致的。
flush tables with read lock;
这个命令就让整个库处于只读状态。
mysqldump配合–single-transaction,通过可重复读也可以用来备份,前提是存储引擎要支持事务和对应的隔离级别。
set global readonly=true
也可以达到全库只读的状态,但是不建议使用。
表级锁
表锁
lock tables ... read;
lock tables ... write;
表锁一般是在数据库引擎不支持行锁的时候才会被用到。
元数据锁(meta data lock, MDL)
MDL不需要你显式使用,对表做增删改查时会自动加MDL读锁,对表结构做变更时会自动加MDL写锁。
MDL读锁之间不互斥,因此可以有多个线程同时对一张表增删改查。
MDL读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,因此如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
行锁
行锁是存储引擎层实现的,不是所有存储引擎都支持行锁,比如MyISAM就不支持行锁,不支持行锁就得使用表锁,同一张表任何时刻只能有一个更新在执行,这就大大限制了业务并发度。
InnoDB的行锁在需要的时候自动加,但并不是不需要了就立刻释放,而是在事务提交时释放,这就是两阶段锁协议。因此如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的更新往后放。
间隙锁
为了解决幻读,InnoDB引入了间隙锁(Gap Lock),间隙锁锁的是两个值之间的空隙,也就是说,在扫描行的过程中,不仅加了行锁,还给行两边的空隙加上了间隙锁。间隙锁和行锁合称next-key lock,每个next-key lock是前开后闭区间。
注意,间隙锁是在可重复读隔离级别下才会生效的。如果使用读提交,就没有间隙锁了,但要注意可能出现的数据和日志不一致问题,需要把binlog格式设置为row.
简单了解的加锁规则:
- 加锁的基本单位是next-key lock
- 查询过程中访问到的对象才加锁
- 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁
- 索引上的等值查询,如果是唯一索引,next-key lock退化为行锁
注意,这里等值查询指的是通过树搜索的方式定位记录。
死锁和死锁检测
当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,即死锁。
出现死锁以后,一种策略是等待,直到超时,默认超时时间是50秒。
show variables like 'innodb_lock_wait_timeout';
这个超时时间不能设置的太小,因为可能不是死锁,而只是锁等待,超时时间设置的太小的话会有很多误伤。
另一种策略是开启死锁检测,检测到死锁后回滚某一个事务,让其他事务得以继续执行。死锁检测默认是开启的。
show variables like 'innodb_deadlock_detect';
死锁检测的问题是可能占用大量CPU资源,尤其是热点行的高并发更新时。表现出来的现象就是CPU利用率飙高,但每秒执行不了几个事务,因为CPU资源都消耗在死锁检测上了。
那么如何解决热点行更新问题呢?最根本的办法是控制好并发度,在数据库中间件或数据库源码里增加限流控制。如果没有这样技术手段,可以将针对一行的逻辑改成多行。