转载

锁-深挖

软件系统中经常会使用到锁,锁是用来协调计算机中多个线程或进程对于同一资源的并发操作,目的是保障数据的一致、有效。

锁有很多种,从锁的不同侧重点上看有以下几类:数据库锁、业务代码锁、分布式锁

数据库锁

加锁的目的?

在我们了解数据库锁之前,首先我们必须要明白加锁的目的是为了解决什么问题,如果你还不清楚的话,那么从现在起你应该知道,数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。如果你想一个人静一静,不被别人打扰,那么请在你的房门上加上一把锁。

什么是事务?

事务:一个最小的工作单元,一个事务对应一组批量的操作,事务保证批量操作中的 SQL 语句(insert,update,delete 语句)要么全部执行,要么全部不执行。在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。

事务的特性:事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。(比如:A向B转账,不可能A扣了钱,B却没有收到)
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。(比如:A正在从一张银行卡里面取钱,在A取钱的过程中,B不能向这张银行卡打钱)
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务的隔离级别:
  • 读未提交:read uncommitted

    事物A和事物B,事物A未提交的数据,事物B可以读取到
    这里读取到的数据叫做“脏数据”
    这种隔离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别
    
  • 读已提交:read committed

    事物A和事物B,事物A提交的数据,事物B才能读取到
    这种隔离级别高于读未提交
    换句话说,对方事物提交之后的数据,我当前事物才能读取到
    这种级别可以避免“脏数据”
    这种隔离级别会导致“不可重复读取
    Oracle默认隔离级别
    
  • 可重复读:repeatable read

    事务A和事务B,事务A提交之后的数据,事务B读取不到
    事务B是可重复读取数据
    这种隔离级别高于读已提交
    换句话说,对方提交之后的数据,我还是读取不到
    这种隔离级别可以避免“不可重复读取”,达到可重复读取
    比如1点和2点读到数据是同一个
    MySQL默认级别
    虽然可以达到可重复读取,但是会导致“幻像读”
    
  • 串行化:serializable

    事务A和事务B,事务A在操作数据库时,事务B只能排队等待
    这种隔离级别很少使用,吞吐量太低,用户体验差
    这种级别可以避免“幻像读”,每一次读取的都是数据库中真实存在数据,事务A与事务B串行,而不并发
    

隔离级别与一致性关系
file

事务隔离级别设置
全局默认隔离级别设置:

  • 可以在my.ini文件中使用transaction-isolation=(READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE)
    动态设置隔离级别:
  • SET TRANSACTION ISOLATION LEVEL READ COMMITTED;(设置会话级隔离级别为READ COMMITTED)
  • SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(设置会话级隔离级别为READ COMMITTED)
  • SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;(设置全局级隔离级别为READ COMMITTED)
  • select @@tx_isolation; (查看当前会话隔离级别)
  • select @@global.tx_isolation; (查看系统当前隔离级别)

事务开启操作

  • BEGIN 开始一个事务
  • ROLLBACK 事务回滚
  • COMMIT 事务确认
  • SAVEPOINT savepoint_name; // 声明一个 savepoint子事务,防止回滚回滚完全放弃事务,保留点再事务处理完成(执行一条 ROLLBACK 或 COMMIT)后自动释放。
  • ROLLBACK TO savepoint_name; // 回滚到savepoint,执行多条操作时,回滚到想要的那条语句之前。
  • RELEASE SAVEPOINT savepoint_name; // 删除指定保留点

设置事务提交模式

  • SET AUTOCOMMIT=0 禁止自动提交
  • SET AUTOCOMMIT=1 开启自动提交(默认开启)
  • set autocommit=off 或者 start transaction
脏读、幻读、不可重复读

1.脏读:(读取未提交数据)

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

时间顺序 转账事务 取款事务
1 开始事务
2 开始事务
3 查询账户余额为2000元
4 取款1000元,余额被更改为1000元(未提交)
5 查询账户余额为1000元(产生脏数据)
6 取款操作发生未知错误,事务回滚,余额变更为2000元
7 转入2000元,余额被更改为3000元(脏读1000+2000)
8 提交事务
备注 按照正常逻辑此时账户应该为4000元

2.不可重复读:(前后多次读取,数据内容不一致)

是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

时间顺序 事务A 事务B
1 开始事务
2 第一次查询,小明的年龄为20岁
3 开始事务
4 其他操作
5 更改小明的年龄为30岁
6 提交事务
7 第二次查询,小明的年龄为30岁
备注 按照正确逻辑,事务A前后两次读取到的数据应该一致

3.幻读:(前后多次读取,数据总量不一致)

