数据事务设计遵循ACID的原则。
MySQL数据库提供了四种默认的隔离级别,读未提交(read-uncommitted)、读已提交(或不可重复读)(read-committed)、可重复读(repeatable-read)、串行化(serializable)。
MySQL的默认隔离级别是RR。
InnoDB实现了两种标准行级锁,一种是共享锁(shared locks,S锁),另一种是独占锁,或者叫排它锁(exclusive locks,X锁)。
S锁允许当前持有该锁的事务读取行。
X锁允许当前持有该锁的事务更新或删除行。
如果事务T1持有了行r上的S锁
,则其他事务可以同时持有行r的S锁
,但是不能对行r加X锁
。
如果事务T1持有了行r上的X锁
,则其他任何事务不能持有行r的X锁
,必须等待T1在行r上的X锁
释放。
如果事务T1在行r上保持S锁
,则另一个事务T2对行r的锁的请求按如下方式处理:
InnoDB支持多种粒度的锁,允许行级锁和表级锁的共存。例如LOCK TABLES ... WRITE
等语句可以在指定的表上加上独占锁。
InnoBD使用意向锁来实现多个粒度级别的锁定。意向锁是表级锁,表示table中的row所需要的锁(S锁或X锁)的类型。
意向锁分为意向共享锁(IS锁)和意向排它锁(IX锁)。
IS锁表示当前事务意图在表中的行上设置共享锁,下面语句执行时会首先获取IS锁,因为这个操作在获取S锁:
IX锁表示当前事务意图在表中的行上设置排它锁。下面语句执行时会首先获取IX锁,因为这个操作在获取X锁:
事务要获取某个表上的S锁和X锁之前,必须先分别获取对应的IS锁和IX锁。
锁的兼容矩阵如下:
按照上面的兼容性,如果不同事务之间的锁兼容,则当前加锁事务可以持有锁,如果有冲突则会等待其他事务的锁释放。
如果一个事务请求锁时,请求的锁与已经持有的锁冲突而无法获取时,互相等待就可能会产生死锁。
意向锁不会阻止除了全表锁定请求之外的任何锁请求。
意向锁的主要目的是显示事务正在锁定某行或者正意图锁定某行。
常见的锁有Record锁、gap锁、next-key锁、插入意向锁、自增锁等。
下面会对每一种锁给出一个查看锁的示例。
示例的基础是一个只有两列的数据库表。
数据表test
只有两列,id
是主键索引,code
是普通的索引(注意,一定不要是唯一索引),并初始化了两条记录,分别是(1,1),(10,10)。
这样,我们验证唯一键索引就可以使用id列,验证普通索引(非唯一键二级索引)时就使用code列。
要看到锁的情况,必须手动开启多个事务,其中一些锁的状态的查看则必须使锁处于waiting
状态,这样才能在mysql的引擎状态日志中看到。
命令:
这条命令能显示最近几个事务的状态、查询和写入情况等信息。当出现死锁时,命令能给出最近的死锁明细。
Record Lock
是对索引记录的锁定。记录锁有两种模式,S模式和X模式。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;
表示防止任何其他事务插入、更新或者删除id =10
的行。
记录锁始终只锁定索引。即使表没有建立索引,InnoDB也会创建一个隐藏的聚簇索引(隐藏的递增主键索引),并使用此索引进行记录锁定。
开启第一个事务,不提交,测试完之后回滚。
事务加锁情况
可以看到有一行被加了锁。由之前对锁的描述可以推测出,update语句给id=1
这一行上加了一个X锁
。
第一个事务保持原状,不要提交或者回滚,现在开启第二个事务。
执行update
时,sql语句的执行被阻塞了。查看下事务状态:
喜闻乐见,我们看到了这个锁的状态。状态标题是'事务正在等待获取锁',描述中的lock_mode X locks rec but not gap
就是本章节中的record记录锁,直译一下'X锁模式锁住了记录'。后面还有一句but not gap
意思是只对record本身加锁,并不对间隙加锁,间隙锁的叙述见下一个章节。
间隙锁作用在索引记录之间的间隔,又或者作用在第一个索引之前,最后一个索引之后的间隙。不包括索引本身。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
这条语句阻止其他事务插入10和20之间的数字,无论这个数字是否存在。
间隙可以跨越0个,单个或多个索引值。
间隙锁是性能和并发权衡的产物,只存在于部分事务隔离级别。
select * from table where id=1;
唯一索引可以锁定一行,所以不需要间隙锁锁定。
如果列没有索引或者具有非唯一索引,该语句会锁定当前索引前的间隙。
在同一个间隙上,不同的事务可以持有上述兼容/冲突表中冲突的两个锁。例如,事务T1现在持有一个间隙S锁,T2可以同时在同一个间隙上持有间隙X锁。
允许冲突的锁在间隙上锁定的原因是,如果从索引中清除一条记录,则由不同事务在这条索引记录上的加间隙锁的动作必须被合并。
InnoDB中的间隙锁的唯一目的是防止其他事务插入间隙。
间隙锁是可以共存的,一个事务占用的间隙锁不会阻止另一个事务获取同一个间隙上的间隙锁。
如果事务隔离级别改为RC,则间隙锁会被禁用。
按照官方文档,where
子句查询条件是唯一键且指定了值时,只有record锁,没有gap锁。
如果where
语句指定了范围,gap锁是存在的。
这里只测试验证一下当指定非唯一键索引的时候,gap锁的位置,按照文档的说法,会锁定当前索引及索引之前的间隙。(指定了非唯一键索引,例??code=10,间隙锁仍然存在)
开启第一个事务,锁定一条非唯一的普通索引记录
由于预存了两条数据,row(1,1)和row(10,10),此时这个间隙应该是1<gap<10
。我们先插入row(2,2)来验证下gap锁的存在,再插入row(0,0)来验证gap的边界。
开启第二个事务,在code=10
之前的间隙中插入一条数据,看下这条数据是否能够插入。
插入的时候,执行被阻塞,查看引擎状态:
插入语句被阻塞了,lock_mode X locks gap before rec
,由于第一个事务锁住了1到10之间的gap,需要等待获取锁之后才能插入。
如果再开启一个事务,插入(0,0)
可以看到:指定的非唯一建索引的gap锁的边界是当前索引到上一个索引之间的gap。
最后给出锁定区间的示例,首先插入一条记录(5,5)
开启第一个事务:
第二个事务,试图去更新code=5的行:
执行到这里,如果第一个事务不提交或者回滚的话,第二个事务一直等待直至mysql中设定的超时时间。
Next-key锁实际上是Record锁和gap锁的组合。Next-key锁是在下一个索引记录本身和索引之前的gap加上S锁或是X锁(如果是读就加上S锁,如果是写就加X锁)。
默认情况下,InnoDB的事务隔离级别为RR,系统参数innodb_locks_unsafe_for_binlog
的值为false
。InnoDB使用next-key锁对索引进行扫描和搜索,这样就读取不到幻象行,避免了幻读
的发生。
当查询的索引是唯一索引时,Next-key lock会进行优化,降级为Record Lock,此时Next-key lock仅仅作用在索引本身,而不会作用于gap和下一个索引上。
如上述例子,数据表test
初始化了row(1,1),row(10,10),然后插入了row(5,5)。数据表如下:
由于id
是主键