全局锁
对整个数据库实例加锁,命令时flush tables with read lock,加锁后整个库处于只读状态,对数据的增删改和建表修改表结构以及更新事务都会被阻塞。
应用场景是做全库逻辑备份,期间只允许select操作。
对全库的只读,set global readonly = true也能做到,它与FTWRL命令的区别是,readonly的值会被用来做其他逻辑,比如判断是否是从库;在异常处理机制上有差异,执行FTWRL后如果客户端发生异常断开,MySQL会自动释放这个全局锁,整个库回到可正常更新状态。而设为readonly后,如果客户端异常,则数据库就会一直保持readonly状态,会长时间不可写,风险加高。
表级锁
MySQL表级锁有两种:表锁和元数据锁(meta data lock, MDL)
表锁
表锁的语法是lock tables tablename read/write,和FTWRL相似,可以用unlock tables主动释放锁,也可以在客户端断开时自动释放。lock tables语法除了会限制其他线程读写,也会限制本线程接下来的读写。表锁的读锁,本线程只读;表锁的写锁,本线程可以读写。
MDL元数据锁
metadata lock不需要显式使用,在访问一个表的时候会被自动加上,作用是保证读写的正确性。对一个表做增删改查时,加MDL读锁。对表做表结构变更时,加MDL写锁。
读锁之间不互斥,读写锁之间、写锁之间是互斥的。
如何安全的给一个表加字段
1)首先解决长事务,事务不提交,就会一直占着MDL锁,可以在MySQL的information_shcema库的innodb_trx表查看到当前执行中的事务,如果DDL执行前有长事务,则要考虑先暂停DDL或者kill掉长事务。
2)对于请求频繁的热点表,可以在alter table语句里面设定等待时间,如果在指定时间内能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃,之后再让DBA通过重试执行。alter table tablename wait N add column; alter table tablename nowait add column;
行锁
MySQL的行锁是在引擎层由各个引擎自己实现的,InnoDB支持行锁,MyISAM不支持行锁。
两阶段锁协议
InnoDB事务中,行锁在需要的时候才会加上,但是并不是不需要了就立即释放,而是要等到事务结束才释放。
根据两阶段锁协议的特性,可以知道,如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁
并发系统中不同的线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待状态,称为死锁。
解决死锁的策略:
- 直接进入等待,直到超时,超时时间通过设置参数innodb_lock_wait_timeout完成。
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他的事务得意继续进行,通过设置参数innodb_deadlock_detect设置为on来开启死锁检测。
实际生产中,一般通过开启死锁检测来应对,因为设置锁超时时间的话,时间太短可能会误伤很多正常简单的锁等待;如果时间太长则在线服务又无法接受。
不过死锁检测的时间复杂度是O(n)的,并发量高的死锁检测需要大量的消耗cpu资源,如果热点行更新时开启执行死锁检测,就会看到CPU利用率很高,但是每秒却执行不了几个事务。
解决热点行更新的性能问题
导致热点行更新性能问题的根本原因是并发量大时的死锁检测耗费CPU资源。
一种思路是确保不会出现死锁时关闭死锁检测,但是这可能导致业务上出现大量超时,这是有损的。
另一种思路是控制并发量,如果同一行同时最多只有10个线程在更新,那么死锁检测的成本就会很低。控制并发量的话,应该放在数据库服务端进行,因为在客户端难以保证并发控制,单个客户端限制10个并发,但是如果客户端很多上百个,那么服务端的并发仍然很高。在数据库服务端控制并发,可以通过中间件实现,也可以通过修改MySQL源码实现。基本思路是,对于相同行的更新,在进入引擎之前排队。这样InnoDB内部就不会出现大量的死锁检测工作了。
借鉴与上一种思路,也可以在业务设计上优化,来减少热点行并发。可以考虑将一行改成逻辑上的多行,来减少锁冲突,比如将账户总额设计为10个记录的值的总和。不过这样增加了业务上的复杂度。
如何删除表的前1万行数据?
A.delete from T limit 100000;
B.在一个连接中循环执行20次delete from T limit 500;
C.在20个连接中同时执行delete from T limit 500;
答案是B。
因为A的单个语句时间较长,锁的时间也就长,而且大事务还会导致主从延迟。
C的话,会人为造成锁冲突。