事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测获取到的数据如同鬼影一般。

  • 例子1:
时间顺序 事务A 事务B
1 开始事务
2 第一次查询,数据总量为100条
3 开始事务
4 其他操作
5 新增100条数据
6 提交事务
7 第二次查询,数据总量为200条
备注 按照正确逻辑,按照正确逻辑,事务A前后两次读取到的数据总量应该一致
  • 例子2:在事务1中,查询User表id为1的是用户否存在,如果不存在则插入一条id为1的数据。
select * from User where id = 1;

在事务1查询结束后,事务2往User表中插入了一条id为1的数据。

insert into `User`(`id`, `name`) values (1, 'Joonwhee');

此时,由于事务1查询到id为1的用户不存在,因此插入1条id为1的数据。

insert into ` User`(`id`, `name`) values (1, 'Chillax');

但是由于事务2已经插入了1条id为1的数据,因此此时会报主键冲突,对于事务1 的业务来说是执行失败的,这里事务1 就是发生了幻读,因为事务1读取的数据状态并不能支持他的下一步的业务,见鬼了一样。这里要灵活的理解读取的意思,第一次select是读取,第二次的insert其实也属于隐式的读取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。

  • 例子3:目前工资为1000的员工有10人。
    事务1,读取所有工资为1000的员工,共读取10条记录 。
con1 = getConnection();  
Select * from employee where salary =1000;

这时另一个事务向employee表插入了一条员工记录,工资也为1000

con2 = getConnection();  
Insert into employee(employeeName,salary) values("Lili",1000);  
con2.commit();

事务1再次读取所有工资为1000的员工,共读取到了11条记录,这就产生了幻读。

//con1  
select * from employee where salary =1000;

不可重复读和幻读到底有什么区别呢?
(1)不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。

幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改

事务实现的原理

下面我首先讲实现事务功能的三个技术,分别是日志文件(redo log 和 undo log),锁技术以及MVCC,然后再讲事务的实现原理,包括原子性是怎么实现的,隔离型是怎么实现的等等。最后在做一个总结,希望大家能够耐心看完

  • redo log与undo log介绍
  • mysql锁技术以及MVCC基础
  • 事务的实现原理
redo log 与 undo log介绍

什么是redo log ?

redo log叫做重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中。假设有个表叫做tb1(id,username) 现在要插入数据(3,ceshi)

img

start transaction;
select balance from bank where name="zhangsan";
// 生成 重做日志 balance=600
update bank set balance = balance - 400; 
// 生成 重做日志 amount=400
update finance set amount = amount + 400;

img

redo log作用是什么?

mysql 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到Boffer Pool(缓冲池)里头,把这个当作缓存来用。然后使用后台线程去做缓冲池和磁盘之间的同步。

那么问题来了,如果还没来的同步的时候宕机或断电了怎么办?还没来得及执行上面图中红色的操作。这样会导致丢部分已提交事务的修改信息!

所以引入了redo log来记录已成功提交事务的修改信息,并且会把redo log持久化到磁盘,系统重启之后在读取redo log恢复最新数据。

  • 总结:redo log是用来恢复数据的,用于保障,已提交事务的持久化特性(记录了已经提交的操作)

什么是undo log?

undo log 叫做回滚日志,用于记录数据被修改前的信息。他正好跟前面所说的重做日志所记录的相反,重做日志记录数据被修改后的信息。undo log主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
还用上面那两张表

img

每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。

undo log 有什么作用?

undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以根据undo log的信息来进行回滚到没被修改前的状态。

  • 总结:undo log是用来回滚数据的用于保障,未提交事务的原子性

mysql锁技术以及MVCC基础

mysql锁技术

当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时必须有一种措施来进行并发控制。不然很有可能会造成不一致。
读写锁
解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可,这两种锁被称为:

  • 共享锁(shared lock),又叫做"读锁"
    读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。

  • 排他锁(exclusive lock),又叫做"写锁"
    写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。

    img

  • 总结:通过读写锁,可以做到读读可以并行,但是不能做到写读,写写并行

MVCC基础

MVCC介绍
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。一般情况下,事务性储存引擎不是只使用表锁,行加锁的处理数据,而是结合了MVCC机制,以处理更多的并发问题。Mvcc处理高并发能力最强,但系统开销 比最大(较表锁、行级锁),这是最求高并发付出的代价。

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号。

