1、为什么要有数据库的锁机制?
为了解决数据库层面的并发问题,多个事务同时修改一份数据资源,可能导致脏读、不可重复读、幻读的问题。
2、读锁和写锁
数据库的锁都分为读锁和写锁,读锁是共享锁(S锁),各个读锁之间是共享数据的;写锁是排他锁(X锁),加了一把写锁,对于这个数据就不能加其它写锁了。
3、全局锁
使用flush tables with read lock(FTWRL),可以锁住整库,一般用来做全库的备库。
一般我们不会在主库上做备份,而是在备库上做备份。
4、表锁
锁住整个表的数据,Lock tables,一般不用。
5、MDL锁
元数据锁,分为MDL读锁和MDL写锁,这两个锁都是自动加上的,加的时机如下:
1)对表进行增删改查的时候,会自动加MDL读锁,表示只能读取表的元数据,不能改表结构;
2)对表修改表结构的时候,会自动加MDL写锁,表示这边在改表结构,不能再有其它的事务改表结构了。
MDL锁都是在事务提交以后释放的,所以如果我们需要在线上环境给一个表加字段,那么就不能有长事务正在执行,要不然MDL锁会一起不释放,影响其它事务的运行。
我们通过innodb_trx表可以查出来长事务。
6、online DDL
Online DDL是Mysql5.6以后提供的一种支持在数据库数据操作执行过程当中,执行DDL操作,而不完全阻塞当前数据库数据操作的机制。
以后没有online DDL,每次DDL操作,都会通过MDL锁锁住全表,等锁释放以后,才能继续执行数据库的DML操作。有了online DDL以后,就简单很多了,不会影响DML操作,但是注意,还是会影响一点,因为online DDL过程还是加了锁,只是加锁过程较短,具体过程如下:
1)加MDL x锁,直接锁表。这一阶段,会根据原表创建临时表;
2)降级为MDL s锁,此时DML操作可执行。这一阶段,会把原表的数据一行一行copy到临时表中;
3)升级为MDL x锁,直接锁表。这一阶段,会启用临时表。
7、行锁
7.1 二阶段锁
首先,行锁都是二阶段锁,二阶段表示锁的加锁阶段与解锁阶段,一个事务当中,每次执行如下语句的时候,都会加锁:
Update/delete xxxxxx where xxxx(X锁)
Select ....... where xxxx lock in share mode(S锁)
Select ....... where xxxx for update(X锁)
解锁只有在事务提交的时候执行。
所以,一个事务当中的不同语句加的锁的时机是不一样的,有早有晚,所以我们应当尽量把最可能造成锁冲突、最可能影响并发度的锁放在后面,这样这把锁的加锁时间就短了。
7.2 行锁升级表锁
由于行锁其实是锁的索引,所以所有全表扫描的行锁语句,都会自动升级为表锁,会导致全表扫描的情况包括:
——检索没加索引;
——索引上有隐式类型转换;
——用了二级索引,但是二级索引取的数据占全部数据的30%左右,优化器就会觉得还不如走全表扫描算了,效率可能更高。
7.3 锁冲突
一条SQL语句加锁之后,就会影响其余加锁语句的执行,锁与锁之间的影响是:
——通过lock in share mode加读锁后,不影响其它加读锁SQL的执行,但会阻塞其它加写锁SQL的执行;
——通过update或for update加写锁后,会影响其它加读锁和加写锁SQL的执行。
7.4 Next-key lock
InnoDB给SQL语句加的锁是Next-key lock,而不是行锁,next-key lock包含了行锁和间隙锁,SQL语句加的锁有可能是行锁,也有可能是间隙锁,也有可能是两种都有。
1)对于唯一索引的等值查询加锁,分两种情况:
● 能查到值,那么next-key lock会退化成行锁;
● 查不到值,那么next-key lock会退化成间隙锁。
2)对于普通索引的等值查询加锁,分两种情况:
● 加s锁,并且查询是覆盖索引,那么加next-key lock,由于普通索引不是唯一索引,所以会继续往下找,因此又会再加一个next-key lock,并且会退化成间隙锁。并且锁只会在普通索引的B+树上,不会对主键索引的B+树上锁,因此不影响主健索引上的查询操作;
● 加X锁,不管是不是覆盖索引,next-key lock会退化成间隙锁。
3)对于主健索引的范围查询加锁,会根据范围加next-key lock,注意,范围查询不会退化成行锁或者间隙锁,还须注意,如果范围正好卡在next-key lock的边界上,那么会继续往下一个next-key lock找不满足要求的值,所以会在原来的基础上又多了一个next-key lock;
4)对于普通索引的范围查询加锁,会根据范围加next-key lock,由于范围查询不会退化成行锁或者间隙锁,因此最后的锁就是next-key lock。
5)最后,间隙锁是共享锁,不同事务对于同一个间隙索是兼容的,都可以加锁,不会阻塞。而行锁的读锁是共享锁,写锁是排他锁。间隙锁是共享锁会带来一个问题,就是两个事务都申请到了同一个间隙锁,那么它们对这个间隙进行insert操作,就会死锁,被各自的间隙锁给阻塞了。
8、死锁
事务不同线程之间存在循环资源依赖,你依赖我,我依赖你,都在互相等待,就会死锁,解决的办法有:
● 尽量在所有事务中,对表的执行顺序保持一致;
● innoDB有锁的超时机制,默认50S,超时自动回滚,这种方式不太方便,50S时间太长了,如果设置短一点,又会把很多不是死锁的误判成死锁;
● 一般情况下都需要开启死锁检测,这样每次加锁的时候,都会扫描会不会死锁,如果会死锁,回滚。
通过show engine innodb status 会打印死锁日志,看看为什么死锁。
9、乐观锁
上面讲的库锁、表锁、MDL锁、next-key lock都是悲观锁,都是通过执行相应的SQL语句,由数据库来加锁的。
对于写入多的并发场景,我们可以使用悲观锁,但是如果是读多写少的并发场景,我们还可以使用乐观锁,在代码层面解决并发问题。因为,写入少的话,并发冲突的问题就很少,因此可以不用考虑加悲观锁。
实现乐观锁的方式是加一个新的字段version(数据的版本号)来实行乐观锁,具体步骤如下:
1)加一个新字段,version
2)先读表里面的数据,拿到我们需要更新的那行数据的version
3)更新数据时,比较当前数据库数据的version与第二步取到的version的值是否相等:
Update xxx set xxxValue = “ABABA” and version = versionValue+1 where version = versionValue
这样就算两个线程都来执行这条语句,第一条执行完以后,第二条也无法执行,因此version变了。