根据加锁的范围,MySQL中的锁大致分为全局锁、表级锁和行锁。
全局锁就是对整个数据库实例加锁,MySQL中提供的加锁的命令是:
Flush tables with read lock(FTWRL)
使用这个命令可以让整个库处于只读状态,其它线程例如:(增删改操作)都会被阻塞;
全库逻辑备份,把整库每个表都select出来存成文本;
整库只读:
思考以下不加锁的结果?
现在一个电商网站发起逻辑备份,在备份期间呢,你买了一双球鞋,在业务逻辑中先扣掉余额,然后在已购买的列表里添加球鞋;
从时间顺序上,肯定是先备份了用户的余额,然后用户购买,在备份相应的购买商品;
出现的问题就是,用户的账号内的余额没扣,但是已经买到了鞋子;
那么不加锁出现的问题就是备份系统备份得到的库不是一个逻辑时间点的,这个视图逻辑不一致;
官方自带的逻辑备份工具是mysqldump,使用参数-single-transaction, 在导数据前会启动一个事务,来确保拿到一致性视图,由于MVCC的支持,数据是可以正常更新的;single-transaction方法 只适用于所有表使用事务引擎的库,一致性读的前提是存储引擎支持这个隔离级别;
对于MyISAM这种不支持事务的引擎,在备份中有更新,破坏备份的一致性就必须使用FTWRL命令;
既然表级锁是全库只读,为何不直接set global readonly = true
readonly是可以让全库进入只读状态,还是建议使用FTWRL,原因如下:
表锁的语法是:lock tables…read/write,可以采用unlock tables主动释放锁,也可以在客户端断开的时候自动释放;lock tables除了限制别的线程读写外,也会限定本线程接下来的操作:
例如,如果某个线程A执行语句lock tables t1 read,t2 write;其它线程写t1,读写t2的语句都会被阻塞;同时,线程A在执行unlock tables之前,也只能执行读t1,读写t2的操作;
MDL不需要显示使用,在访问一个表的时候会自动加上,MDL的作用是保证读写的正确性;
当对一个表做增删改查操作的时候,加入MDL读锁;当要对表结构变更操作的时候,加入MDL写锁;
思考一下,如何安全的给一个小表加字段?
sessionA先启动会对表t加一个MDL读锁;
sessionB需要的也是MDL读锁,都可以执行;
sessionC会被阻塞,因为sessionA的MDL读锁还未被释放;
随后的要在表t上新申请MDL读锁的操作都会被sessionC锁阻塞,如果客户端有重传机制,查询请求又很频繁,这个数据库的线程会爆满;
回到上面的问题,如何安全的给小表加字段呢?
MariaDB已经合并了AliSQL,都支持DDL NOWAIT/WAIT n这个语法
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_nme WAIT N add column...
MySQL的行锁是在引擎层的各个引擎自己实现的,并不是所有的引擎都支持行锁,InnoDB是支持行锁的,这也是其取代MyISAM的一个重要原因之一,如何通过减少锁冲突来提升业务并发度。
行锁就是针对数据表中行记录的锁,比如事务A更新了一行,这个时候事务B也更新了一行,必须等到事务A的操作完成之后才可以更新;
实际上事务B的update语句会被阻塞,直到事务A执行commit之后,事务B才可以继续执行;在InnoDB中,行锁是在需要的时候才加上,但是要等到事务结束时才释放,这就是两阶段协议。
那么如果在事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
在并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源,就会导致这几个线程都会进入无限等待的状态,就是死锁。
上图就是,事务A在等待事务B释放id=2的行锁,事务B在等待事务A释放id=1的行锁,二者互相等待对方的资源释放,就进入了死锁状态,当出现死锁时有两种应对策略:
innodb_lock_wait_timeout的默认时间是50s,第一个锁住的线程要过50s才会超时退出,其它线程才可以继续执行,太长太短都不好,建议采用第二种策略。
每个新加入的线程都会判断是不是由于自己的加入导致了死锁,这是一个时间复杂度为O(n)的操作,假设有1000个并发线程同时更新同一行,那么死锁检测的操作百万量级的。即便最终没有检测到死锁,但是耗费了大量的CPU资源,导致CPU的利用率很高,每秒却无法执行多个事务。
如果能够控制并发度,一行最多只有10个线程在更新,死锁检测的成本低就不会出现这个问题,并发控制要在数据库服务端,也可以考虑在中间件中实现。在MySQL中实现就是在进入引擎前排队,这样InnoDB内部就不会有大量的死锁检测工作了。