以上片段摘自《高性能Mysql》这本书对MVCC的定义。他的主要实现思想是通过数据多版本来做到读写分离。从而实现不加锁读进而做到读写并行。MVCC在mysql中的实现依赖的是undo log与read view;

  • undo log :undo log 中记录某行数据的多个版本的数据。

  • read view :用来判断当前版本数据的可见性

    img

    MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。不同存储引擎的MVCC实现是不同的,典型的有乐观(optimistic)并发控制和悲观(pessimistic)并发控制。

MVCC具体实现分析

轻松理解MYSQL MVCC 实现机制
下面,我们通过InnoDB的MVCC实现来分析MVCC使怎样进行并发控制的.
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.

  • 例子:
  • CREATE(创建表)
create table yang( 
id int primary key auto_increment, 
name varchar(20));
  • INSERT(插入数据)
start transaction;
insert into yang values(NULL,'yang') ;
insert into yang values(NULL,'long');
insert into yang values(NULL,'fei');
commit;

假设系统的版本号从1开始.
对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined
  • SELECT(查询数据)
    InnoDB会根据以下两个条件检查每行记录:
    (1)InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
    (2)行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
    只有a,b同时满足的记录,才能返回作为查询结果.

  • DELETE(删除数据)
    InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
    看下面的具体例子分析:
    第二个事务,ID为2;

start transaction;
select * from yang;  //(1)
select * from yang;  //(2)
commit;

假设1

假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据; 第三个事务ID为3;

start transaction;
insert into yang values(NULL,'tian');
commit;

这时表中的数据如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 undefined
2 long 1 undefined
3 fei 1 undefined

假设2

假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4; 第四个事务:

start   transaction;  
delete from yang where id=1;
commit;

此时数据库中的表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined
4 tian 3 undefined

接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 undefined
3 fei 1 undefined
  • UPDATE
    InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.

假设3

假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作;第5个事务:

start  transaction;
update yang set name='Long' where id=2;
commit;

根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined
4 tian 3 undefined
2 Long 5 undefined

继续执行事务2的(2),根据select 语句的检索条件,得到下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 yang 1 4
2 long 1 5
3 fei 1 undefined

还是和事务2中(1)select 得到相同的结果.

锁是基于什么实现的?

为了后面大家后面对锁理解的更透彻,所以务必要对此进行说明,锁是基于什么实现的,你现实生活中家里的锁是基于门来实现的,那么数据库的锁又是基于什么实现的呢? 那么我在这里可以告诉你,数据库里面的锁是基于索引实现的,在Innodb中我们的锁都是作用在索引上面的,当我们的SQL命中索引时,那么锁住的就是命中条件内的索引节点(行锁),如果没有命中索引的话,那我们锁的就是整个索引树(表锁),如下图一下锁住的是整棵树还是某几个节点,完全取决于你的条件是否有命中到对应的索引节点。 (Mysql Server也会提供表级别锁)

innodb索引结构图(B+ tree):
file

存储引擎

介绍下mysql的常用存储引擎,

  • Innodb:支持到行级锁、支持事务、Mysql默认存储引擎 (主要介绍)
  • Myisam:支持到表级锁,不支持事务、适用查询多、增改少的数据(8.0弃用)

file

锁的分类

数据库里有的锁有很多种,为了方面理解,所以我根据其相关性"人为"的对锁进行了一个分类,分别如下

  • 基于锁的属性分类:共享锁、排他锁。
  • 基于锁的粒度分类:表锁、行锁、记录锁、间隙锁、临键锁。
  • 基于锁的状态分类:意向共享锁、意向排它锁。
  • 基于锁的预期分类:乐观锁、悲观锁。

属性锁

共享锁(Share Lock)

共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。

事务执行read时,可以持有这个锁。当事务1持有某一行数据的S锁,其他事务就只能申请这一行数据的S锁,不能申请X锁。

file

共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。

排他锁(eXclusive Lock)

排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。

事务执行update或delete时,可以持有这个锁。当事务1持有某一行数据的X锁,其他事务不能申请这一行的S和X锁。

file

排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取。避免了出现脏数据和脏读的问题。

粒度锁

表锁

表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;

特点: 粒度大,加锁简单,容易冲突;

file

自增长键锁(AUTO-INC Locks):自增长键锁是一个特殊的表锁,当事务插入自增长列时需要获取该锁。innodb_autoinc_lock_mode配置选项配置用于自动增量锁定的算法,它允许您选择如何在可预测的自动增量值序列和插入操作的最大并发之间进行权衡。

行锁

行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问;

特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高;

file

记录锁(Record Lock)

记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录。

记录锁用于锁定索引记录,比如SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,该语句防止其他事务插入、更新、删除t.c1=10的记录。记录锁总是会锁住索引记录,即使当表没有定义索引时,InnoDB也会创建一个隐藏的聚簇索引用于记录锁(见https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html)。

