关于InnoDB的读写锁类型以及加锁方式
(本文为了方便,英文关键词都都采用小写方式,相关知识点会简单介绍,争取做到可以独立阅读)
文章开始我会先介绍本文需要的知识点如下:
- innodb的聚簇索引(聚集索引)和非聚簇索引(二级索引、非聚集索引)的知识
- innodb的隔离级别(isolation level)
- 简单的sql知识(能读懂sql语句)
- MVCC(Multi-Version Concurrent Control)多版本并发控制
- 数据的脏读、幻读(如果有时间会详细讲一下脏读如果没时间,网上讲这个地方的也很多)
问题1:读有几种模式、加锁有几种方式
我们先看一个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,本文也只介绍这种模式
读的模式分为两种:
- 快照读(snapshot read)
- 当前读(current read)
我们先来了解一下MVCC:
MVCC是为了实现数据库的并发控制而设计的一种协议。与其相对的事LBCC即基于锁的并发控制(Lock-Based Concurrent Control)。要实现数据库的并发访问控制,最简单的做法就是加锁访问,即读的时候不能写(这个读为当前读,后面介绍。允许多个线程同时对想读的内容加锁,即共享锁或叫S锁),写的时候不能读(只能有一个线程对同一内容进行写操作,即排它锁,X锁)。这样的加锁访问,其实并不算是真正的并发,或者说它只能实现并发的读,既读写串行化,这样就大大降低了数据库的读写性能。
LBCC是四种隔离级别中级别最高的Serialize隔离级别。MVCC对比LBCC它的最大好处便是,读不加锁,读写不冲突。在MVCC中,读操作可以分成两类,快照读(Snapshot read)和当前读(current read)。快照读,读取的是记录的可见版本(可能是历史版本,即最新的数据可能正在被当前执行的事务并发修改),不会对返回的记录加锁,如上面的sql语句1;而当前读,读取的是记录的最新版本,并且会对返回的记录加锁,保证其他事务不会并发修改这条记录。如上面的sql语句2,3,4。不同的是2加的是s锁,3、4加的是x锁,insert加的也是x锁。
注:MVCC只在RC和RR两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容
加锁的方式:(未涉及意向锁)
先看一个sql语句
update my_table set name ='zhang' where id = 1;假设id为主键:此条sql执行的时候会给此行数据加x锁,如下图
mysql的innodb默认的隔离模式为RR模式,既可重复读,Innodb的RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),因此不存在幻读现象。但是标准的RR只能保证在同一事务中多次读取同样记录的结果是一致的,而无法解决幻读(不保证在事务中出现)问题。Innodb的幻读解决是依靠MVCC的实现机制做到的。其他模式以后有时间再加在这里不对其他模式做讲解。
这里因为id为主键,innodb中在主键上存在聚簇索引,其他的索引均为二级索引,这里做一下简单介绍
聚簇索引:在innodb存储引擎中,主键的存在至关重要,及时你不为表设置主键,存储引擎也会隐式的定义一个主键,只是对用户来说透明。之所以说他重要,是因为聚簇索引的存储是和数据存储在一起的,而聚簇索引的数据就是数据存储的顺序。如果需要查找的数据是连续的,那么按照聚簇索引查找到的数据位置也是连续的,只需要按顺序读取就可以。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页(这里和后面的二级索引有区别,二级索引的叶子节点指向数据页数据行的逻辑指针,需要按照指针再去检索数据)。 在一张表上最多只能创建一个聚集索引,因为真实数据的物理顺序只能有一种。
二级索引:表数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,其行数量与数据表行数据量一致。
看上面的sql语句,或者看之前的几条sql,这个语句执行的时候会给这条记录加x锁,这时候如果其他事务中的语句也在进行锁的操作(既更新、插入或者删除,以及2语句当前读操作加的s锁)就会造成锁争用(innodb出现锁争用的时候处理方式为回滚超时获取不到锁的事务)。
我们先按照上面四条语句两条并发时的相互影响的情况来
情况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锁。在实际情况中可能sql更复杂,如果用到了其他列索引,mysql会按照索引顺序筛选结果,也许并不会进行全表加x锁和gap锁。
注:转自自己的博客
希望大家多评论交流,互相学习