c mysql锁_Mysql心路历程:Mysql各种锁机制(入门篇)

这一篇文章是本人数据库的第二篇,也是对数据库学习的阶段性总结。对于数据库锁的了解,是区分程序员,尤其是Java程序员,中高级的一个重要标志。也是日常,我们开发中,经常碰到坑的地方。往往,我们无脑的CURD过程中,其实已经出现问题了,锁问题,但是我们并没有发现,你那是没有被大访问量冲击。一旦一朝我们冲击到了,那损失和锅,是要自己承担下来的。不多说,我们接下来就来一步步看看Mysql的锁机制。

一、Mysql锁的类型

整体上,Mysql锁的类型,从全局的到细节的,包含如下几个:

全局锁

表级锁

普通的表锁

MDL (元数据锁)

行锁

读锁(共享锁)

写锁(排他锁、叉锁)

间隙锁

Next-key lock

大概上,我们日常生产学习中,所能接触到的就这几大种类了(个人的脑容量也就能掌握这么多了,(⊙﹏⊙)b),接下来,我们一个个的说说。

二、 全局锁

顾名思义,全局锁就是对整个数据库实例加锁。Mysql提供了一个加锁的语句:Flush tables with read lock (FTWRL)。它能使整个实例上面,只读,所有的写和更新,都会被阻塞。全局锁的使用经典的使用场景是做全局的数据备份使用,具体的操作,可能平时我们碰到不多,不过要了解几点:

每次全局备份过程中,如果是InnoDB引擎完全可以通过MVCC创建一致性视图,来保证不受备份中,其他操作的影响,问题是不一定所有的数据引擎都是InnoDB

我们同样可以通过set global readonly=true 来进行只读性的设置,但是和FTWRL的区别如下:

有些数据库把第一种模式用作,设置成备库的只读限制,而不是用来做备份的,影响面比较大

异常处理机制不一样:FTWRL这种机制,如果设置之后,客户端异常断开连接了,数据库会主动释放全局的锁,恢复正常;而set这种方式,客户端异常断开了,就不会恢复原状,数据库会一直只读,影响很大。

具体对数据库的全局锁,不仅仅阻塞增删改操作(DML),对数据库表的增删改字段(DDL)也会被阻塞

三、表级锁

我们首先要知道的是,每次进行select操作或者DML的时候,对表加的都是MDL的读锁,而进行DDL的时候,对表加的是MDL的写锁,让我们首先来个印象。接下来来看看普通的表锁与MDL(元数据锁meta data lock)的区别。

1、普通的表锁

普通的表锁也是分读锁与写锁,数据库提供语句操作:lock tables … read/write,使用unlock tables进行释放锁。具体注意的点是:加了普通的表锁之后,对当前加锁线程接下来的数据库操作,都是有影响的。

举个例子:如果A线程使用语句lock tables t1 read, t2 write; 这个语句,那么,其他线程写t1和读写t2都会被阻塞;同时线程A再进行unlock之前,也只能读t1和读写t2,连写t1都是不被允许的。自然也不能访问其他的表

在没有出现行锁之前,都是通过表锁进行并发控制的,上面例子可见,影响面还是太大,限制太严格了。

2、元数据锁(MDL)

MDL不需要主动加锁,每当我们访问一个数据表的时候,会自动被加上,作用是防止在我们进行表的操作的时候,进行了表结构的变更。再5.5这个版本中被引入了Mysql中:

当对一个表进行增删改查的时候,加MDL的读锁

当进行一个表的结构变更的时候,加MDL的写锁

读写锁的MDL之间的互斥关系是:

读锁与读锁不互斥

读锁与写锁互斥

写锁与写锁互斥

具体有个经典的例子:经常发生的是,我们给一个表加了个一个字段或者几个字段,很小心了,但是加的过程中,直接整个表挂了,接下来的操作都失败了或者不返回。接下来我们就看看具体的操作过程:

sessionA

sessionB

sessionC

sessionD

begin;

select * from t limit1

select * from t limit1

alter table t add f int (block)

select * from t limit1 (block)

可见,我们sessionC操作之后,由于sessionA是没有结束事务的,我们MDL会随着事务的开启而加锁,事务的结束而释放锁,所以,sessionA这时候保持住了MDL的读锁。然后sessionC想要获取MDL的写的时候,由于读写互斥,sessionC就被阻塞了。接下来的语句,也都执行不了了,因为接下俩的语句要申请MDL的读锁,而有写锁已经在阻塞状态,读锁又要排队等这个写锁执行释放,那接下来的现象可想而知。

我们如何安全的对一个表进行加字段的操作呢:

在information_schema库里面的innodb_tx表中,可以查到当前正在执行的事务,如果是一个长事务,我们可以先考虑kill掉这个事务

如果是频繁访问的断事务比较多的情况,我们可以使用alter table tablename wait N add col这种类型的操作,如果拿不到MDL写锁,一段时间会释放阻塞,不长期影响数据库。

四、行锁

这个过程比较复杂,首先,我们来看看,Mysql加行锁,是使用两阶段加锁策略的,我们看看什么叫做两阶段加锁:

两阶段锁协议,整个事务分为两个阶段,前一个阶段为加锁,后一个阶段为解锁。在加锁阶段,事务只能加锁,也可以操作数据,但不能解锁,直到事务释放第一个锁,就进入解锁阶段,此过程中事务只能解锁,也可以操作数据,不能再加锁。两阶段锁协议使得事务具有较高的并发度,因为解锁不必发生在事务结尾。它的不足是没有解决死锁的问题,因为它在加锁阶段没有顺序要求。如两个事务分别申请了A, B锁,接着又申请对方的锁,此时进入死锁状态。

1、什么时候加行锁

正常,我们select语句时候,是不会添加行锁的,只会加上MDL的读锁,即使这条语句是全表扫描,也不会加行锁,只不过全表扫描,查询较慢罢了,并不会因为锁的问题而对其他操作进行阻塞。下面是我总结的一些加行锁的场景:

select * from t where id = 1 in share model 对主键为1的这一行,加行锁,共享锁

select * from t where id = 1 for update 对主键为1的这一行,加行锁,排它锁,叉锁

update t set col1 = 1 where id =1 对主键为1的这一行加行锁,排它锁,叉锁

update t set col1 = 1 where col2 =1 如果col2没有索引,那么是加普通表锁;如果col2是非唯一索引,对所有col2为1的行,加行锁;如果col2是唯一索引,对col2为1的这一行,加行锁。行锁都是排它锁

p.s.:当然上面所有所列取的操作,都是首先加了MDL的读锁的

2、加行锁的影响

行锁,之所以存在,就是提高并发度的。取代以前,我们要整表进行加锁,而引起同一时刻,只能有一个线程对数据表进行增删改的操作,下面我们看一个具体的数据库操作:

事务A

事务B

begin;

update t set k = k+1 where id = 1;

update t set k = k+2 where id = 2;

begin;

update t set k = k+3 where id = 1;

commit;

事务B的update会被阻塞,因为id为1的这行行锁被事务A所持有

begin的时候,没有任何行锁被持有,只有当具体操作进行是,依次请求MDL的读锁,这一行的排它行锁

所有,当前事务持有的行锁,语句执行完都不会释放,知道commit之后才释放

所以,按照这种逻辑,越是并发度高的数据表,越要靠事务的后面写,因为持有行锁时间短,影响并发度的时间越短。

3、这里我们引出死锁

首先我们看接下来的这个模拟操作:

事务A

事务B

begin;

update t set k = k+1 where id = 1;

begin;

update t set k = k+3 where id = 2;

update t set k = k+2 where id = 2;

update t set k = k+3 where id = 1;

这就是一个经典的死锁场景,我们来分析下:

事务A的update t set k = k+1 where id = 1;获取了id为1这一行的行锁(排它锁)

事务B的update t set k = k+3 where id = 2;获取了id为2这一行的行锁

事务A的update t set k = k+2 where id = 2;要获取id为2的行锁,而获取不到,阻塞

事务B的update t set k = k+3 where id = 1;要获取id为1的行锁,获取不到,阻塞

对于这种,Mysql有两种机制进行处理:

innodb_lock_wait_timeout可以通过这个参数,进行设置锁等待时间,超过这个时间,阻塞的进程释放所有持有的锁,回滚。

innodb_deadlock_detect通过设置为on,能主动监测死锁,通过回滚死锁联调中的一个事物,来解决死锁

第一种情况虽然能控制,死锁,但是时间不好设置,例如我们设置一个10s,如果一个线程被锁住,要等待10s才能进行回滚,并发度自然不高,如果我设置低了,1s,那么一个正常等待的,并非死锁,也会被回滚。如此一来得不偿失。下面重点说说主动死锁检测

4、主动死锁检测

每当吧innodb_deadlock_detect设置成on,MySQL会主动检测死锁:

一个线程加入

即将被等待其他线程的锁而堵住

判断当前线程持有的锁,是否堵住了其他系统中正在运行的线程

如果是,将回滚当前线程的事务

看起来很好,然后会有代价:每次对比是否当前线程堵住了其他线程这一步,会对比所有系统正在执行的线程,时间复杂度是O(n)。当前执行的线程数少不成问题,如果是1000个正在执行的线程,那么这就是100w次的对比,这个过程极度消耗CPU资源。结果可能检测出没有死锁,然后会发现:最终CPU飚的老高,然而执行的条数没几个!解决办法有下面几个:

临时关掉innodb_deadlock_detect,但是这样会有很多超时,不实用

控制数据库的并发度:可以从中间件这层控制(Java这里),或者有能力的从数据库这一层进行控制

业务字段拆分:例如我们将一行记录拆分成多行,让一行的并发度下降(例如并发扣减id为1这一行的金额,我们可以拆分成id为100,id为200,id为300这三行的金额,最后要查询的时候,将这三行相加)

五、结束

下面一篇文章,会重点讲间隙锁,这个内容最为复杂,涉及到了所谓数据库解决幻读的机制问题,特别单独抽出一章来讲解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值