加锁是为了可以并发访问数据库中的共享资源,在对数据的增删改查时可以确保一致性。

1. InnoDB存储引擎中的锁

1.1 锁的类型

共享锁(S) 允许不同事务读同一行数据。因为没有对数据进行修改,所以锁是兼容的。
排它锁(X) 允许事务删除或更新同一行数据。此时会对数据进行修改,因此必须互斥访问,锁是不兼容的。

注意X锁和S锁都是行锁,InnoDB存储引擎默认支持行锁是他适合多并发的优势所在,但是只有操作具有索引的字段时,InnoDB才会使用行锁,否则使用表锁,即InnoDB也支持表锁和事务。

MyISAM默认支持表锁,不支持行锁和事务。

S锁和X锁的兼容性:




然后再来介绍意向锁,意向锁为表级别的锁,其目的是为了协调表锁和行锁的关系。

意向共享锁(IS) 事务想要获得一张表某几行的共享锁。
意向排它锁(IX) 事务想要获得一张表某几行的排他锁。

IS锁和IX锁和X锁和S锁的兼容性:

有了意向锁可以支持更细粒度的加锁,如果将上锁的对象看成一棵树,如果对最下层的对象(细粒度对象)上锁,那么需要先对上层对象(粗粒度对象)上意向锁。

如下图,如果要对页上的某行记录加X锁,那么需要对其上层的数据库A、表、页加意向锁IX(加意向锁表示这个表里面是有行级锁的,其他事务想要给这个表加表锁,需要先判断这个表是否有表级意向锁),最后才是对行记录加X锁,若上层某一个部分因为锁不兼容而导致等待,那么对记录加X锁的操作就要等待上层锁的完成才可以执行。

那么意向锁用在什么地方呢?,InnoDB存储引擎不是主要加行级别的锁吗,为什么还要表级别的意向锁?

解释:

一个事务A给表加了普通表锁之后,表示可以修改表中的任何一行,其他事务就不能对这张表的行加锁。同理可得,一个事务要对一个表加锁的话,是不是得确认里面没有行级锁了才可以加表锁,如果没有意向锁,就得一行一行的判断每一行是否加了行锁,这样效率会非常低,有了意向锁之后,直接通过判断该表是否加了表级别的意向锁即可,这样就提高了判断的效率,如果该表加了意向锁,那么其他事务就不能对该表加表锁,如果该表没有加意向锁,说明该表里面一行行锁都没有加,那么这个事务就可以对该表加表锁。

1.2 一致性非锁定读

InnoDB引擎的默认隔离级别为可重复读级别,表示读操作不会等待访问的行上的X锁的释放,而是直接读取事务开始时的行数据版本。

具体实现就是一致性非锁定读。是指InnoDB存储引擎通过多版本并发控制的方式读取当前行数据,如果读取的行正在执行DELETE或UPDATE操作,当前读的操作不会去等待X锁的释放,而是读取当前行的一个快照文件(不是最新数据,而是事务开始时的行数据版本,如果是read committed隔离级别,则会读取最新数据快照)

一致性非锁定读提高了数据库的并发性

1.3 一致性锁定读

一致性锁定读用于用户需要显式的对数据库读取操作加锁以保证数据逻辑的一致性,有两种方式如下

SELECT … FOR UPDATE 对读取的行加一个X锁,其他事务不能加锁
SELECT … LOCK IN SHARE MOD 对读取的行加一个S锁,其他事务可以加S锁

使用上述语句时,必须在一个事务中,因此使用时要加上BEGIN、START TRANSACTION 或 SET AUTOCOMMIT=0

2. 三种行锁算法

记录锁(Record Lock) 单个行记录上的锁
间隙锁(Gap Lock) 锁定一个范围,但不包含该记录本身
临键锁(Next-Key Lock) 记录锁+间隙锁,锁定一个范围和记录本身

Record Lock 主要锁住索引记录,如果没有设置索引,则会使用隐式的主键来锁定。

Next-Key Lock 则会锁定范围和记录本身,例如一个索引包含10和13这两个值,那么可能被锁定的区间有:(-∞,10],(10,13], (13,+∞)。

注意当查询的字段具有唯一键值属性时,会将 Next-Key Lock 锁降级为 Record Lock锁,如下例:

先建表:

create table t (
    a int primary key
)
insert into t set a = 1;
insert into t set a = 2;
insert into t set a = 5;

在下列操作中,会话A会先对5进行X锁定,而a字段是主键且唯一,所以锁定的是5这个值,而不是(2,5)这个范围。此时若插入a=4,也不会被阻塞,锁由 Next-Key Lock 锁降级为 Record Lock锁后提高了并发性。

但是当查询的字段是辅助索引的时候,则锁不会降级,但是主键索引的锁依然会降级,如下例:

create table t (
    a int,
    b int,
    primary key(a), #a为主键索引
    key(b) #b为辅助索引
)
insert into t set a = 1, b =1;
insert into t set a = 5, b = 3;
insert into t set a = 10, b = 8;

若执行select * from t where b = 3 for update;,此时对于主键索引a来说,锁会由 Next-Key Lock 降级为 Record Lock,锁定范围仅为5;而对于辅助索引b,锁不降级,依然为Next-Key Lock锁,锁定范围为(1,3]和(3,8)。对于这些锁定的范围无法再插入或修改数据,除非锁被释放。

优点:Next-Key Lock锁可以解决 repeatable read 隔离级别下幻读的问题,避免了不可重复读现象。幻读指的是在同一事务下,连续执行两次同样的sql语句可能导致不同的结果。经过Next-Key Lock锁后,会把索引覆盖的区间锁定,防止出现幻读

缺点:间隙锁有一个问题,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害。

3. 死锁

如果程序是串行的,则不会发生死锁,死锁只存在于并发中,即A和B资源互相等待,却都没有结果,一直等下去就造成了死锁,如下例:

出现了A等B,B等A的现象

出现死锁后,会被InnoDB存储引擎自动检测到且回滚事务

推荐阅读:

4. 参考

MySQL技术内幕(InnoDB存储引擎第2版)