这一篇文章是本人数据库的第二篇,也是对数据库学习的阶段性总结。对于数据库锁的了解,是区分程序员,尤其是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这三行的金额,最后要查询的时候,将这三行相加)
五、结束
下面一篇文章,会重点讲间隙锁,这个内容最为复杂,涉及到了所谓数据库解决幻读的机制问题,特别单独抽出一章来讲解。
来源:oschina
链接:https://my.oschina.net/u/1461304/blog/3057575