(本文为了方便,英文关键词都都采用小写方式,相关知识点会简单介绍,争取做到可以独立阅读)
文章开始我会先介绍本文需要的知识点如下:
- innodb的主键索引(一级索引)和二级索引(辅助索引)的知识
- innodb的隔离级别(isolation level)
- MVCC(Multi-Version Concurrent Control)多版本并发控制
- 数据的脏读、幻读(如果有时间会详细讲一下脏读如果没时间,网上讲这个地方的也很多)
- 简单的sql知识(能读懂sql语句)
我们先看一个mysql表和几条语句,方便后面使用
表名称: my_table
搜索引擎:innodb
表结构:
1. select * from my_table where id = 1;
2. select * from my_table where id = 1 lock in share mode;
3. select * from my_table where id = 1 for update;
4. update my_table set address = 'tianjin' where id = 1;
先说 隔离级别,mysql 隔离级别分为四种:
未提交读( read uncommitted )、提交读(read committed)、重复读(repeatable read)、序列化(serializable)
其中mysql默认的隔离级别为重复读(repeatable read),以下简称为rr,本文也只介绍这种模式
问题1:读有几种模式、加锁有几种方式
读的模式分为两种:
- 快照读(snapshot read)
- 当前读(current read)
在聊读模式之前,我们需要先来了解一下MVCC:
MVCC是为了实现数据库的并发控制而设计的一种协议。与其相对的是LBCC 即基于锁的并发控制(Lock-Based Concurrent Control)。要实现数据库的并发访问控制,最简单的做法就是加锁访问,即读的时候不能写( 这个读为当前读,后面介绍。允许多个线程同时对想读的内容加锁,即共享锁或叫S锁),写的时候不能读(只能有一个线程对同一内容进行写操作,即排它锁,X锁)。这样的加锁访问,其实并不算是真正的并发,或者说它只能实现并发的读,既读写串行化,这样就大大降低了数据库的读写性能。
LBCC是四种隔离级别中级别最高的Serialize隔离级别。MVCC对比LBCC它的最大好处便是, 读不加锁,读写不冲突,在需要加锁的时候,尽可能的少锁定行(锁策略, 后面有讲 )。
快照读(Snapshot read)和当前读(current read)释义:
-
快照读,读取的是记录的可见版本(可能是历史版本,即最新的数据可能正在被当前执行的事务并发修改),不会对返回的记录加锁,如上面的sql语句1;
-
当前读,读取的是记录的最新版本,并且会对返回的记录加锁,保证其他事务不会并发修改这条记录。如上面的sql语句2,3,4。不同的是2加的是s锁,3、4加的是x锁,insert加的也是x锁。
注:MVCC只在RC和RR两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容
加锁的方式:
mysql进行并发控制有两种锁:
- 共享锁(s锁):也称为读锁,是因为一般是读的过程中加的锁,一个事务加s锁的时候,另一个事务还可以获得s锁,例如语句2
- 排他锁(x锁):也称为写锁,一个事务加x锁的时候,其他事务拿不到锁,只能等待。例如语句3和语句4
先看一个sql语句
update my_table set name ='zhang' where id = 1;
假设id为主键:此条sql执行的时候会给此行数据加x锁,如下图
mysql的innodb默认的隔离模式为 RR模式,既可重复读, Innodb的RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),因此不存在幻读现象。但是 标准的RR只能保证在同一事务中多次读取同样记录的结果是一致的,而无法解决幻读问题。Innodb的幻读解决是依靠MVCC的锁策略实现机制做到的。
主键索引(一级索引)和二级索引
innodb中在主键上存在聚簇索引类型的一级索引,其他的索引均聚簇索引类型的二级索引,这里做一下简单介绍
一级索引:在innodb存储引擎中,主键的存在至关重要,及时你不为表设置主键,存储引擎也会隐式的定义一个主键,只是对用户来说透明。之所以说他重要,是因为聚簇索引的存储是和数据存储在一起的,而聚簇索引的数据就是数据存储的顺序。如果需要查找的数据是连续的,那么按照聚簇索引查找到的数据位置也是连续的,只需要按顺序读取就可以。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页(这里和后面的二级索引有区别,二级索引的叶子节点存储的是主键,需要再进行回聚集索引上(简称回表)查询真实的数据。 )。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。
二级索引:表数据存储顺序与索引顺序无关。对于二级索引,其也是聚集索引,但是叶结点不包含真实行数据,只包含索引字段值及主键,其行数量与数据表行数据量一致。
mvcc的锁策略
update my_table set name ='zhang' where id = 1;
看上面的sql语句,或者看之前的几条sql,这个语句执行的时候会给这条记录加x锁,这时候如果其他事务中的语句也在进行锁的操作(既更新、插入或者删除,以及语句2当前读操作加的s锁)就会造成锁争用(innodb出现锁争用的时候处理方式为回滚超时获取不到锁的事务)这种操作当然是惨痛的。
我们在上面说了mvcc的锁策略是尽量减少锁定的行。而且还要解决幻读的问题,所以有了一系列的锁策略(先看总结,后面有实例)。
行锁
对于查询条件为主键和唯一索引的语句:是行级锁,只锁定满足条件的行,record lock。
间隙锁 (gaplock)
对于查询条件为非唯一索引:是范围锁定,锁定范围为索引上按照条件需要扫描的范围
表锁
对于查询条件没有用到索引的语句:直接锁定全表。
实例讲解
我们先按照上面四条语句两条并发时的相互影响的情况来
情况1:id为主键
1. select * from my_table where id = 1;
2. select * from my_table where id = 1 lock in share mode;
我们上面说过,语句1为快照读,对其他的读或者写没有影响。所以这两条语句并行时,1读快照,2为语句加s锁。
select * from my_table where id = 1 lock in share mode;
情况2:id为主键
2. select * from my_table where id = 1 lock in share mode;
3. select * from my_table where id = 1 for update;
其中语句2加s锁,3加x锁(在数据被加s锁的时候,其他的给这条想要读取这条记录也需要给这条记录加s锁,这就是为什么s锁是共享锁。此时是不允许再给这条记录加x锁的)两种锁是不能同时存在在一条记录上的。所以两条语句谁先上锁谁先执行,另一个等待。
情况3:id为主键
3. select * from my_table where id = 1 for update;
4. update my_table set address = 'tianjin' where id = 1;
这种情况下两条语句都需要给数据加x锁,所以显然不能并发执行。
下面我们来讨论一下id不为主键的情况
id若不为主键,则不能使用主键索引,而在innodb中有一下几种情况
- 二级唯一索引
- 二级不唯一索引
- 没有索引
由于只要不是快照读则一定会加锁,我们已经了解了锁的形式,则不难明白不论是先加x锁还是s锁哪一种,都一定不能再加另一种锁,所以我们下面只分析加锁的方式
情况4:假设id为二级唯一索引(unique)
4. update my_table set address = 'tianjin' where id = 1;
这里很明显需要加x锁,但是这里的加锁和id为主键(索引为主键索引)的情况加锁不完全一样,会稍微复杂一点 。
这个时候我们需要对索引知识有一定的了解,上面说过二级索引中的叶子节点存储的除了索引信息还有主键,也就是说我们需要先在二级唯一索引中查找到这条记录的主键,然后通过主键去查找到数据实际的存储位置并给这条数据加锁。注意,这里的加锁应该是加在了索引上和数据本身上(或者说是聚簇索引上也可以,因为两者是存储在一个结构中的)而不只是二级唯一索引上。
情况5:age为二级非唯一索引,id为主键
5. update my_table set address = 'tianjin' where age = 25;
此种情况比前一种情况更特殊,因为情况3和4都只能找到一条记录,只需要对这条记录加锁,则不会发生结果集被修改的情况。但是如果age为二级非唯一索引,我们看到如下表格中有两条记录age=25
如果我们在update的过程中,有一个用户插入了一条age也为25的数据,那么就是发生一种现象,你明明更新了所有的age=25的数据,但是执行完了却有一条数据没有更新的幻觉,这就是幻读(可以自行查找资料,避免本文过长)。这个时候显然只给查找出的数据加锁是解决不了这个问题的。所以就有了gap锁(间隙锁字面上可能更好理解)这里需要画图大家理解一下:
如图这里在age为25的有两个 ,id分别为1和3。我们在修改执行上面语句的时候,如果没有gap锁,则可能发生一种情况:另一个事务执行如下语句
update my_table set age=25 where id=2;
则发生幻读现象。gap锁可以防止在语句或者事务执行过程中有满足条件的记录插入进来造成幻读。所以说在此种情况下,除了给满足条件的二级索引和数据(或聚簇索引)加x锁之外还要给相关的间隙加锁。可以理解为这个加gap锁,不只是锁记录,还要锁边界。是在二级索引的范围内防止新的索引项加入,因为二级索引本身也是有序的。 情况6:age上无索引,id为主键
5. update my_table set address = 'tianjin' where age = 25;
这种情况下,所有记录都被加上了X锁,每条记录间的间隙(GAP)也同时被加上了GAP锁,其实就是锁表了。