一、锁的分类

二、从数据操作的类型划分:读锁、写锁

在并发事务中读-读并不会引起什么问题。对于写-写读-写写-读可能会引起一些问题,需要使用mvcc或加锁解决。由于既要允许读-读情况不受影响,所以mysql实现了一个由两种类型的锁组成的锁系统来解决。通常被称为共享锁(Share Lock)排他锁(Exclusive Lock),也叫读锁和写锁。

(1) 对记录加S锁

SELECT ... LOCK IN SHARE MODE;或者SELECT ... FOR SHARE;(8.0)

(2) 对记录加X锁

SELECT ... FOR UPDATE;(8.0)

在mysql5.7及之前版本,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0中,如果后面加nowait、skip locked可以跳过等待,或者只返回没锁定的行。

三、从数据操作的粒度划分:表级锁、页级锁、行锁

锁定的数据范围越小,往往系统需要耗费更高的资源。所以数据库系统需要在高并发响应系统性能两方面进行平衡,这样就产生了锁粒度的概念。

1. 表锁

该锁会锁定整张表,并不依赖存储引擎。表锁可以避免死锁问题,但是并发率大打折扣。
表锁分为:表级别的s锁、x锁意向锁自增锁元数据锁

1.1 表级别的s锁、x锁

一般情况下,不会使用innodb存储引擎提供的表级s锁和x锁。在一些特殊情况下,比如崩溃恢复过程中用到。在系统遍历autocommit = 0,innodb_table_locks=1时,手动获取表锁方式未:

LOCK TABLES t READLOCK TABLES t WRITEunlock tables : 解锁当前加锁的表show open tables : 查看表当前是否加锁

1.2 意向锁

意向锁是一种表锁,它的存在是为了协调行锁和表锁关系的,它不与行锁冲突,表明某个事务正在某些行持有了锁。
意向锁分为意向共享锁(IS)和意向排他锁(IX)。
如果我们给某一行数据加上了排他锁,数据库会自动给更大一级的空间加上意向锁。

1.3 自增锁(AUTO-INC锁)

所有插入数据的方式总共分三类,分别是:

  • Simple inserts (简单插入),可以预先确定要插入的行数。
  • Bulk inserts (批量插入),事先不知道要插入的行数。
  • Mixed-mode-inserts (混合模式插入),只指定了部分id的值,还有未知id。

在插入时,mysql采用自增锁的方式来实现。当向使用auto_increment列插入数据时需要获取一种特殊的表级锁,在插入语句时加一个自增锁。然后再语句执行后,再把自增锁释放掉。一个事务再持有锁时,其他事务的插入语句都要被阻塞,所以并发性并不高。所以innodb通过innodb_autoinc_lock_mode的不同取值来提供不同的锁定机制。

  • 0 (传统锁定模式),并发差,就如上面所说的流程。
  • 1 (连续锁定模式) ,mysql8.0之前默认的模式。对于插入数量已知情况下,只在分配过程中保持,而不是直到语句完成。
  • 2 (交错锁定模式),在这种模式下,所有类insert语句都不会使用表级自增锁。自动递增保证在所有并发执行中是唯一且单调递增的。但是可能存在间隙。

1.4 元数据锁(MDL锁)

当对一个表做增删改查操作的时候,加MDL读锁;当要对表的结构变更时,加MDL写锁。
用来解决DML和DDL操作之间一致性问题,不需要显式使用。

2. 行锁

行锁也成为记录锁,mysql服务器层并没有实现行锁机制,行锁只在存储引擎层实现。
InnoDB与MyISAM的最大不同有两点:一是支持事务,二是采用了行级锁。
行锁分为:记录锁间隙锁临键锁插入意向锁

2.1 记录锁

官方的类型名称为:LOCK_REC_NOT_GAP,用来锁住一条记录的。
记录锁分为S型记录锁X型记录锁

2.2 间隙锁

MYSQL在RR隔离级别下是可以解决幻读的。解决方案有两种,第一种是MVCC,第二种是加间隙锁。对一条记录加了gap锁,并不会限制其他事务对这条记录加记录锁或者gap锁。

  • 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
  • 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
  • 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。

2.3 临键锁

临键锁可以理解为一种特殊的间隙锁,上面说过了通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。

2.4 插入意向锁

在插入一条记录时,如果插入位置被别的事务加了gap锁,那么就需要等待。在等待时,innodb规定必须再内存中生成一个锁结构,表明有事务再等待。把这种类型的锁命名为Insert intention locaks
插入意向锁是一种特殊的间隙锁。

3. 行锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁多,开销介于表锁和行锁之间,会出现死锁。
每个层级的锁数量是有限制的,应为锁会占用空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁。

四、从对待锁的态度划分:乐观锁、悲观锁

1 悲观锁

悲观锁总是假设最坏的情况,每次去拿数据都认为别人会修改,所以每次在拿数据的时候都会上锁。(每次只有一个线程使用,其他线程阻塞)。比如行锁、表锁都是在操作前就上锁,其他资源都需要阻塞。
java中的synchronized和reentrantlock等独占锁就是悲观锁思想的实现。
注意: select … fro update语句在执行过程中所有扫描的行都会被锁上,因此在mysql中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。

2. 乐观锁

乐观锁认为对同一数据的并发操作不会总发生,不用每次都上锁。在更新的时候判断有没有人去更新这个数据即可。也就是不采用数据库自身的锁机制,而是通过程序来实现。在java中juc.atomic包下的原子变量类就是使用了乐观锁的一种实现方式:cas实现的。

  • 乐观锁的版本号机制
  • 乐观锁的时间戳机制

五、从加锁方式划分:显示锁、隐式锁

1. 显示锁


即一个事务对新插入的记录可以不显示的加锁。但是由于事务id的存在,相当于加了一个隐式锁。别的事务在对这条记录加锁时,会帮当前事务生成一个锁结构,从而减少锁的数量。

六、其他锁

1 全局锁

全局锁就是对整个数据库加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令。典型的使用场景是:做全库逻辑备份。

Flush tables with read lock

2. 死锁

死锁就是两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。处理方式有两种:

  • 等待,直到超时(innodb_lock_wait_timeout=50s)
  • 使用死锁检测进行死锁处理。

3. 如何避免死锁

  • 合理设计索引,使业务sql尽可能通过索引定位更少的行,减少锁竞争。
  • 调整业务sql执行顺序,避免长时间持有锁的事务在前面。
  • 避免大事务,尽量将大事务拆成多个小事务处理。

七、锁的结构

一个锁的本质就是在内存中创建一个锁结构与之相关,符合下边条件的记录会被放到一个锁结构中。

  • 在同一个事务中进行加锁操作
  • 被加锁的记录在同一页面中
  • 加锁的类型是一样的
  • 等待状态是一样的

innodb存储引擎中的锁结构如下:

八、锁监控

show status like 'innodb_row_lock%'



mysql中把事务和锁的信息记录在了information_schema库中,涉及到的三张表为:innodb_trx、innodb_locks和innodb_lock_waits。