在当下互联网技术的发展状态下,数据的高并发是随处可见,那么数据库如何解决高并发所带来的问题呢。锁便是计算机用于解决多进程或多线程并发服务中保持数据一致性的关键。本文主要介绍MySQL中的锁。
本人还写了MySQL相关博文,有兴趣的研友可以点击如下链接,请各位研友指正并留言。
MySQL索引及优化
MySQL事务与隔离级别
索引为什么选择B+Tree
乐观锁与悲观锁并不是实际的锁,它是对锁的一种抽象。本文要讲解的MySQL中的锁都属于悲观锁,MVCC机制则属于乐观锁。
乐观锁认为本次服务对数据进行操作时,其他服务不会修改数据,所以乐观锁在操作数据时,并不会加锁抵抗其他服务修改数据,而是在对数据操作完之后再去校验是否有其他服务修改了数据。
比如在数据中添加一列版本号,每次事务提取数据与版本号,完成事务时,版本号加一,当提交本次事务时,若本次事务版本号大于数据库中的版本号则持久化;若本次事务版本号低于数据库版本号则认为其他事务已对数据进行了更新,应放弃本次服务的修改;Redis中的锁就是一种乐观锁。本文最后讲解的MVCC也是一种乐观锁。
悲观锁认为本次对数据进行操作时,其他服务会修改数据,所以悲观锁在读取数据时对数据进行上锁,其他事务只能等待其锁释放,从而达到了一种串行化执行顺序,保证了数据的一致性。但由于事务进行串行执行,所以吞吐量慢,同时易造成死锁。MySQL中的锁,如共享读和排他写都属于悲观锁。
MySQL中的锁按访问粒度可分为:
本部分只讲解常用的表锁与行锁,页面锁的效率间于表锁与行锁之间。
MySQL的MyISAM存储引擎只支持表锁,即对表中的所有行进行限定访问。当用户只是更新一行数据时,也会造成其他行不可访问,由此可见表锁的粒度比较大,在高并发情况下,数据吞吐量很低,但数据的一致性得到最大化。由于访问数据的事务由于表锁需要排队访问,所以不会存在死锁的问题。
表锁的特点:开销小,加锁快;不会出现死锁;锁定粒度大,并发度最低。
表锁分为两种:
由定义可知:
这样的表锁机制会带来什么问题呢?在高并发的互联网应用场景中,由于排它写锁优先级高于共享读锁,所以有可能导致共享读锁没有机会执行。
MyISAM存储引擎支持并发插入,以减少给定表的读和写操作之间的争用:
MyISAM引擎中锁的设置:
1、共享读锁:
lock table xuebao read;
2、排它写锁:
lock table xuebao write;
3、释放锁:
unlock tables;
MySQL的InnoDB引擎支持行级锁,但InnoDB的行锁并不是真实的在一张表中对某一行进行加锁,而是对索引的键进行加锁,所以一条SQL在实际执行时,若没有用到索引,则该SQL不会使用到行锁而是升级为表锁。
InnoDB引擎的锁可以分为如下几类:
1、意向共享锁;
2、意向排他锁;
3、共享锁;
4、排他锁;
5、间隙锁;
意向共享锁是自动添加的,不需要用户的干预。InnoDB事务在获取共享锁之前,需要先获取到该数据的意向共享锁。
意向排他锁是自动添加的,不需要用户的干预。InnoDB事务在获取排他锁之前,需要先获取到该数据的意向排他锁。
共享锁允许多个事务的共享锁去读取一行数据,但会阻塞其他事务的排他锁。
MySQL的SELECT语句并没有默认加共享锁或排他锁,所以SELECT加共享锁如下所示:
SELECT id from student_table where age = 16 LOCAL IN SHARE MODE;
排他锁会阻塞其他任何锁,处于独占数据的状态。
MySQL的UPDATE、DELETE 和 INSERT语句会自动添加排他锁。SELECT语句也可以添加排他锁,如下所示:
SELECT id from student_table where age = 16 FOR UPDATE;
需要注意的是没有加任何锁的SELECT语句,并不会与读锁或写锁产生锁竞争关系,所以某一行已经加排他锁了,但没有加任何锁的SELECT语句还是可以访问到数据。
间隙锁也是InnoDB引擎管理的,不需要用户的干预。间隙锁产生的原因需要从InnoDB的锁产生讲起。因为InnoDB引擎的锁是针对索引实现的,而索引是一种数据结构,无论实现该数据结构的是B-Tree还是B+Tree,一个索引键值与另一个键值之间是用链表实现的,那么我们在使用行锁时,若指定的行不是具体某一行,而是一个范围时,InnoDB实际锁定的是对应的一个索引范围,所以链表之间的指针也被锁定了不准修改,其他事务想在该范围内插入数据也就行不通了,这就形成了间隙锁。如下所示:
SELECT * from student_table where id > 20 FOR UPDATE;
id为主键,id>20为范围条件,所以该条语句会自动添加间隙锁。
间隙锁的缺点:从间隙锁的锁定粒度来看,要大于行锁的锁定粒度,那么在高并发情景下,间隙锁的数据吞吐量就会小于行锁。所以用户访问数据时,尽量使用相等条件,少用范围条件。
需要注意的是:当对不存在的一行添加锁时,InnoDB引擎同样会使用间隙锁,导致其他事务想插入该条不存在的行数据时被阻塞。
锁的常见问题有两种:
1、行锁升级为表锁
2、死锁
3、锁的性能优化
在介绍行锁时,已经指出InnoDB的行锁是基于索引实现的,如果访问数据的语句实际没有用到索引,那么就不可能用到行锁。如何知道访问语句有没有使用到索引呢?需要用户使用explain对单条SQL进行排查,其方法详见文献《MySQL索引及优化》。当SQL语句未使用索引而加行锁时,InnoDB会自动将行锁升级为表锁。
避免措施:从行锁升级为表锁的介绍可知,用户需要加锁的SQL语句一定要通过explain检测是否使用了索引,从而可避免行锁升级表锁。
两个事务A、B,都需要获取两个资源X、Y,A事务先获取到X资源,B事务先获取到Y资源,此时事务A、B并不冲突,现在A事务开始请求Y资源,因Y资源正被B事务加锁占用,从而A事务阻塞并等待B事务释放Y资源,同时,B事务开始请求X资源,因X资源正被A事务加锁占用,从而B事务阻塞并等待A事务释放X资源。我们把这种A、B事务同时阻塞并等待对方资源的状态称为死锁。
在数据发生死锁之后,InnoDB引擎一般都能自动检测到,并使一个事务释放锁并回退,另一个事务从而获得锁,得以完成事务。
但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决,默认值是50s,参数innodb_deadlock_detect可以控制这个逻辑,默认开启。
比如:事务A中有两条操作,一条是获取数据,一条是修改数据,那么事务A没有必要在获取数据时申请共享锁,在修改数据时申请排他锁,只需要在事务开始时,获取排他锁即可。
比如:两个事务A、B,都需要获取两个资源X、Y时,若事务A、B都以相同的顺序来访问资源X、Y,那么就不会产生死锁。
关于优化的问题,其实都是具体场景具体分析,此处只给出了一些常用的经验,具体措施,还需回归场景。
多版本并发控制,Multi-Version Concurrency Control,MVCC。MVCC 是一种并发控制的方法,实现对数据库的并发访问。从前文可知锁就是控制并发操作的,但是系统开销较大,而MVCC可以在大多数情况下代替行级锁,以降低其系统开销。
在InnoDB中有四种隔离级别,其中REPEATABLE READ级别满足事务之间互不影响,同时具有高并发性。这种效果是不能由上面介绍的悲观锁实现的,因为一旦使用悲观锁,高并发性大大降低。本章开头提到了一种乐观锁,它系统开销小,并发性强,所以REPEATABLE READ隔离级别便使用了乐观锁的思想来实现,也就是本节介绍的MVCC。
基于乐观锁的思想,MVCC也采用数据的版本号来保证数据的一致性。MVCC在每一行数据的后面隐式添加两列数据,一列为行数据创建时间,一列为行数据删除时间。它们存储的是对该行数据操作的事务系统版本号,版本号是按照事务开启的顺序递增的。为了方便讲解,此处的系统版本号用事务ID替代,如下所示:
id | name | 行数据创建时间 | 行数据删除时间 |
---|---|---|---|
1 | Damon | 1 | 2 |
该行数据表示,该行是由事务1创建的,同时被事务2删除了。
多事务的情况下,如何保证一个事务能访问到有效数据呢?需要当前事务的系统版本号大于等于创建时间,同时小于删除时间或删除时间为空,那么就表示事务可以访问到该行数据。
本节将以实际的SQL操作介绍MVCC的运行机制,为了讲解清晰,系统版本号用事务ID替代。
当事务1执行如下操作:
start transaction;
insert into student_table values(1,'Damon');
insert into student_table values(2,'Tom');
insert into student_table values(3,'Xuebao');
commit;
事务1完成后,表中的数据如下所示:
id | name | 行数据创建时间 | 行数据删除时间 |
---|---|---|---|
1 | Damon | 1 | undefined |
2 | Tom | 1 | undefined |
3 | Xuebao | 1 | undefined |
因为事务1只进行了插入操作,所以表中各行数据只有创建时间被标记为1。
事务2开始执行如下操作:
start transaction;
Delete from student_table where id = 3;
commit;
事务2完成后,表中的数据如下所示:
id | name | 行数据创建时间 | 行数据删除时间 |
---|---|---|---|
1 | Damon | 1 | undefined |
2 | Tom | 1 | undefined |
3 | Xuebao | 1 | 2 |
因为事务2只进行了删除操作,所以表中id为3的行数据删除时间被标记为2。
事务3开始执行如下操作:
start transaction;
Select * from student_table where id = 1;
Select * from student_table where id = 3;
commit;
事务3的第一句Select:版本号为3大于id为1的行数据的创建时间1且删除时间未定义,所以该语句可以访问到id为1的数据。
事务3的第二句Select:版本号为3大于id为3的行数据的创建时间1,但大于删除时间2,所以该语句不能访问到id为3的数据。
所以事务3执行完后,应返回如下表数据:
id | name | 行数据创建时间 | 行数据删除时间 |
---|---|---|---|
1 | Damon | 1 | undefined |
2 | Tom | 1 | undefined |
InnoDB引擎在执行Update操作时,其实是新添加了一行数据,同时更新老数据行的删除时间。
事务4开始执行如下操作:
start transaction;
Update student_table Set name=Tom2 where id = 2;
commit;
事务4执行后,首先将id为2的行数据删除时间设置为事务4的系统版本号4,同时新插入一行原数据,创建时间为4,删除时间未定义,如下表所示:
id | name | 行数据创建时间 | 行数据删除时间 |
---|---|---|---|
1 | Damon | 1 | undefined |
2 | Tom | 1 | undefined |
3 | Xuebao | 1 | 2 |
2 | Tom2 | 4 | undefined |
本文介绍了MySQL中锁相关的知识点,以乐观锁、悲观锁为主线,依次介绍了MySQL中的5种悲观锁,意向共享锁、意向排他锁、共享锁、排他锁、间隙锁;然后分析了使用悲观锁时常见的三个问题,并给出了锁的性能优化策略;最后详细分析了MySQL的乐观锁,即MVCC机制,并给出增删改查四种情况下MVCC的分析过程。