锁是用于管理对共享资源的并发访问,是数据库系统区别于文件系统的一个关键特性
MySQL中的锁
本文主要来谈InnoDB引擎,InnoDB引擎支持行锁、表锁粒度的锁
意向锁
为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock)
意向锁是表级锁
什么是意向锁呢?如果没有意向锁,当已经有人使用行锁对表中的某一行进行修改时,如果另外一个请求要对全表进行修改,那么就需要对所有的行是否被锁定进行扫描,在这种情况下,效率是非常低的;不过,在引入意向锁之后,当有人使用行锁对表中的某一行进行修改之前,会先为表添加意向排他锁(IX),再为行记录添加排他锁(X),在这时如果有人尝试对全表进行修改就不需要判断表中的每一行数据是否被加锁了
申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突
锁的兼容性
共享锁(读锁、S锁),允许事务读一行数据。
排他锁(写锁、X锁),允许事务删除或更新一行数据。
读锁会阻塞其他事务写,写锁会阻塞其他事务的读和写
行锁的算法
- Record Lock:行锁,单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止幻读、防止间隙内有新数据插入、防止已存在的数据更新为间隙内的数据
- Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。InnoDB行锁默认加锁方式是next-key 锁
如何加锁
首先需要了解MVCC + 2PL + 事务隔离级别
MVCC
ref: https://mp.weixin.qq.com/s/Jeg8656gGtkPteYWrG5_Nw
首先我们需要了解MVCC(Multi-Version Concurrency Control),与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)
特点:读不加锁,读写不冲突
InnoDB之所以并发高,因为快照读不加锁,所有普通select都是快照读
MVCC只在 READ COMMITTED 和 REPEATABLE READ 2个隔离级别下工作
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外)
select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。
在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
两段锁协议
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
- 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁,在进行写操作之前要申请并获得X锁。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
- 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
- 这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度等同于串行的执行
3事务隔离
隔离性:并发事务之间互相影响的程度,比如一个事务会不会读取到另一个未提交的事务修改的数据。不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷。
锁可以实现事务并发,但也导致了一些问题
- 更新丢失:A事务正常运行,B事务回滚
- 脏读 :A事务读取到了B事务没有提交的数据,B事务有回滚,或者继续更新,反正数据变了,A读到的无效
- 不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读
Read Uncommited
可以读取未提交记录。此隔离级别,不会使用,忽略。
Read Committed (RC)
针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在不可重复读
Repeatable Read (RR)
针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),存在幻读
这也是InnoDB的默认隔离级别
Serializable
从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁),不存在幻读。
1.读未提交RU
2.读已提交RC
InnobDB加锁是对索引加行锁。如果没有索引的话,会给整张表的所有数据行的加行锁,再进行过滤,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)
实现方式:
- 普通读(快照读):基于MVCC并发控制,在每次读之前生成一个ReadView
- 当前读:基于锁的并发控制,行锁或表锁
- RC下:加S, X锁
3.可重复读RR
- InnoDB默认隔离级别为RR
- InnoDB RR解决了幻读
实现方式:
-
普通读(快照读):基于MVCC并发控制,在该事务第一次读之前生成一个ReadView
-
当前读:基于锁的并发控制,行锁或表锁
-
select for update;锁住了,另一个事务阻塞,解决幻读
-
RR下:加S,X + Gap锁 = next-key锁,此时避免了幻读
-
begin; select * from student where s_name = 'john' for update; 行锁 /*where id>=1 for update;*/ next-key锁 /*上面事务不提交,下面的会一直阻塞*/ begin ; insert into student values(2,'john','c2'); commit; /* 如果是RC级别下,就会造成幻读 */
-
避免幻读 Next-key = 行锁+Gap锁
- 如果where条件部分命中或者全部命中,则会加Gap锁
- Gap锁会用在非唯一索引或者不走索引的当前读中
- 受限于这种实现方式,Innodb很多时候会锁住不需要锁的区间。