数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
锁种类
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁、行锁、间隙锁。
全局锁
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。(延伸一下,还有没有别的备份方式,有什么问题)
表级锁
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write。与FTWRL类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
元数据锁 MDL 不需要显式使用,MDL 的作用是,保证读写的正确性。A 线程在对 x 表做查询的同时,B 线程执行了 DDL 语句修改了这个 x 表的结构,不就 gg 了。
因此,在 MySQL 5.5版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。读锁间不互斥,读写之间、写锁之间互斥。
但是 MDL 有一个潜在的问题,读写之间互斥,对于大表或者热表会导致线程阻塞,现在新的版本支持 DDL NOWAIT/WAIT n 语法。
行锁
MySQL的行锁是在引擎层由各个引擎自己实现的,MyISAM 引擎就不支持行锁,InnoDB 是支持行锁的。
行锁就是针对数据表中行记录的锁。这很好理解,比如事务 A 更新了一行,而这时候事务 B 也要更新同一行,则必须等事务 A 的操作完成后才能进行更新。
例子:
┌————————————————————————————┬————————————————————————————┐
事务A 事务B
├————————————————————————————┼————————————————————————————┤begin; updata t set is_deleted = 1 where id = 1;updata t set is_deleted = 1 where id = 2;
├————————————————————————————┼————————————————————————————┤
begin;
updata t set is_deleted = 1 where id = 1;
├————————————————————————————┼————————————————————————————┤commit;
└————————————————————————————┴————————————————————————————┘
事务 B 的 update 语句会被阻塞,直到事务 A 执行 commit 之后,事务 B 才能继续执行。
由此可见日常开发中如果在一个事务中,要锁多个行,要把最可能锁冲突的行放在后面。
死锁和死锁检测
例子:
┌————————————————————————————┬————————————————————————————┐
事务A 事务B
├————————————————————————————┼————————————————————————————┤begin; begin;updata t set is_deleted = 1 where id = 1;
├————————————————————————————┼————————————————————————————┤
updata t set is_deleted = 1 where id = 2;
├————————————————————————————┼————————————————————————————┤updata t set is_deleted = 1 where id = 2;
├————————————————————————————┼————————————————————————————┤
updata t set is_deleted = 1 where id = 1;
└————————————————————————————┴————————————————————————————┘
解决死锁问题有两个策略
1、等待直至超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
2、发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on,表示开启这个逻辑。
超时等待业务无法接受,死锁检测浪费 cpu 性能。
对于热点行,解决死锁检测导致的 cpu 占用高的问题,一个是业务保证一定不会出现死锁关闭死锁检测,另一个是业务层面是做并发度控制,控制同一行的数据操作在 DB 层的并发数减少死锁检测的消耗。
间隙锁
是 Innodb 在可重复读提交下为了解决幻读问题时引入的锁机制,在 mvcc 下幻读出现的根本原因是,读视图和当前读的策略不同。对于普通已有数据的查询操作 mvcc 读视图可以解决幻读问题,但是对于当前读却不能解决幻读问题。增、删、改操作操作是当前读的。
Innodb 为了解决当前读的幻读问题,使用了 next-key lock 算法,Innodb行锁在搜索或者扫描表索引时,会在遇到的索引记录上设置共享锁或者排它锁,因此行锁实际是索引记录锁。另外, 在索引记录上设置的锁同样会影响索引记录之前的“间隙(gap)”。即next-key lock是索引记录行加上索引记录之前的“gap”上的间隙锁定。
例子:无间隙锁,产生幻读
┌—————————————————————————————┬——————————————————————————————┐
事务A 事务B
├—————————————————————————————┼——————————————————————————————┤begin; begin;INSERT INTO t (id,name) VALUES (1, 'a');commit;
├—————————————————————————————┼——————————————————————————————┤select * from t;INSERT INTO t (id,name) VALUES (1, 'b');
└—————————————————————————————┴——————————————————————————————┘
例子:SELECT * FROM t WHERE id > 100 FOR UPDATE;可以使间隙锁生效。
┌—————————————————————————————┬——————————————————————————————┐
事务A 事务B
├—————————————————————————————┼——————————————————————————————┤begin; begin;select * from t where id > 1 for update;
├—————————————————————————————┼——————————————————————————————┤INSERT INTO t (id,name) VALUES (1, 'a');
├—————————————————————————————┼——————————————————————————————┤INSERT INTO t (id,name) VALUES (1, 'b');
└—————————————————————————————┴——————————————————————————————┘
间隙锁存在的问题,在业务查询中会不经意间用到大范围查询,就如例子中的 id > 100 会锁定 100 以外的间隙,导致所有 100 以外的新增全部阻塞影响性能。
