今天我们来研究下数据库的锁,首先数据库锁的设计就是用来处理并发问题的,作为多用户共享的资源,出现并发访问的时候,数据库就要合理的控制访问资源的规则,锁🔐就是用来实现这些访问规则的重要数据结构!
根据锁的范围大小,MySQL里面的锁一共可以分为全局锁,表级锁,行锁三大类。下面我们来一一介绍下这些锁:
全局锁:
全局锁就是对整个数据库实例加锁,加了全局锁之后我们的整个数据库就会变成只读的状态,其他线程的操作都会阻塞,比如数据的增删改,修改表结构,建表等语句。
MySQL提供了一个加全局锁的命令就是
Flush tables with read lock(FTWRL)
我们只要执行整个命令就可以让数据库进入只读状态了
全局锁场景用途:
全局锁使用的典型场景就是,在MySQL的引擎不支持事务的情况下做全库逻辑备份! 这个前提条件引擎不支持事务(下面细说)。 主要原理就是因为他会让我们数据库只读,确保不会有其他线程对数据库做更新,然后对整个库做备份。
但是这样会带来两个问题:
- 如果我们数据库要备份,就说明在这期间所有的更新操作都得停止,相当于我们的服务瘫痪。
- 如果我们数据库做了主从复制的,在从库上备份,那么从库就不能执行从主库同步过来的binlog,就会导致主从延迟。
既然有上面这两个问题,那么我们不加这个锁可以么?
还是不行,不加锁还是会有问题比如: 账户余额表跟已拥有装备表; 我一个用户在这个时间点买了商品A;我们后台逻辑先去账户余额表里扣余额,再去已拥有装备表里加商品数据,我们在扣减余额的时候正准备去商品表里加数据的时候把数据库锁住,只能已读,那么我们备份的数据余额那是扣减了,已拥有的商品可是没加上去。如果我们后面数据丢失需要用这个备份来恢复数据的话,那么就是用户发现自己花的钱跟商品对不上了(咦!!我黑切呢!!!!!)
所以现在很少有用到MyISAM引擎的重要原因之一就是他不支持事务!
现在说说那个前提条件就是如果我们的引擎就是支持事务的,比如就是用的InnoDB引擎,那么我们记得我们的InnDB引擎在每次开启一个事务的时候就会创建一个当前的视图,读取的数据都是来自启动事务的视图的,因此整个备份的过程,看到的数据都是一致的,不会看到备份过程中其他事务提交的数据。 所以如果MySQL没用到这些支持事务的引擎,就只能这样了(现在也很少这样用MySQL了)
Tips:set global readonly = true 一样可以让全数据库已读,用这个可以么: 这个命令有两个缺点: 1、在一些系统中,readOnly的值会用来判断做其他逻辑,比如这个库是主库还是从库,修改这个变量影响的范围更大,不建议使用 2、而且这个命令使用后如果客户端发生异常,那么数据库会一直保持这个readOnly这个状态,就会导致数据库长期处于这个只读状态,风险太大了。FTWRL这个命令只要客户端异常断开,MySQL会自动释放这个全局锁,就比较能接受!
表级锁
MySQL里面的表级锁有两种:
表锁:表锁的语法是lock tables … read/write, 与我们上面的那个FTWRL的全局锁类似,也都可用unlock tables主动释放锁,而且客户端断开之后也会自动释放锁,需要注意的点就是lock tables语法除了会限制别的线程读写,也限定了本线程的操作对象。 比如,我们在线程A中执行了一条语句:lock tables t1 read, t2 wirte; 那么线程A对表T1就只能读,对T2只能读写,其他线程写t1,跟读写t2都会阻塞,而且在释放这个锁之前,线程A也不能访问其他表! Tips:当然对于我们常用的支持行锁的InnoDB引擎来讲,为了处理常见的并发,并不需要锁住整个表,毕竟锁一个表影响还是太大了!
元数据锁(MDL):MDL不需要显式的使用,在访问一个表的时候会被自动加上,在语句结束后并不会马上释放,而会等到整个事务提交后再释放。它在我们对一个表做增删改查操作的时候会加MDL读锁;在我们对表结构变更操作的时候,加MDL写锁。 读锁之前不互斥,所以我们有多个线程可以同时对一张表进行增删改查。 读写锁互斥,写锁之间也是互斥的。这样就可以保证我们表结构操作的安全性,比如有两个线程要同时给一个表加字段,其中一个线程就要等另一个线程执行完才能开始执行。
行锁
与全局锁跟表级锁不同的是,MySQL的行锁是在各个引擎自己实现的,不是所有的引擎都支持行锁,比如MyISAM引擎就是不支持行锁的,所以用MyISAM引擎的MySQL在碰到并发控制的时候只能用表锁,就意味着在这个库里面的同一张表上的任意时刻就只能有一个更新在执行,很印象业务的并发速度,InnoDB是支持行锁的,所以这也是MyISAM被InnoDB取代的重要原因之一!
两阶段锁:
我们先看看上面的场景,在上面的这个操作序列中,事务B的update语句执行时会怎么样呢? 我们先了解两阶段锁的概念:在InnoDB的事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放, 而是要等到事务结束时才释放,这个就是两阶段锁协议 知道了这个概念之后我们就知道了事务B的update语句会被阻塞,知道事务A执行commit之后,事务B才能继续执行。 所以知道了这个概念之后,我们的事务中需要锁多个行的时候,就可以把最可能造成锁冲突,最可能影响并发度的锁尽量往后放,因为要尽可能的减少我们锁行的时间,这样就能提高我们的并发度!
粒度越来越细,我们都知道线程越是安全隔离的,性能就越低,能支撑的并发度就越小,相反我们行锁的性能能够支持的并发度比表锁跟全局锁高,那么随之带来的问题就是安全的问题, 在使用了行锁的情况下,我们不得不考虑得就是,它的一个死锁的问题
死锁:
我们熟悉并发编程的伙伴知道死锁形成的四大条件之一就是互相持有锁资源不放,再去获取锁。我们来看看数据库里这样的场景:
这个时候事务A获得id=1的行锁,然后事务B获得id=2的行锁,事务A继续执行持有id=1的行锁,去获取id=2的行锁,直接阻塞,然后事务B也不会commit释放,因为它下面的语句就是自己持有者id=2的行锁,去获取id=1的行锁,这样互相等待对方1释放,就进入了死锁状态: 解决办法:
- 设置一个超时时间,这个超时时间可以通过参数innodb_lock_wait_timeout来设置,一般默认是50s,一旦超时就直接退出执行。
- 发起死锁检测,发现死锁之后,主动的回滚死锁链条中的某个事务,让其他事务可以继续执行下去,将参数
innodb_deadlock_detect 设置为on开启(默认也是on开启状态)
我们日常的业务中还是会采用第二种策略,因为50s的死锁等待时间对于我们业务系统来说是等不起的,同样我们也不能把这个时间设置的很小,比如设置成1s。但是我们如果只是普通的锁等待超过1s就退出了,那么就是正常的业务执行都被杀死了。也是不稳妥的做法。 但是尽管第二种策略能够快速的发现并进行处理,它同样有着它的缺点就是它需要更多的消耗CPU资源 比如我们的业务场景确实很大,同时又1000个并发线程要同时更有新一行,那么每个线程都会去判断自己会不会造成死锁,死锁检测总共就是100万这个量级,虽然可能最终的检测结果是没有死锁,但是这个期间就会消耗大量的CPU资源,所以我们就会看我们的CPU利用率很高,但是每秒执行不了几个事务。 所以,我们可以来思考下这个问题:如果优化数据表里热点行的更新导致的性能问题? 它的关键就在于死锁检测,因为死锁检测要耗费大量的CPU资源! 解决:
- 直接关掉死锁检测:如果我们能确保这个业务一定不会出现死锁,那么就可以把死锁检测关掉,但是这个操作是有风险的,因为我们业务设计的时候一般不会把死锁当成一个严重的错误,因为出现死锁了,就可以回滚,然后通过业务重试一般就没问题了,这就是业务无损的,但是关掉了死锁检测就意味着可能出现大量的超时,就是业务有损的。
- 并发控制:我们会考虑到这个问题就是因为对同一行的更新并发量太大了,我们就可以尝试着对并发进行控制
客户端做并发控制:这个方法在我们客户端少的情况下可行,但是如果客户端很多的话,比如一个客户端控制5个并发,但是有几百个客户端,并发量也会很大 数据库端做并发控制: 用数据库的中间件进行实现, - 改源码:
如果有能力又该MySQL的源码,也可以直接做在MySQL里面,就是在向同行的更新的时候,进入引擎之前就排队,这样在InnoDB内部就不会有大量的死锁检测了。 - 从设计上优化这个问题:比如我们可以将一行改成多行来减少锁冲突,比如我们同时修改着一个公共账户的余额,那么我们可以把这个账户分成10行,这个账户的总额就等于这10个记录的总和,处理业务的时候就要随机选一条来加入,就可以减少对同一行的并发了。需要注意得就是对这个业务逻辑的边界处理了,比如一部分余额变成0了这种
|