MySQL:全局锁,表锁和行锁

        数据库为多用户共享的,当出现并发访问的时候,需要使用锁来控制资源的访问。根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类。

1、全局锁

        全局锁即给整个数据库实例加锁。主要的使用场景:做全库逻辑备份的时候,为了保证备份期间的库在同一个逻辑时间点,即保证一致性,类似于前面我们提到的可重复读的效果

        MySQL提供了一个加全局读锁的方法,命令是 Flush tables with read lock (FTWRL)。使用这个命令后,这个数据库处于只读状态,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

备份的时候加锁原因:以买东西为例

        用户买东西,首先会从余额里扣除金额,然后在订单里添加商品。如果备份数据库,不加锁,并且备份顺序为先备份用余额,再备份订单商品,有可能备份了用户余额后,用户下订单买东西提交事务,然后再备份订单商品表, 此时订单商品已存在。最后备份出来的数据为。最后用户余额为买东西前的余额,没有减少,但是订单商品却多了。

        这种情况可能用户会觉得赚了,但是如果备份顺序反过来,先备份商品表再备份余额表,用户就会发现我付了钱,但是商品没有加,这中结果就会更加的严重。

所以,我们需要保证备份时候的视图一致性,也就是在可重复读隔离级别下开启一个事务;

mysqldump

        官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。

        但是有些存储引擎是不支持事务的,比如MyISAM,这时候mysqldump就没办法起到作用了,所以single-transaction方法只适用于所有的表使用事务引擎的库,对于MyISAM等不支持事务的引擎,依然需要通过Flush tables with read lock (FTWRL)加全局锁。

        我们刚刚提到,加锁是让数据库只读,但是set global readonly=true 也可以实现数据库只读,是不是也可以再备份的时候使用。但是这里还是建议使用FTWRL,原因有两部分:

        a) 有的系统中,readonly有一些其他的逻辑意义,比如说用来区分是主库还是备库,备库加readonly只读,所有修改readonly可能会影响一些逻辑意义;

        b) 在异常处理上有区别。如果使用FTWRL命令后客户端异常重启,数据库会自动释放全局锁,而如果使用readonly,数据库会一直处于只读状态,这样会导致整个库长时间处于不可写状态,风险较高。

2、表级锁

表锁:

        MySQL里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。表锁语法: lock tables … read/write。与FTWRL类似,可以用unlock tables主动释放锁。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。如:

lock tables T1 read,T2 write;

执行到这里时,进入当前线程。
    a)  T1 仅允许 所有人 读,线程外 如需写、更新要等待线程退出线程内 如需写、更新会引发mysql报错。
    b)  T2 仅允许 线程内 读写更新,线程外 如需写、更新要等待 空间退出 。

其他线程T1、读写T2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读T1、读写T2的操作。连写T1都不允许,自然也不能访问其他表。

InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,因为锁住整个表的影响面还是太大。

元数据锁(MDL)

        MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。比如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列或者直接删除了表,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

        在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁

        a)  读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查(MVCC保证数据一致性)。

        b) 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

如果对一个表修改数据库的表结构,举例:

第一步: sessionA先执行select查询语句,对表加MDL读锁
第二步: sessionB执行select查询语句,需要MDL读锁,由于读锁之间不互斥,也可以正常执行
第三步: sessionC执行alert语句,修改数据库结构,需要MDL写锁,由于sessionA的MDL读锁还没有释放,读锁和写锁是互斥 的,所以sessionC会blocked被堵塞
第四步: 如果再执行sessionD的select查询语句,虽然是需要MDL读锁,但是由于第三步sessionC的MDL写锁被block住还没有释放,所以sessionD也申请不到MDL读锁,会被blocked,所以表就会完全变成不可读状态。

安全的给表加字段:

       首先我们要解决长事务,事务不提交,就会一直占着MDL锁。在MySQL的information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者kill掉这个长事务

但是,如果你要变更的表虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?

这时候kill可能未必管用,因为新的请求马上就来了。比较理想的机制是,在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后再通过重试命令重复这个过程。

3、行锁

        MySQL的行锁是在存储引擎自己实现的。MyISAM引擎就支持行锁,所以其并发控制职能使用表锁。对于使用MyISAM引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB是支持行锁的,这也是MyISAM被InnoDB替代的重要原因之一。

共享锁:

        共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

排他锁:

        排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

        对于排他锁大家的理解可能可能会有误区,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句,加共享锁可以使用select ... lock in share mode语句。所以加过排他锁的数据行在其他事务中是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制

InnoDB行锁实现:

        InnoDB行锁是通过给索引上的索引项加锁来实现的。所以,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。
        a) 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁
        b) 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的。
        c)当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。即便在条件中使用了索引字段,但具体是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。
 

两阶段锁协议:

如下图在一个事务中执行两条更新语句,更新不同的行;

第一步:开启事务:

第二步:执行更新id=1的行,锁住id=1的行;

第三步:执行更新id=2的行,锁住id=2的行;

第四步:提交事务,释放id=1和id=2的行的行锁;

        在InnoDB事务中,行锁是在需要的时候才加上的(在执行到sql语句时加上的),但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

        所以,如果一个事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁(行)尽量往后放。这样这行影响并发度的行锁住的时间就会缩短。

死锁和死锁检测

        当对于数据库某个表的某一行做更新或删除等操作,执行完毕后该条语句不提交,由于没有提交事务,所以该行行锁并没有释放,另一条对于这一行数据做更新操作的语句在执行的时候就会处于等待状态,此时的现象是这条语句一直在执行,但一直没有执行成功,也没有报错,形成死锁。

如下图两个事务执行如下顺序的更新操作;

解决死锁的两种策略:

        a) 直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置(在InnoDB中,innodb_lock_wait_timeout默认值是50s)。

        这种策略在出现死锁是,会一直等待50s才会释放锁,对于系统来说时间太久,如果设置的时间过短,又无法区分是否是业务的正常等待,所以不建议使用这种策略。

       b)  发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑(默认是打开的)。

        主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的。每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是是否出现死锁。但是也是有缺点的:这样的死锁检测会非常的占用内存,消耗CPU资源。所以要根据业务做一些取舍,或者使用中间件等;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你认识小汐吗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值