Mysql隔离级别,锁的原理
抛开剂量谈毒性,就是耍流氓。
抛开存储引擎和隔离级别谈锁,也是耍流氓。
所以,先介绍一下存储引擎和隔离级别。
1.存储引擎
MyISAM:每个表有三个文件,.frm文件用于存储表结构,.myd文件用于存储数据,.myi文件用于存储索引
不支持事务,不支持外键,不支持聚集索引,不支持行锁
Innodb:每个表有两个文件,.frm文件用于存储表结构,.idb文件用于存储表的索引和数据
InnoDB为什么推荐使用自增ID作为主键?
答:自增ID可以保证每次插入时B+索引是从右边扩展的,可以避免B+树的频繁合并和分裂(对比使用UUID)。如果使用字符串主键和随机主键,会使得数据随机插入,效率比较差。
2.隔离级别
2.1.脏读
脏读就是指一个事务访问到了另一个事务还未提交的数据
将隔离级别设置为read uncommitted(读未提交)
A:启动事务,查询数据,此时数据为初始状态
B:启动事务,更新数据,但不提交
A:再次读取数据,发现数据已经被修改了,这就是所谓的“脏读”
2.2.不可重复读
不可重复读是指在一个事务内两次读到的数据是不一样的(侧重修改数据)
将隔离级别设置为read committed(已提交读)
A:启动事务,查询数据,此时数据为初始状态
B:启动事务,更新数据,提交事务
A:再次读取数据,发现数据已发生变化,说明B提交的修改被事务中的A读到了,这就是所谓的“不可重复读”
2.3.幻读
幻读是指在一个事务内两次读取到的数据数量不一致(侧重插入数据)
A:启动事务,查询数据,此时数据为初始状态
B:启动事务,插入数据,提交事务
A:再次查询、更新数据,发现数据量不一致,这就是所谓的“幻读”
第二次查询,数据量没有变化,更新的时候,数据量发生变化,是因为MVCC解决了部分幻读的问题
2.4.MVCC (多版本并发控制)
MVCC作用于RC和RR隔离级别,用于解决数据库的并发读写操作
MVCC解决了不可重复读和部分幻读的问题
MVCC的实现原理:
2.4.1
- 6字节的事务ID(DB_TRX_ID)
- 7字节的回滚指针(DB_ROLL_PTR)
事务100插入初始数据
事务200更改数据
当事务200更改该行的值时,会进行如下操作:
- 用排他锁锁定该行
- 记录redo log
- 把该行修改前的值copy到undo log
- 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行
- 当事务正常提交时Innbod只需要更改事务状态为COMMIT即可,不需做其他额外的工作
- 而Rollback则稍微复杂点,需要根据当前回滚指针从undo log中找出事务修改前的版本,并恢复
- Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。
2.4.2
2.4.3
为了判断版本链中的哪个版本是当前事务可见的,提出了ReadView的概念,它包含4个主要内容
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表;
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值;
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值;
creator_trx_id:表示生成该ReadView的事务的事务id。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断
RC隔离级别下
每次查询都新建一个read view
事务100第一次查询, m_ids{100,200},版本链{90},90符合A,可见
事务100第二次查询,m_ids{100,200},版本链{90,200},200符合D而且在列表中,不可见,然后看90
事务100第三次查询,m_ids{100},max_trx_id=201,版本链{90,200},200符合D而且不在列表中,可见
事务100第四次查询,m_ids{100},max_trx_id=301,版本链{90,200,300},300符合D而且不在列表中,可见
事务100第五次查询,m_ids{100},版本链{90,200,300,100},100符合C,可见
RR隔离级别下
一个事务中每次查询使用一个read view:m_ids{100,200},max_trx_id=201
事务100第一次查询,版本链{90},90符合A,可见
事务100第二次查询,版本链{90,200},200符合D而且在列表中,不可见,然后看90
事务100第三次查询,版本链{90,200},200符合D而且在列表中,不可见,然后看90
事务100第四次查询,版本链{90,200,300},300符合B,不可见,然后看200
事务100第五次查询,版本链{90,200,300,100},100符合C,可见
3.锁
共享锁、排他锁、意向共享锁、意向排他锁、记录锁、间隙锁、临键锁、插入意向锁、自增锁
3.1.共享锁(S)
共享锁也称读锁,行级别的锁,当前事务加共享锁之后,其他事务可以加共享锁,但不能加排他锁
加锁方式:select colname from tablename where ... lock in share mode;
insert into tablename select colname from tablename2 (对tablename2自动加共享锁)
A事务对数据加共享锁之后,B事务也可以对同一数据加共享锁,但加排他锁超时失败
3.2.排他锁(X)
排他锁也称写锁,行级别的锁,当前事务加排他锁之后,其他事务不能加共享锁和排他锁
加锁方式:select colname from tablename where ... for update;
insert ... delete... update...
3.3.意向共享锁(IS)
表级别的锁,加共享锁之前,先要获取意向共享锁
3.4.意向排他锁(IX)
表级别的锁,加排他锁之前,先要获取意向排他锁
3.5.记录锁(Record Lock)
锁精确加在某一行上,基于主键和唯一索引
select * from test where id=5 for update;//该句只锁id=5这一行
3.6.间隙锁(Gap Lock)
只在RR隔离级别下生效
锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前(负无穷,first-key)或最后一个索引之后(last-key,正无穷)的无限空间。
下表中的间隙锁(-∞,3),(3,5)......(13,+∞)
3.7.临键锁(Next-Key Lock)
基于非唯一索引的,每个非唯一索引上都有一把临键锁
只在RR隔离级别下生效
记录锁与间隙锁组合起来用就叫做临键锁
下表中存在的临键锁:(-∞,3],(3,5],......(13,+∞)
3.8.插入意向锁(Insert Intention)
插入意向锁是一种Gap锁,不是意向锁
插入的时候会获取当前间隙的gap锁,和插入行的X锁
假如有一个索引间隙4-7,不同的事务分别插入5和6,每个事务都会产生一个(4,7)插入意向锁,所以插入意向锁和插入意向锁是兼容的
插入意向锁和间隙锁是不兼容的
4.案例分析
id是主键,name是普通索引
4.1.唯一键+存在记录
select * from test where id = 5 for update;
只锁id=5的这一行
4.2.唯一键+不存在记录
select * from test where id = 18 for update;
RR隔离级别:获得间隙锁(16,20),id为17,18,19的不能插入,id为16,20的可以修改
RC隔离级别:只锁猪id=18的行锁
4.3.普通索引+存在记录
select * from test where num = 7 for update;
RR隔离级别:获得临键锁(5,7]和区间锁(7,9),num为6,8的不能插入,num为5,9的可以修改
RC隔离级别:获得num=7的行锁
4.4.普通索引+不存在记录
select * from test where num = 12 for update;
RR隔离级别:获得间隙锁(10,13)
RC隔离级别:获得num=12的行锁
5.死锁
两个事务都持有对方需要的锁,并且在等待对方释放,并且双方都不会释放自己的锁。
案例1
循环对同一资源加锁
解决办法:两个事务的业务顺序一致
案例2
S锁X锁
解决办法:把S锁改成X锁
案例3
Gap锁
解决办法:大事务化小事务,或者降低隔离级别
6.锁状态查询
https://www.sohu.com/a/253338562_495675
http://www.bubuko.com/infodetail-3344103.html
参考:https://tech.meituan.com/2014/08/20/innodb-lock.html
https://blog.csdn.net/sofia1217/article/details/50778906