数据库锁

基本概念

加锁的目的

数据库的锁是为了解决事务的隔离性问题,为了让事务之间相互不影响,每个事务进行操作的时候都会对数据加上一把特有的锁,防止其他事务同时操作数据。

锁是基于什么实现的

在Innodb中,锁是作用在索引上面的,当我们的SQL命中索引时,那么锁住的就是命中条件内的索引节点(行锁),如果没有命中索引的话,那我们锁的就是整个索引树(表锁)

锁的分类

  • 基于锁的属性分类:共享锁、排他锁。
  • 基于锁的粒度分类:行级锁(innodb)、表级锁(innodb、myisam)、页级锁(innodb引擎)、记录锁、间隙锁、临键锁、自增锁。
  • 基于锁的状态分类:意向共享锁、意向排它锁。
  • 基于加锁的态度分类:悲观锁、乐观锁。

InnoDB的锁

隐式锁定和显式锁定

  • 隐式锁定
    InnoDB在事务执行过程中,使用两阶段锁协议(不主动进行显示锁定的情况)
    • 随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
    • 锁只有在事务执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。
  • 显式锁定
    InnoDB也支持通过特定的语句进行显示锁定(存储引擎层)
    1
    2
    select ... lock in share mode //共享锁
    select ... for update //排他锁

锁类型

1. 基本锁

在一个高并发系统中,会出现多会话同时访问同一资源的情况,此时即产生了竞争。
为了保证数据的一致性,必须要用锁机制来控制资源的并发访问。

InnoDB采用的行锁的设计(MyISAM只支持表锁),行锁带来更高的并发性,但管理复杂度也要比表锁更高。
InnoDB共实现了2种标准的行级锁:

  • 共享锁(S Lock),允许持有锁的事务读取数据。
  • 排它锁(X Lock),允许持有锁的事务修改和删除数据。

2. 意向锁 Intention locks

InnoDB存储引擎支持多粒度的锁定,即允许行级锁和表级锁同时存在。

  • 意向共享锁(IS lock),表示事务想要获得表中某几行的共享锁。
  • 意向排它锁(IX lock),表示事务想要获得表中某几行的排它锁。

意向锁是一种表级锁,表示事务稍后希望在更细的粒度上(行级别)加锁。

  • select … lock in share mode; 会对表施加IS锁
  • select … for update; 会对表施加IX锁
X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

例如,如果请求在记录r上加X锁,则需要先在表级别上加意向排他锁(IX),如果此时表上存在其他锁,则意向锁需要等待表级别锁的释放,待表级意向锁(IX)获得成功后,才可以对行级别加X锁。

意向锁只会阻塞表级别的请求(如全表扫描、lock tables … write),除此之外不会阻塞任何操作。

3. 行锁/记录锁 Record locks

InnoDB行锁是通过给索引上的索引项加锁来实现的,而不是给表的行记录加锁实现的,这就意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则InnoDB将使用表锁。

由于InnoDB的行锁实现是针对索引字段添加的锁,不是针对行记录加的锁,因此虽然访问的是InnoDB引擎下表的不同行,但如果使用相同的索引字段作为过滤条件,依然会发生锁冲突,只能串行进行,不能并发进行。

即使SQL中使用了索引,但是经过MySQL的优化器后,如果认为全表扫描比使用索引效率高,此时会放弃使用索引,因此也不会使用行锁,而是使用表锁。

例如:select col from t where col=1 for update; 即对col为1的记录添加记录锁,阻止其他事务对此记录的操作。

4. 间隙锁 Gap locks

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

例如:select col from t where col between 1 and 10 for update;会锁住1~10之间的间隙,而不管这段间隙内是否存在记录,因此间隙锁可能只锁住了一段空气。此时如果想插入为col为5的记录会被阻塞,即使5的记录不存在。

间隙锁的唯一目的即是阻止其他的事务往间隙中插入记录,因此不同的事务可以对同样的间隙重复加锁,没有共享和排他类型之分。

InnoDB自动使用间隙锁的条件
  1. 可重复读 Repeatable Read级别下才会有间隙锁。必须在RR级别下。
  2. 检索条件必须有索引。(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
1
2
# 打开间隙锁设置
innodb_locks_unsafe_for_binlog: OFF #默认值为OFF,即启用间隙锁

5. 临键锁 Next-Key locks

行锁与间隙锁组合起来用就叫做Next-Key Lock。
InnoDB默认加锁方式是next-key锁。

next-key lock会对记录本身和记录之前的区间加锁。

A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.

如果一个会话占有了索引记录R的共享/排他锁,其他会话不能立刻在R之前的区间插入新的索引记录。

If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.

例如:假设表中存在索引记录1,10。则next-key lock可能锁住的范围是:(-∞,1]、(1,10]、(10,+∞),对于最后一个区间,next-key lock也会锁住最大记录之后的间隙。

Next-key lock只在MySQL InnoDB*repeatable read隔离级别下使用,主要是用来解决幻读(phantom read)的问题。

当对唯一键值进行锁定时,查询的索引含有唯一属性,next-key lock将会降级为record lock,即仅锁住唯一记录。
而如果唯一键由多个列组成,而查询仅使用其中一列,则其实是range查询,InnoDB会依然使用next-key lock进行锁定。

6. 插入意向锁 Insert intention locks

插入意向锁是间隙锁的一种,其由insert语句在插入记录前获取,代表将在间隙中插入记录的意向。

多个事务可以对同一个间隙重复加insert intention lock,只要插入的记录值不同,事务就不会冲突。

例如表中已存在记录1和10,两个事务分别想插入5和6。两个事务都会对1和10记录之间的间隙(2,9)加insert intention lock,但由于插入的记录值不同,因此后续对要插入的记录获取X锁的时候并不会冲突。

7. 自增锁 auto-inc locks

自增锁是一种特殊类型的表锁,只要在事务对auto_increment类型的列插入数据时,才会施加auto-inc lock,此时其他想插入的事务都需要等待该锁的释放,而持有该锁的事务可以获得连续的primary key值。