file

触发条件:精准条件命中,并且命中的条件字段是唯一索引;

例如:update user_info set name=’张三’ where id=1 ,这里的id是唯一索引。

记录锁的作用:加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题。

每一个InnoDB都一个特殊的聚簇索引,该索引存储行记录的内容,通常它就是主键的代名词。

  • 当你创建主键时,InnoDB就使用它作为聚簇索引
  • 当没有主键时,InnoDB寻找第一个非空的唯一索引列作为聚簇索引
  • 当没有主键或非空唯一索引时,InnoDB内部会生成一个叫做GEN_CLUST_INDEX的索引,该索引作用在一个包含行ID的合成列上,表上的行按这个ID列排序,该ID是一个6字节随着数据插入单调递增的字段。
间隙锁(Gap Lock)

间隙锁属于行锁中的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。

间隙锁用于锁定索引记录之间的数据,或者锁住第一个索引之前的记录或最后一个索引之后的记录。比如SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE,该语句会防止其他事务插入c1=15的记录,即时当前数据库并没有c1=15的记录,因为间隙锁会锁住10到20之间的记录。

间隙可能跨越单个索引值,多个索引值,甚至可能为空。

使用唯一索引搜索唯一行的语句不需要间隙锁定,比如SELECT * FROM child WHERE id = 100,如果id字段具有唯一索引,该语句只会使用记录锁,而不是间隙锁。

同一个间隙上的gap X锁和gap S锁可以同时被不同事务持有,因为当从索引中清除记录,则必须合并由不同事务保留在记录上的间隙锁。

间隙锁的唯一目的是防止其他事务插入间隙,间隙锁可以共存,一个事务占用的间隙锁定不会阻止另一个事务在同一个间隙上进行间隙锁定。共享和独占间隙锁之间没有区别。它们彼此不冲突,它们执行相同的功能。

可以明确禁用间隙锁定。如果将事务隔离级别更改为READ COMMITTED,则会发生这种情况,在这些情况下,对于搜索和索引扫描禁用间隙锁定,并且仅用于外键约束检查和重复键检查。

比如下面的表里面的数据ID 为 1,4,5,7,10 ,那么会形成以下几个间隙区间,-n-1区间,1-4区间,7-10区间,10-n区间 (-n代表负无穷大,n代表正无穷大)

file

触发条件:范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中。

例如:对应上图的表执行select * from user_info where id>1 and id < 4(这里的id是唯一索引) ,这个SQL查询不到对应的记录,那么此时会使用间隙锁。

间隙锁作用:防止幻读问题,事务并发的时候,如果没有间隙锁,就会发生如下图的问题,在同一个事务里,A事务的两次查询出的结果会不一样。

file

临键锁(Next-Key Lock)

临键锁也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。

Next-Key锁是某个索引记录的记录锁和该索引记录之前间隙的间隙锁的组合,假设一个索引包含值10, 11, 13, 和 20,那在该索引上可能的Next-Key锁包含以下几个
  (negative infinity, 10]
   (10, 11]
   (11, 13]
   (13, 20]
   (20, positive infinity)

默认情况下,InnoDB在REPEATABLE READ事务隔离级别运行。在这种情况下,InnoDB使用Next-Key锁进行搜索和索引扫描,从而防止幻读。

例如:下面表的数据执行 select * from user_info where id>1 and id<=13 for update ;

会锁住ID为 1,5,10的记录;同时会锁住,1至5,5至10,10至15的区间。

file

触发条件:范围查询并命中,查询命中了索引。

临键锁的作用:结合记录锁和间隙锁的特性,临键锁避免了在范围查询时出现脏读、重复读、幻读问题。加了临键锁之后,在范围区间内数据不允许被修改和插入。

状态锁

状态锁包括意向共享锁和意向排它锁,把他们区分为状态锁的一个核心逻辑,是因为这两个锁都是都是描述是否可以对某一个表进行加表锁的状态。

意向锁的解释:当一个事务试图对整个表进行加锁(共享锁或排它锁)之前,首先需要获得对应类型的意向锁(意向共享锁或意向共享锁)

意向共享锁:当一个事务试图对整个表进行加共享锁之前,首先需要获得这个表的意向共享锁。

意向排他锁:当一个事务试图对整个表进行加排它锁之前,首先需要获得这个表的意向排它锁。

插入意向锁(Insert Intention Locks)

插入意向锁定是在行插入之前由INSERT操作设置的一种间隙锁定,该锁定表示:如果插入到相同索引间隙中的多个事务,他们不插入间隙内的相同位置,则不需要等待彼此。

