编程知识 cdmana.com

MySQL数据库锁机制

01 概述

    数据库事务ACID中的隔离性是通过锁和MVCC实现的,锁用于并发写操作,MVCC用于并发读操作。因此,数据库锁是控制并发的一种手段。

02 分类

2.1 粒度锁/范围锁

    根据锁的粒度,可以分为:

    记录锁:锁住行

    表锁:锁住表

    页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

    数据库锁:锁住整个库

2.2 算法锁

    算法锁(基于行锁的算法):记录锁,间隙锁,临键锁。

    注:记录锁、间隙锁、临键锁,都属于排它锁。

 2.3 属性锁

    在MySQL中,锁可以分为两类:

    共享锁Shared Locks(简称S锁,属于行锁):共享锁是将对象数据变为只读形式,不能进行更新,所以也称为读取锁定;

    排他锁Exclusive Locks(简称X锁,属于行锁):排他锁是执行INSERT/UPDATE/DELETE时,其他事务不能读取该数据,因此也称为写入锁定;

    意向锁

    为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

    当一个事务试图对整个表进行加锁(共享锁或排它锁)之前,首先需要获得对应类型的意向锁(意向共享锁或意向共享锁)。

    意向共享锁Intention Shared Locks(简称IS锁,属于表锁),表示事务准备给数据行加上共享锁,也就是说一个数据行在加共享锁之前必须先取得该表的IS锁;

    意向排他锁Intention Exclusive Locks(简称IX锁,属于表锁),表示事务准备给数据行加上排他锁,也就是说一个数据行加排他锁之前必须先取得该表的IX锁。

    注:意向锁是InnoDB数据操作之前自动加的,不需要用户干涉。

2.4 状态锁

    基于属性锁的状态:意向共享锁,意向排它锁。

2.5 读写锁

    根据读写行为可以分为读锁和写锁。

    读锁:共享锁、shared locks、S锁

    写锁:排他锁、exclusive locks、X锁

 

 读锁

 对于普通SELECT语句,InnoDB不会加任何锁;

    select ... lock in share mode:共享锁

    select ... for update:排他锁

    写锁

    1、DELETE:删除一条数据时,先对记录加X锁,再执行删除操作;

    2、INSERT:插入一条记录时,会先加“隐式锁”来保护这条新插入的记录在本事务提交前不被别的事务访问;

    3、UPDATE:

    如果被更新的列,修改前后没有导致存储空间变化,那么会先给记录加X锁,再直接对记录进行修改;

    如果被更新的列,修改前后导致存储空间发生了变化,那么会先给记录加X锁,然后将记录删除,再INSERT一条新记录。

    隐式锁:一个事务插入一条记录后,还未提交,这条记录会保存本次事务id,而其他事务如果想来对这个记录加锁时(比如执行update、delete操作)会发现事务id不对应,这时会产生X锁,所以相当于再插入一条记录时,隐式地给这条记录加了一把隐式X锁。

03 表锁与行锁

3.1 表锁

    表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。

    最常使用的MYISAM与INNODB都支持表级锁定(行锁失效时使用表锁)。

 3.2 行锁

    行级锁是Mysql中锁定粒度最细的一种锁(该锁是对索引记录进行加锁,锁是在加索引上而不是行上的。

    行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表锁。

3.3 区别

    行级锁:开销大,加锁慢,会出现死锁,锁粒度小,发生锁冲突的概率最低,并发度最高。

    表级锁:开销小,加锁快,不会产生死锁,锁粒度大,发生锁冲突的概率最高,并发度最低。

3.4 选择

    表级锁更适合查询为主,只有少量按索引更新数据的应用场景,如web应用;而行级锁则更适合于有大量按照索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLAP)系统。

04 间隙锁与临键锁

4.1 记录锁

    记录锁/普通行锁:事务加锁后锁住的只是表的某一条记录。

    条件:

    1、键值在条件范围内

    2、记录存在

    作用:

    加了记录锁之后可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

4.2 间隙锁

    当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

    注意:间隙锁只会在 Repeatable read隔离级别下使用。

    条件:

    1、范围查询(非等值查找)并且查询未命中记录;

    2、查询条件必须命中索引;

    3、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。

    注:MySQL默认事务隔离级别就是RR,所以可以借助间隙锁消除幻读(默认的行锁是临键锁,已经包含了间隙锁)。

    作用:

    防止幻读问题

