锁就是协调多个用户或者客户端并发访问某一资源的机制,保证数据并发访问时的一致性和有效性。
全局锁
MySQL 全局锁会关闭所有打开的表,并使用全局读锁锁定所有表。
FLUSH TABLES WITH READ LOCK;
UNLOCK TABLES;
当执行 FTWRL 后,所有的表都变成只读状态,数据更新或者字段更新将会被阻塞。
场景
一般用在整个库(包含非事务引擎表)做备份(mysqldump 或者 xtrabackup)时。
mysqldump 包含一个参数
--single-transaction
,可以在一个事务中创建一致性快照,然后进行所有表的备份。因此增加这个参数的情况下,备份期间可以进行数据修改。但是需要所有表都是事务引擎表。所以建议使用InnoDB 存储引擎。
表级锁
表级锁有两种:表锁和元数据锁。
表锁
场景
- 事务需要更新某张大表的大部分或全部数据。如果使用默认的行锁,不仅事务执行效率低,而且可能造成其它事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高事务执行速度;
- 事务涉及多个表,比较复杂,可能会引起死锁,导致大量事务回滚,可以考虑表锁避免死锁。
lock tables t14 read;
lock tables t14 write;
表读锁本线程和其它线程可以读,本线程写会报错,其它线程写会等待。
元数据锁
MDL不需要显式使用,在访问一个表的时候会被自动加上。
MDL的作用是,保证读写的正确性。MDL 锁的出现解决了同一张表上事务和 DDL 并行执行时可能导致数据不一致的问题。
对开发而言尽量避免慢查询,事务要及时提交,避免大事务。
对于 DBA 来说,也应该尽量避免在业务高峰执行 DDL 操作。并且DDL期间需要避免长事务。
在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
行锁
- InnoDB 支持事务:适合在并发条件下要求数据一致的场景。
- InnoDB 支持行锁:有效降低由于删除或者更新导致的锁定。
两阶段锁
行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
行锁
两种类型的行锁:
- 共享锁(S):允许一个事务去读一行,阻止其它事务获得相同数据集的排他锁;
- 排他锁(X):允许获得排他锁的事务更新数据,阻止其它事务取得相同数据集的共享读锁和排他写锁。
共享锁(S):select * from table_name where … lock in share mode;
排他锁(X):select * from table_name where … for update(当前读)
。
行锁算法
Record Lock:单个记录上的索引加锁。
Gap Lock:间隙锁,对索引项之间的间隙加锁,但不包括记录本身。
Next-Key Lock:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身。
加锁规则
5.x系列<=5.7.24,8.0系列 <=8.0.13
- 原则1:加锁的基本单位是next-key lock,next-key lock是前开后闭区间。
- 原则2:查找过程中访问到的对象才会加锁。
- 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
- 优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
- 一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
加锁分析
非索引字段查询(RC)
如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由 server 层进行过滤。
唯一索引查询(RC)
如果查询的条件是唯一索引,那么 SQL 需要在满足条件的唯一索引上加锁,并且会在对应的聚簇索引上加锁。
非唯一索引查询(RC)
如果查询的条件是非唯一索引,那么 SQL 需要在满足条件的非唯一索引上都加上锁,并且会在它们对应的聚簇索引上加锁。
非索引字段查询(RR)
RR 隔离级别下,非索引字段做条件的当前读不但会把每条记录都加上 X 锁,还会把每个 GAP 加上GAP 锁。(条件字段加索引的重要性!)
唯一索引查询(RR)
如果能确保索引字段唯一,那其实一个等值查询,最多就返回一条记录,而且相同索引记录的值,一定不会再新增,因此不会出现 GAP 锁。
以唯一索引为条件的当前读,不会有 GAP 锁。
非唯一索引查询(RR)
新增GAP锁+对应数据的X锁
悲观锁
借助数据库锁机制在修改数据之前锁定,再修改的方式被称为悲观并发控制。
优点:利用锁机制保证了数据的顺序执行,不需要自己控制,加锁、释放完全由数据库代劳
缺点:一旦一个事务获取了锁,其他的事务必须等待,势必会影响系统的吞吐量
适用场景:写入操作比较频繁的场景
乐观锁
假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据冲突与否进行检测。
优点:由于不需要加锁,其他的事务可以同时操作数据,相比于悲观锁,系统吞吐量会提高
缺点:锁机制,如果并发度较高,失败重试的情况会成为系统瓶颈
适用场景:读取操作比较频繁的场景
锁定位
1、show processlist,查看state
死锁
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。
“唯一键”引起的死锁
会话 A获取了排它锁开始插入,之后的事务(“会话 B”,“会话 C”)再去执行时会出现 Duplicate Key(重复的值)问题,此时它们都会去申请该行记录的共享锁。如果这个时候,占据排它锁的事务出现回滚(“会话 A”),另外的两个事务会同时去申请排它锁。但是,在数据库中,排它锁和共享锁是互斥资源,也就导致了死锁。
之所以在出现 Duplicate Key 时会加上共享锁,是因为冲突检测是读操作,所以,冲突之后的轮询仍然会有共享限制。
解决方法
- 检测到死锁的循环依赖,立即返回一个错误,将参数 innodb_deadlock_detect设置为 on 表示开启这个逻辑;
- 等查询的时间达到锁等待超时的设定后放弃锁请求。这个超时时间由 innodb_lock_wait_timeout 来控制。默认是50 秒。
方案1有额外的CPU检测开销,确保无死锁时建议关闭检测。或者将一行改成逻辑上的多行来是控制并发度,减少锁冲突。以影院账户为例,可以考虑放在多条记录上,比如10个记录,影院的账户总额等于这10个记录的值的总和。
降低死锁概率
- 更新 SQL 的 where 条件尽量用索引;
- 基于 primary 或 unique key 更新数据;
- 减少范围更新,尤其非主键、非唯一索引上的范围更新;
- 加锁顺序一致,尽可能一次性锁定所有需要行;
- 将 RR 隔离级别调整为 RC 隔离级别。
分析死锁
SHOW FULL PROCESSLIST; //State字段,waiting for ... lock
show engine innodb status\G; //查看最后一次死锁信息
另外设置 innodb_print_all_deadlocks = on 可以在 err log 中记录全部死锁信息。
INNODB_TRX 表记录了当前处于运行状态的所有事务,包含非常详细的信息,例如:事务是否正在等待一个锁、事务是否正在执行等等。
INNODB_LOCKS 记录的是 InnoDB 事务去请求但没有获取到的锁信息和事务阻塞其他事务的锁信息。
INNODB_LOCK_WAITS记录了事务的锁等待状态。