数据库锁设计的初衷是处理并发问题。作为多用户共享的资源,当出现并发访问的时候,数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。
根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类。
全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本。
让整库都只读,听上去就很危险:
官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
对于全部是InnoDB引擎的库,我建议你选择使用–single-transaction参数,对应用会更友好。
一致性读是好,但前提是引擎要支持这个隔离级别。比如,对于MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL命令了。
既然要全库只读,为什么不使用set global readonly=true的方式呢?确实readonly方式也可以让全库进入只读状态,但我还是会建议你用FTWRL方式,主要有两个原因:
MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。
表锁的语法是 lock tables … read/write。与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
而对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。
另一类表级的锁是MDL(metadata lock)。MDL不需要显式使用,在访问一个表的时候会被
自动加上。MDL的作用是,保证读写的正确性。
在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当
要对表做结构变更操作的时候,加MDL写锁。
我们可以看到session A先启动,这时候会对表t加一个MDL读锁。由于session B需要的也是
MDL读锁,因此可以正常执行。
之后session C会被blocked,是因为session A的MDL读锁还没有释放,而session C需要MDL写锁,因此只能被阻塞。
如果只有session C自己被阻塞还没什么关系,但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。前面我们说了,所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了。
如何安全地给小表加字段?
首先我们要解决长事务,事务不提交,就会一直占着MDL锁。
如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?
在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者DBA再通过重试命令重复这个过程。
MariaDB已经合并了AliSQL的这个功能,所以这两个开源分支目前都支持DDL NOWAIT/WAIT n这个语法。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁和死锁检测
当出现死锁以后,有两种策略:
在InnoDB中,innodb_lock_wait_timeout的默认值是50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过50s才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。
但是,我们又不可能直接把这个时间设置成一个很小的值,比如1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
正常情况下我们还是要采用第二种策略,即:主动死锁检测,而innodb_deadlock_detect的默认值本身就是on。 但他是需要额外资源的。一个事务需要检测自身的加入是否会导致死锁,这个时间复杂度是O(n),在大量请求的情况下会导致CPU爆满。
怎么解决由这种热点行更新导致的性能问题呢?
这个并发控制要做在数据库服务端。如果你有中间件,可以考虑在中间件实现;如果你的团队有能修改MySQL源码的人,也可以做在MySQL里面。基本思路就是,对于相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
从设计上优化这个问题?
你可以考虑通过将一行改成逻辑上的多行来减少锁冲突。还是以影院账户为例,可以考虑放在多条记录上,比如10个记录,影院的账户总额等于这10个记录的值的总和。这样每次要给影院账户加金额的时候,随机选其中一条记录来加。这样每次冲突概率变成原来的1/10,可以减少锁等待个数,也就减少了死锁检测的CPU消耗。
如果在执行逻辑备份的时候,对主库的表执行DDL操作,会发生什么情况?
Q1: SET SESSION TRANSACTION ISOLATION REPEATABLE READ; //开启可重复读事务隔离
Q2: START TRANSACTION WITH CONSISTENT SNAPSHOT; //启动事务,WITH CONSISTENT SNAPSHOT保证一致性视图
Q3: SAVEPOINT sp //备份点
/** 时刻1 */
Q4: show create table t1; //获取表结构
/** 时刻2 */
Q5: select * from t1 //获取表数据
/** 时刻3 */
Q6: ROLLBACK TO sp //回滚到备份点,释放MDL锁
/** 时刻4 */
...