比如有两个索引记录4和7,两个事务分别插入5和6两条记录,在他们获取5和6的排他锁之前(插入数据之后获取的),他们都会获取4到7之间的插入意向锁,但他们不会互相阻塞,因为他们插入了不同的位置。

InnoDB支持多个粒度锁定,允许行锁和表锁共存。比如,LOCK TABLES ... WRITE语句会获取某一个表的X锁。在对记录加S锁或者X锁时,必须保证其在相同的表上有对应的意向锁或者锁强度更高的表级锁。InnoDB使用意向锁在多个粒度级别实现锁定,意向锁是表级别锁,它表明事务在之后会申请表中某一行的X或S锁,有两种类型的意向锁:

  • IS(intention shared lock):表明事务之后要申请表中特定行的S锁
  • IX(intention exclusive lock):表明事务之后要申请表中特定行的X锁

SELECT ... FOR SHARE使用了IS锁,SELECT ... FOR UPDATE使用IX锁

为什么我们需要意向锁?
意向锁光从概念上可能有点难理解,所以我们有必要从一个案例来分析其作用,这里首先我们先要有一个概念那就是innodb加锁的方式是基于索引,并且加锁粒度是行锁,然后我们来看下面的案例。 (意向锁的作用:https://blog.csdn.net/dreamvyps/article/details/84500543)

意向锁协议规则
在事务申请S锁之前,它必须申请表的IS锁或更强的锁
在事务申请X锁之前,它必须申请表的IX锁

锁兼容性
当存在一个锁时,一个事务能够再申请其他锁,则这两个锁是兼容的,否则是冲突的,申请锁的事务会等待锁释放。
X锁和其他锁都不兼容,IS和IX锁互相兼容,S和S、IS兼容,S和X、IX不兼容

第一步

事务A对user_info表执行一个SQL:update user_info set name =”张三” where id=6 加锁情况如下图;

file

第二步

与此同时数据库又接收到事务B修改数据的请求:SQL: update user_info set name =”李四”;

1、因为事务B是对整个表进行修改操作,那么此SQL是需要对整个表进行加排它锁的(update加锁类型为排他锁);

2、我们首先做的第一件事是先检查这个表有没有被别的事务锁住,只要有事务对表里的任何一行数据加了共享锁或排他锁我们就无法对整个表加锁(排他锁不能与任何属性的锁兼容)。

3、因为INNODB锁的机制是基于行锁,那么这个时候我们会对整个索引每个节点一个个检查,我们需要检查每个节点是否被别的事务加了共享锁或排它锁。

4、最后检查到索引ID为6的节点被事务A锁住了,最后导致事务B只能等待事务A锁的释放才能进行加锁操作。

思考

在A事务的操作过程中,后面的每个需要对user_info加持表锁的事务都需要遍历整个索引树才能知道自己是否能够进行加锁,这种方式是不是太浪费时间和损耗数据库性能了?

所以就有了意向锁的概念:如果当事务A加锁成功之后就设置一个状态告诉后面的人,已经有人对表里的行加了一个排他锁了,你们不能对整个表加共享锁或排它锁了,那么后面需要对整个表加锁的人只需要获取这个状态就知道自己是不是可以对表加锁,避免了对整个索引树的每个节点扫描是否加锁,而这个状态就是我们的意向锁。

加锁补充

for update锁

for update仅适用于InnoDB的锁,必须在开启的事务中才能生效。

在开启事务后,使用for update锁时InnoDB默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别,表级锁时,不管是否查询到记录,都会锁表。

当开启一个事务进行for update的时候,另一个事务也有for update的时候会一直等着,直到第一个事务commit或者rollback或者断开连接,第二个事务才会拿到锁进行后面操作。

锁的应用例子:

例子: 假设表user ,存在有id跟name、status字段,id是主键,status加有索引。

  • 例1: 明确指定主键,并且有此记录,行级锁
    ```
    SELECT * FROM user WHERE id=1 FOR UPDATE;

SELECT * FROM user WHERE id=1 and name=’张三’ FOR UPDATE;


* 例2: 明确指定主键/索引,若查无此记录,无锁

SELECT * FROM user WHERE id=-1 FOR UPDATE;


* 例3::无主键/索引,表级锁

SELECT * FROM user WHERE name=’张三’ FOR UPDATE;


* 例4:主键/索引不明确,表级锁

SELECT * FROM user WHERE id<>’3’ FOR UPDATE;

SELECT * FROM user WHERE id LIKE ‘3’ FOR UPDATE;
```

代码锁

分布式锁

参考文章:

正文到此结束