3.1 从数据操作的类型划分:读锁、写锁
对于数据库中并发事务的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-读</span>情况并不会引起什么问题。对于<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-写</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-写</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-读</span>这些情况可能会引起一些问题,需要使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">MVCC</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>的方式来解决它们。在使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>的方式解决问题时,由于既要允许<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-读</span>情况不受影响,又要<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-写</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读-写</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写-读</span>情况下的操作<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">相互阻塞</span>,所以MySQL实现了一种由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock,S Lock)和排他锁(Exclusive Lock, X Lock),也叫读锁(read lock)和写锁(write lock)。
- <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">读锁</span>:也称为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">共享锁</span>、英文用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S</span>表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
- <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写锁</span>:也称为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">排他锁</span>、英文用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X</span>表示。当前写操作没有完成之前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上。
举例(行级读写锁:)如果一个事务T1已经获得了某个行r的读锁,name此时另外的一个事务T2是可以取获得这个行r的读锁的,因为读取操作并没有改变行r的数据;但是,如果事务T3想获得行r的写锁,则必须等待事务T1、T2释放行r上的写锁才行。
总结:这里的兼容是指对同一张表或记录的锁的兼容性情况。
X 锁 | S 锁 | |
---|---|---|
X 锁 | 不兼容 | 不兼容 |
S 锁 | 不兼容 | 兼容 |
1. 锁定读
在采用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">加锁</span>方式解决<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">脏读</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">不可重复读</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">幻读</span>这些问题时,读取一条记录时需要获取该记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>,其实是不严谨的,有时候需要在读取记录时就获取记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT</span>语句格式:
-
对读取的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>:
SELECT ... LOCK IN SHARE MODE; # 或 SELECT ... FOR SHARE;#(8.0新增语法)
在普通的SELECT语句后边加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK IN SHARE MODE</span>,如果当前事务执行了该语句,那么它会为读取到的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>,这样允许别的事务继续获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>(比如说别的事务也使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... LOCK IN SHARE MODE</span>语句来读取这些记录),但是不能获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>(比如使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... FOR UPDATE</span>语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,那么他们会阻塞,直到当前事务提交之后将这些记录上的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>释放掉。
-
对读取的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>
SELECT ... FOR UPDATE;
在普通的SELECT语句后边加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">FOR UPDATE</span>,如果当前事务执行了该语句,那么它会为读取到的记录加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,这样既不允许别的事务获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>(比方说别的事务使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT .. LOCK IN SHARE MODE</span>语句来读取这些记录),也不允许获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>(比如使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SELECT ... FOR UPDATE</span>语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,那么它们会阻塞,直到当前事务提交之后将这些记录上的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>释放掉。
MySQL8.0新特性:
在5.7及之前的版本,SELECT ... FOR UPDATE,如果获取不到锁,会一直等待,直到<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">innodb_lock_wait_timeout</span>超时。在8.0版本中,SELECT ... FOR UPDATE, SELECT ... FOR SHARE添加<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">NOWAIT</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">SKIP LOCKED</span>语法,跳过锁等待,或者跳过锁定。
-
通过添加NOWAIT、SKIP LOCKED语法,能够立即返回。如果查询的行已经加锁:
- 那么NOWAIT会立即报错返回
- 而SKIP LOCKED也会立即返回,只是返回的结果不包含被锁定的行。
select * from t1 for update nowait; select * from t1 for update skip locked;
2. 写操作
平常所用到的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">写操作</span>无非是<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">UPDATE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>这三种:
-
<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>:
针一条记录做DELETE操作的过程其实是先在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后获取这条记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,再执行<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">delete mark</span>操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的锁定读。
-
<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">UPDATE</span>:在对一条记录做UPDATE操作时分为三种情况:
-
情况1:未修改该记录的键值,并且被更新的列占用存储空间在修改见后未发生变化。
则先在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后再获取一下记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁定读</span>。
-
情况2:未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。
则现在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中定位到这条记录的位置,然后获取一下记录的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">B+</span>树中位置的过程看成是一个获取<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁定读</span>,新插入的记录由<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>操作提供的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>进行保护。
情况3:修改了该记录的键值,则相当于在原记录上做<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>操作之后再来一次<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>操作,加锁操作就需要按照<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DELETE</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">INSERT</span>的规则进行了。
-
-
<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>:
一般情况下,新插入一条记录的操作并不加锁,通过一种称之为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">隐式锁</span>的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。
3.2 从数据操作的粒度划分:表级锁、页级锁、行锁
为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">消耗资源</span>的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">并发响应</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">系统性能</span>两方面进行平衡,这样就产生了“<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">锁粒度(Lock granularity)</span>”的概念。
对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表级别</span>进行加锁,自然就被成为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表级锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">表锁</span>,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。
1. 表锁(Table Lock)
该锁会锁定整张表,它是MySQL中最基本的锁策略,并<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">不依赖于存储引擎</span>(不管你是MySQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">开销最小</span>的策略(因为锁粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">避免死锁</span>问题。当然,锁的粒度大所带来最大的负面影响就是出现锁征用的概率也会最高,导致<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">并发率大打折扣</span>。
①表级别的S锁、X锁
在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这张表添加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>的。在对某个表执行一些诸如<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">ALTER TABLE</span>、<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DROP TABLE</span>这类的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DDL</span>语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">DDL</span>语句也会发生阻塞。这个过程其实是通过在<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">server层</span>使用一种称之为<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">元数据锁</span>(英文名:<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">Metadata Locks</span>,简称<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">MDL</span>)结构来实现的。
一般情况下,不会使用InnoDB存储引擎提供的表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>。只会在一些特殊情况下,比方说<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">崩溃恢复</span>过程中用到。比如,在系统变量<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">autocommic=0,innodb_table_locks=1</span>时,<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">手动</span>获取InnoDB存储引擎提供的表t的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>或者<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>可以这么写:
- <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES t READ</span>:InnoDB存储引擎会对表<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">t</span>加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>。
- <span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES t WRITE</span>:InnoDB存储引擎会对表<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">t</span>加表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>。
不过尽量避免在使用InnoDB存储引擎的表上使用<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">LOCK TABLES</span>这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">行锁</span>,关于InnoDB表级别的<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">S锁</span>和<span style="color:#a27e22;background:#e9e9e9;font-size:16px;font-family:Helvetica;">X锁</span>了解一下即可。
举例:下面我们讲解MyISAM引擎下的表锁。