按照锁的大小,我们分为全局锁,表锁,页锁,和行锁。
不同级别的锁有不同的作用,这里我简单列出我在工作过程中对这些锁的理解。
什么使用用全局锁?
全局锁是用来对整个数据库加锁的锁。
我们可以使用Flush Table With Read Lock(FTWRL)来让数据库进入只读状态。
使用这个命令之后,其他线程的如下操作会被阻塞:DML数据增删改操作,DDL数据定义操作,如建表,修改表结构等操作以及更新类事务的提交语句。
全局锁的使用场景在我们公司为:重要库的一天一次的全量备份,次要库的一周一次的全量备份。
做全库的逻辑备份,我们就需要保证再这段时间内没有新的数据,保证数据库只读,否则就会出现数据异常问题,之后恢复数据也会出现问题。
虽然说全局锁肯定也不太好,毕竟使用了全局锁就意味着主库备份时肯定不能写入修改数据了,而从库备份时也就不能同步主库过来的binlog日志,都会有些影响。
但是如果不加全局锁,再备份期间就会导致一些修改操作,而备份恢复的时候这些修改操作已经执行,从而导致的一些数据异常问题,所以加锁能帮助我们避免这些问题。
其实这也就是所谓的数据一致性问题而已,我们在备份的时候,如果可以记录对这些备份数据的修改,并且我们备份数据的时候,能先拿到一下这些数据的快照,那么似乎就可以解决这些数据不一致的问题了,所以就有了mysql自带的备份工具mysqldump。
当 mysqldump 使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC 的支持,这个过程中数据是可以正常更新的。但是对于 MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL命令了。所以,single-transaction方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过FTWRL 方法。
补充:内容来自《MySQL45讲》
为什么不使用 set global readonly=true 的方式呢?
readonly 方式也可以让全库进入只读状态,但我还是会建议你用 FTWRL 方式,主要有两个原因:
一是,在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改 global 变量的方式影响面更大,我不建议你使用。
二是,在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。业务的更新不只是增删改数据(DML),还有可能是加字段等修改表结构的操作(DDL)。不论是哪种方法,一个库被全局锁上以后,你要对里面任何一个表做加字段操作,都是会被锁住的。
什么时候用表锁?
对于MyISAM这种不支持行锁的存储引擎,我们需要锁数据的时候就只能以表为单位来进行加锁,比如使用lock table xxx read/write的方式,并且使用unlock tables的方式来释放锁。
表锁的加锁消耗相比于行锁更小,但是由于锁的粒度比较大,因此容易导致并发问题。
当我们在某个线程使用lock table xxx 的方式来对一张表加上读锁的时候,其他线程只能读取当前这张表,如果写那么会阻塞,也就是我们所谓的读读不互斥,读写互斥。
而如果我们对一张表加了写锁,那么其他线程读取这张表的时候就会直接阻塞,可想而知,死锁的发生概率非常的大。
还要注意的一个锁是MDL元数据锁,这个锁不需要显式的调用,它的作用是帮助我们再执行操作的时候保证表结构不被修改。
同样的,我们在读(包括增删改查)取一个表的时候,会得到MDL读锁,读锁之间不互斥。
但是如果我们开始使用alter语句去修改表结构,那么我们必须确保能拿到MDL写锁,而写锁的获取需要保证前面没有读锁,否则前面有读锁时,写锁需要先阻塞,而此时写锁阻塞,再当前alter语句之后的所有语句,包括获取MDL读锁的语句也会进行阻塞,那么此时就又会出现死锁了。
所以我们一般再要进行alter语句的时候我们会设一个超时时间。
什么时候用行锁?
MySQL InnoDB 存储引擎的行锁是基于索引的。当执行一个含有 WHERE 条件的 SQL 查询时,InnoDB 存储引擎会根据相应的索引定位到符合条件的记录,并对这些记录加上行锁。
行锁的粒度是以记录为单位的,只有在命中对应的索引之后,才会对符合条件的记录进行行锁加锁。这种方式可以提高并发性能,因为只有真正需要锁定的记录才会被锁定,而不是对整个表或整个索引进行锁定。
需要注意的是,如果查询没有命中索引,而是使用全表扫描的方式进行,那么会出现全表锁(Table Lock),而不是行锁。全表锁会锁定整个表,对于并发性能会有一定的影响。因此,在设计数据库表和索引时,合理选择和创建合适的索引是非常重要的。
并且行锁的使用遵循两阶段锁原则,上面我也提到了,只有用到了某条记录才会对某条记录加锁,而如果两个事务都需要对同一个行锁进行修改,那么后来者需要等待先来者事务提交之后才会结束阻塞,否则需要等待事务提交。
也正是这个原因,我们应该需要确保最有可能导致阻塞的语句,放在最接近事务结束的地方,我们公司有一个SKU系统,我们会确保对SKU的操作一定是放在最后进行的,也就是一般情况都是我们的save/update语句的下一条语句就是return,确保对其他事务的影响最小。
随着上面的情况,如果我们的事务一修改了记录1,然后事务二修改了记录2,而之后当事务一需要操作记录2的时候,事务二需要操作记录1的时候就会出现死锁。
对于这种死锁,我们的计划是设定锁超时自动释放,参数为innodb_lock_wait_timeout,由于我们是下游,我们的上游接口有一定的时间,所以我们设定这个参数为10s左右。
mysql害提供了一个innodb_deadlock_detect,他会自动进行死锁检测,但是因为死锁检测的性能消耗是很高的。毕竟他需要扫描每一个事务对应的线程有没有被其他线程锁住,这很消耗性能。
所以我们编码的时候一般需要保证尽可能多的测试,确保业务尽可能不出现死锁,也就是从源头上减少出现死锁的可能性。
同时当初还考虑到由于死锁是出现在对同一行记录的操作,所以我们有的时候会把一条记录进行拆分,合并之后才是这条完整的记录,我们可以吧一些操作先操作在其他行上,或者说我们也会创建中间表,然后再一定时间之后合并操作。