4.3 临键锁

    临键锁(行-间隙锁)是InnoDB的行锁默认算法,它是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。

    注:间隙锁所锁定的区间是一个左开右闭的集合,而临键锁锁定是当前记录的区间和下一个记录的区间。

    条件:

    1、范围查询并命中;

    2、查询命中了索引。

    注:行锁命中索引和记录,间隙锁命中索引,未命中范围,临键锁命中索引,且命中范围查找。

    作用:

    结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。

05 乐观锁与悲观锁

5.1 乐观锁

    乐观锁(Optimistic Concurrency Control)的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。

    乐观锁的特点是先进行业务操作,不到万不得已不去拿锁,即“乐观”的认为拿锁多半是成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁机制其实就是在数据库表中引入一个版本号(version)字段来实现的。

 5.2 悲观锁

    之所以叫做悲观锁,是因为它总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁,当其他线程想要访问数据时,都需要阻塞挂起。

MySQL基本都是使用悲观锁,共享锁和排他锁都是属于悲观锁(可以理解为悲观锁的具体实现)。悲观锁也叫作排它锁,在Mysql中是基于for update来实现加锁的。

5.3 区别

    乐观锁是一种思想,不是数据库层面上的锁,是需要自己手动去加的锁。

    悲观锁用的就是数据库的行锁。

5.4 选择

    乐观锁适用于写少读多的情景。

    悲观锁适用于写多读少的情景。

06 查看锁信息

    有多种方法可以查看InnoDB中锁的情况,例如:

    1、select * from information_schema.innodb_locks; #锁的概况

    2、show engine innodb status; #InnoDB整体状态,其中包括锁的情况

07 锁与索引

    行锁是基于索引实现的,如果索引失效,则行锁转表锁。可以这样理解,在加行锁的时候,InnoDB总会尽可能降低加锁的范围,避免全表锁定,此时只能借助索引区快速定位具体行。

08 锁与事务

    数据库事务的隔离性通过锁实现,锁又可以通过表锁或者行锁实现。

09 锁与并发

9.1 基于锁的并发控制流程

    1、事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁);

    2、申请锁的请求被发送给锁管理器,锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否存在冲突,决定是否为该请求授予锁;

    3、若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。

9.2 可能出现的问题

    1、死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。

    2、饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。

    对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。

    拓展:除了锁可以实现并发控制之外,还有其他策略:

    1、基于时间戳的并发控制(乐观锁思想)

    2、基于有效性检查的并发控制

    3、基于快照隔离MVCC的并发控制

10 锁优化

10.1 MYISAM存储引擎锁优化

    concurrent_insert:控制并发插入行为

    1、concurrent_insert=0时,不允许并发插入。

    2、concurrent_insert=1时,如果 MyISAM 表中没有空洞(即表中没有被删除的行),允许一个进程读表时,另一个进程向表的尾部插入记录(MySQL 默认设置)。

    注:数据库行记录被删除后,并不是真正从磁盘上清除,只是给打上“已删除”标签。删除以后的物理空间不能被新的记录所使用,从而形成了空洞。

    3、concurrent_insert=2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。

    如果在数据插入的时候,没有并发删除操作的话,可以尝试把 concurrent_insert设置为 1。

    反之,在数据插入的时候有删除操作且量较大时,也就是会产生“空洞”的时候,就需要把concurrent_insert设置为2。

    low_priority_updates:调节读写行为优先级

    数据库以读为主时,要优先保证查询性能时,可通过low_priority_updates=1设置读优先级高于写优先级。

    数据库以写为主时,则不用设置 low_priority_updates参数。

10.2 InnoDB存储引擎锁优化

    InnoDB模式的原因。因此,InnoDB锁优化要尽可能避免使用表锁。

    InnoDB存储引擎的优化主要从索引和避免死锁两方面入手,具体如下:

    1、尽可能让数据检索通过索引完成,避免InnoDB因为无法通过索引加行锁,而导致升级为表锁的情况;

    2、尽量减少给予范围的数据检索(间隙锁),避免因为间隙锁带来的影响,锁定了不该锁定的记录;

    3、不同的程序访问一组表时,应尽量约定以相同的顺序访问各表;

    4、尽量使用较低的隔离级别。

版权声明
本文为[程序员姜戈]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/4802134/blog/4716431

Tags InnoDB
Scroll to Top