1、加锁的原因
事务并发造成的数据不一致,举例如下:
事务A:update test set k = k + 1 where id = 1;
事务B:update test set k = k + 1 where id = 1;
按照正常执行的话,提交完事务A和B后,k会变成k+2;
如果不对对应记录加锁的话,事务B和事务A同时更新的话,会导致k变成k+1,导致数据丢失。
2、快照读和当前读区别
我们都知道MySQL中的读分为两种:
1、快照读: 读取的不一定是最新的数据,读取的是MVCC为事务提供的快照。
除了串行化的隔离级别之外,其他级别的快照读是不会加任何锁的。
串行化的加锁和可重复读隔离级别的当前单独加锁是一样的。
select xx from xx;
2、当前读: 每次读取的就是当前数据记录对应的最新值!
select xx from xx lock in share mode
select xx from xx for update
update xx set xx = xx
delete from xx where xx = xx
insert into xx values()
对于不同的隔离级别,会加不同的锁。
3、MySQL中的加锁方式:
MySQL中事务采用两阶段加锁方式:
两阶段指的是:
阶段1、当事务中的语句开始执行时,才会申请对应的锁
阶段2、当事务提交或者回滚后,事务中所有语句申请的锁才会释放。
如上图所示,当执行sql语句2的时候才会申请对应的锁,当事务Acommit后才会sql语句2对应的锁。sql语句1也是。
4、MySQL中锁的类型:
MySQL中的锁按照读写分为两种:
1、X锁
X锁叫做排他锁,也叫做写锁,当写数据的时候需要申请X锁。当申请X锁时,数据上不能有X锁和S锁。也就是当其他事务在数据上写数据或者读数据的时候,新的事务不能申请X锁,写数据,当前事务只能被阻塞,等待其他事务释放锁之后,才能申请。
2、S锁
S锁叫做共享锁,也叫做读锁,当读数据的时候需要申请S锁。当申请S锁时,数据上不能有X锁。可以有S锁,也就是当其他事务正在写数据的时候,当前事务不能读取当前数据。S锁读取数据指的是当前读,快照读是不需要加锁的。
MySQL中锁按照加锁对象分为以下四种:
1、行锁
行锁就是加在数据行上的锁,分为两种:
1、S锁:加在数据行上的读锁 sql语句为:select xx from xx lock in share mode;
2、X锁:加在数据行上的写锁 sql语句为:select xx from xx for update
2、表锁
表锁是直接加在表上的锁,分为四种:
1、S锁:加在表上的读锁。sql语句为 lock tables xx read;
2、X锁:加在表上的写锁。sql语句为 lock tables xx write;
3、IS锁:加在表上的意向读锁,当事务申请行锁中的S锁时,MySQL会 自动为表申请IS锁。
4、IX锁:加在表上的意向写锁。当事务申请行锁中的X锁时,MySQL会自动为表申请IX锁。
IS 锁 | IX锁 | 行S锁 | 行X锁 | 表S锁 | 表X锁 | |
---|---|---|---|---|---|---|
IS锁 | 兼容 | 兼容 | 兼容 | 兼容 | 兼容 | 冲突 |
IX锁 | 兼容 | 兼容 | 兼容 | 兼容 | 冲突 | 冲突 |
行S锁 | 兼容 | 兼容 | 兼容 | 冲突 | 兼容 | 冲突 |
行X锁 | 兼容 | 兼容 | 冲突 | 冲突 | 冲突 | 冲突 |
表S锁 | 兼容 | 冲突 | 兼容 | 冲突 | 兼容 | 冲突 |
表X锁 | 冲突 | 冲突 | 冲突 | 冲突 | 冲突 | 冲突 |
为什么会有意向锁呢?
当事务申请表X锁时,需要表中的每条数据行都不能有行S锁和行X锁。如果没有意向锁的话,MySQL需要从头到尾依次扫描每行来判断是否存在行锁,这种做法效率低下。所以,引入意向锁,当事务申请行S锁时,会自动申请IS锁,当事务申请行X锁时,会自动申请IX锁。
IS锁和IX锁是兼容的
IS锁和IX锁是用来克制表锁的,IS锁和IX锁分别对应的为行S锁和行X锁,因为一个表中有很多数据行,同一个数据行的S锁和X锁才会冲突。所以IS锁和IX锁是兼容的。在行锁申请完意向锁后,还需要进一步申请对应的行锁,如果对应的行锁获取不到,事务仍然要被阻塞。
3、间隙锁
我们都知道,MySQL中的数据是以B+树的形式存在的。一个索引对应一个B+树。非叶子节点存放的是索引数据,叶子节点中存放的是对应的数据行。并且叶子节点是按照某一列的顺序排列的。
比如某一列的数据为[0,5,10,15];
则该列对应区间有(-无穷,0),(0,5),(5,10),(10,15),(15,+无穷)。区间对应的就是间隙。间隙锁就是在间隙上加锁,与插入本区间的插入操作冲突,防止插入,用来解决幻读问题,这个我们到后面再细说。
4、NEXT-KEY 锁
NEXT-KEY锁是MySQL中加锁的基本单位。实际就是间隙锁加行锁的组合。对一条数据行加NEXT-KEY锁,实际就是对该数据行加行锁和其前一个间隙加间隙锁。
比如对10加NEXT-KEY锁,为(5,10],间隙右边变成了闭区间,也就是加了一个行锁。
加锁实验详解
我们都知道MySQL有4个隔离级别:读未提交、读提交、可重复读、串行化。等会我们对这四个隔离级别逐一分析。
都哪些操作会引起加锁呢?
1、当前读:直接读取最新的数据,快照读是读取MVCC提供的快照,是不会加锁的。
2、更新操作
3、删除操作
我们都知道MySQL中的数据是以B+树也就是索引的方式存在的,对于不同的索引类型,加锁的方式也不一样。
MySQL中的锁是加在索引上的
所以我们从四个隔离级别,三种操作,和索引方面来逐一实践加锁原则。
首先看一下我们的表结构和数据
create table t(c1 int primary key, c2 int, c3 int, c4 int, unique index i_c2(c2), index i_c3(c3));
insert into t values (10, 11, 12, 13), (20, 21, 22, 23), (30, 31, 32, 33), (40, 41, 42, 43);
注意事项:
1、别忘了切换事务隔离级别。MySQL默认的隔离级别是可重复读。
2、别忘了手动开启事务,因为MySQL默认的是隐式事务,就是在语句执行前后自动的开启和提交事务。需要用start transaction来手动开启事务,通过commit 和rollback来提交和回滚事务。
3、我们可以使用MySQL自带的sql语句来查询当前存在的锁信息。以我使用的MySQL8.0.22为例,查询语句为select * from performance_schema.data_locks;
,不同的MySQL版本,查询语句可能有出入。
查询得到的锁信息如上图所示,其中有很多的列,我只截取了关键信息列:
1、INDEX_NAME:加锁的索引名字
1、LOCK_TYPE:加锁的类型,TABLE代表表锁,RECORD代表行锁。
2、LOCK_MODE:加锁的模式:
X代表NEXT-KEY的X锁
X,REC_NOT_GAP代表行X锁
X,GAP代表间隙锁
S,代表NEXT-KEY的S锁
S,REC_NOT_GAP代表行S锁
S,GAP代表间隙锁
3、LOCK_DATA:锁对应的数据,主键索引对应的为主键ID,非主键索引对应的是索引列数据和对应的主键ID。
1、读未提交
1.1、主键索引
1.1.1、主键索引上等值查询
a、查询的记录存在
start transaction;
select * from t where c1 = 10 for update;
select * from performance_schema.data_locks;
commit;
如上图所示,加了两个锁,一个是IX锁,一个加在主键上的c1=10的数据记录的行X锁