一、概述:
锁是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等)需要保证这个数据在任何时刻最多只有一个线程在访问,保证数据的完整性和一致性。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要。
在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性需要对并发操作进行控制,因此产生了锁。同时锁机制也为实现MySQL的各个隔离级别提供了保证。 锁冲突也是影响数据库并发访问性能的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。
二、MySQL并发事务访问相同记录:
1.读-读情况:并发事务相继读取相同的记录,读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生;
2.写-写情况:在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生,所以在多个未提交事务相继对一条记录做改动时需要让它们排队执行,这个排队的过程其实是通过锁 来实现的。所谓的锁其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的,当一个事务想对这条记录做改动时,首先会看看内存中是否有与这条记录关联的锁结构,没有就会在内存中生成一个锁结构与之关联。如果有就会等待,当提交后便会释放掉它生成的锁结构;
不加锁:不需要在内存中生成对应的锁结构,可以直接进行操作;
加锁成功:在内存中生成了对应的锁结构,而且锁结构的is_waiting属性为false,也就是事务可以继续执行操作
加锁失败:在内存中生成了对应的锁结构,但是is_waiting属性为true,事务需要等待,不可以继续执行操作;
3.读—写或写—读情况:
读—写或写—读,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题。各个数据库厂商对SQL标准的支持都可能不一样。比如MySQL在REPEATABLE READ隔离级别上就已经解决了幻读问题。
三、锁的不同角度分类:
1.从数据操作的类型划分:读锁和写锁
读锁:也称为共享锁,英文用S表示,针对同一份数据多个事务的读操作可以同时进行而不会相互影响,相互不阻塞的;
写锁:称为排它锁,英文用X表示,当前写操作没有完成前会阻断其他写锁和读锁,这样就能确保在给定的时间里只有一个事务能执行写入,防止其他用户读取正在写入的同一资源;
对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上;
X锁 | S锁 | |
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
(1)锁定读:在采用加锁方式解决脏读 、 不可重复读 、 幻读这些问题时,读取一条记录时需要获取该记录的S锁其实是不严谨的,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录。对读取的记录加S锁格式如下:
SELECT ... LOCK IN SHARE MODE;
SELECT ... FOR SHARE;
在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取的记录加S锁,这样允许别的事务继续获取这些记录的S锁,但是不能获取这些记录的X锁,如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。
对读取的记录加X锁:
SELECT ... FOR UPDATE;
在普通的SELECT语句后边加FOR UPDATE,如果当前事务执行了该语句,那么它会为读取的记录加X锁,这样既不允许别的事务获取这些记录的S锁,也不允许获取这些记录的X锁,如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞直到当前事务提交之后将这些记录上的X锁释放掉。
(2)写操作:
A.DELETE:
对一条记录做DELETE操作的过程其实是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行delete mark操作。可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。
B.UPDATE:
情况一:未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。则先在B+ 树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读。
情况二:未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。则先在 B+ 树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读 ,新插入的记录由INSERT操作提供的隐式锁进行保护。
情况三:修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。
C.INSERT:
一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。
四、从数据操作的粒度划分:表级锁、页级锁、行锁
1.表锁(Table Lock):
表锁会锁定整张表,它是MySQL中最基本的锁策略,并不依赖于存储引擎(不管是MySQL的什么存储引擎对于表锁的策略都是一样的),并且表锁是开销最小的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题,当然锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣。
(1)表级别的S锁、X锁:
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。在对某个表执行一些诸如ALTER TABLE、 DROP TABLE这类的 DDL语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(英文名:Metadata Locks,简称MDL)结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的S读和X锁。只会在一些特殊情况下比方说崩溃恢复过程中用到:
LOCK TABLES t READ : InnoDB存储引擎会对表t加表级别的S锁;
LOCK TABLES t WRITE: InnoDB存储引擎会对表t加表级别的X锁;
锁类型 | 自己可读 | 自己可写 | 自己可操作其他表 | 他人可读 | 他人可写 |
读锁 | 是 | 否 | 否 | 是 | 否 |
写锁 | 是 | 是 | 否 | 否 | 否 |
MyISAM在执行查询语句前,会给涉及的表加读锁,在执行增删改操作前会给涉及的表加写锁,InnoDB存储引擎不会为表添加表级别的读锁或者写锁.
(2)意向锁(intention lock):
InnoDB支持多粒度锁,允许行级锁和表级锁共存,意向锁就是其中的一种表锁;如果给某一行数据加上排它锁,数据库会自动给更大一级的空间加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁,这样当其他人想要获取数据表排它锁的时候只需要了解是否有人已经获取了这个数据表的意向排他锁即可。
a.意向锁的存在是为了协调行级锁和表锁的关系、支持多粒度的锁并存;
b.意向锁是一种不与行级锁冲突的表级锁
c.表明某个事务正在某些持有了锁或者该事务准备去持有锁
d.如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁
SELECT column FROM table_name ... LOCK IN SHARE MODE;
e.如果事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁
SELECT column FROM table_name ... FOR UPDATE;
意向共享锁(IS) | 意向排他锁(IX) | |
意向共享锁(IS) | 兼容 | 兼容 |
意向排他锁(IX) | 兼容 | 兼容 |
意向共享锁(IS) | 意向排他锁(IX) | |
共享锁(S) | 兼容 | 互斥 |
排他锁(X) | 互斥 | 互斥 |
f.InnoDB支持多粒度锁,特定场景下行级锁可以与表级锁共存
g.意向锁之间互补排斥,但除了IS与S兼容外,意向锁会与共享锁和排它锁互斥
h.IX、IS是表级锁,不会和行级的X、S锁发生冲突,只会和表级的X、S发生冲突
i.意向锁在保证并发性的前提下实现了行级锁和表级锁共存且满足事务隔离性的要求
(3)自增锁(AUTO-INC锁)
插入数据的三种方式:
Simple inserts:简单插入,预先确定要插入的行数,包括没有嵌套子查询的单行和多行INSERT ... VALUES()和REPLACE语句;
Bulk inserts:批量插入,事先不知道要插入的行数,包括INSERT ... SELECT,REPLACE ... SELECT和LOAD DATA语句;
Mixed-mode inserts:混合模式插入,是Simple inserts语句但是指定部分新行的自动递增值;
AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
innodb_autoinc_lock_mode有三种取值,分别对应不同的锁定模式:
innodb_autoinc_lock_mode = 0(“传统”锁定模式):在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。使得语句中生成的auto_increment为顺序,且在bin log中重放的时候可以保证master与slave中数据的auto_increment是相同的。因为是表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会限制并发能力。
innodb_autoinc_lock_mode =1(“连续“锁定模式):在这个模式下,“bulk inserts”仍然使用AUTO.INC表级锁,并保持到语句结束。对于“simple inserts”(要插入的行数事先已知),则通过在 mutex(轻量锁)的控制下获得所需数量的自动递增值来避免表级AUT0-INC锁,它只在分配过程的持续时间内保持,而不是直到语句完成,不使用表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则"simple inserts"等待AUTO-INC锁如同它是一个“bulk inserts”。
innodb_autoinc_lock_mode = 2("交错"锁定模式):在这种锁定模式下,所有类INSERT语句都不会使用表级AUTO-INC锁,并且可以同时执行多个语句。这是最快和最可扩展的锁定模式,但是当使用基于语句的复制或恢复方案时,从二进制日志重播SQL语句时,这是不安全的。在此锁定模式下,自动递增值保证在所有并发执行的所有类型的insert语句中是唯一且单调递增的。但是由于多个语句可以同时生成数字(即跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的。
(4)元数据锁(MDL锁):当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加MDL写锁,读锁之间不互斥,因此可以有多个线程同时对一张表增删改查,读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。这样就解决了DML和DDL操作之间的一致性问题。DML锁不需要显式使用,在访问一个表的时候会被自动加上。
2.InnoDB中的行锁:
行锁(Row Lock)也称为记录锁,顾名思义就是锁住某一行(某条记录row)。需要的注意的是MySQL服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。行锁的锁定粒度小,发生锁冲突概率低,可以实现的并发度高。但是对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。
(1)记录锁(Record Locks):记录锁就是仅仅把一条记录锁上。记录锁是有S锁和X锁之分的,当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
(2)间隙锁(Gap Locks):gap锁的提出仅仅是为了防止插入幻影记录而提出的,如果对一条记录加了gap锁(不论是共享gap锁还是独占gap锁)并不会限制其他事务对这条记录加记录锁或者继续加gap锁。
(3)临键锁(Next-Key Locks):next-key锁的本质就是一个记录锁和一个gap锁的合体,既能保护该条记录,又能阻止别的事务将新记录插入被保护记录的前边的间隙
(4)插入意向锁(Insert Intention Locks):插入意向锁是在插入一条记录行前由INSERT操作产生的一种间隙锁。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时事务之间不需要互相等待。插入意向锁是一种gap锁,不是意向锁,在insert操作时产生;插入意向锁是一种特殊的间隙锁,并且插入意向锁之间互不排斥,即使多个事务在同一取件插入多条记录,只要记录本身不冲突,事物之间就不会出现冲突等待;
3.页锁:页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当使用页锁的时候会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
五、从对待锁的态度划分:乐观锁、悲观锁
1.悲观锁(Pessimistic Locking):悲观锁是一种思想,顾名思义就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。悲观锁总是假设最坏的情况,每次去拿数据的时候都认为会修改,所以每次在拿数据的时候都会上锁,这样想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
2.乐观锁(Optimistic Locking):乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上可以采用版本号机制或者CAS机制 实现,乐观锁适用于多读的应用类型,这样可以提高吞吐量。
3.使用场景:
(1)乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
(2)悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式可以在数据库层面阻止其他事务对该数据的操作权限,防止读-写和写-写的冲突。
六、按照加锁的方式划分:显式锁、隐式锁