一.mysql锁

数据库系统角度:

  • 表级锁: 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低
  • 行级锁: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高
  • 页级锁: 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

开发人员角度:

  • 悲观锁是悲观的认为每次都会发生并发冲突,屏蔽一切可能违反数据完整性的操作,在可能出现并发的地方加上锁机制,按串行化一一执行

通俗解释是指总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁,读锁,写锁等,都是在做操作之前先上锁,还有 Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现

  • 乐观锁是乐观的认为每次都不会发生并发冲突,只在提交操作时检查是否违反数据完整性,一般是最后提交时用版本做对比,查看是否数据完整才允许提交成功

 通俗解释是指总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁,还在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的

MyIsam 实现了表锁。表锁可以针对数据库表加锁,在锁的灵活性上不如行锁。
InnoDB 存储引擎实现了行锁与表锁(意向锁)。行锁可以以行为单位对数据集进行锁定。

 

二.表级锁

由于 MyISAM 存储引擎使用的锁定机制完全是由 MySQL 提供的表级锁定实现,所以下面我们将以 MyISAM 存储引擎作为示例存储引擎,MySQL 的表级锁有两种模式

  • 表共享读锁(Table Read Lock)
  • 表独占写锁(Table Write Lock)

锁模式的兼容性是这样的

  • 对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求
  • 对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作

MyISAM 在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此,用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁

在 MyISAM 存储引擎中,当执行 SQL 语句的时候是自动加表锁的,在 InnoDB 存储引擎中,如果没有使用索引,表锁也是自动加的

 

三.行级锁(InnoDB)

行级锁不是 MySQL 自己实现的锁定方式,而是由其他存储引擎自己所实现的,如广为大家所知的 InnoDB 存储引擎。考虑到行级锁定均由各个存储引擎自行实现,而且具体实现也各有差别,而 InnoDB 是目前事务型存储引擎中使用最为广泛的存储引擎,所以这里我们就主要分析一下 InnoDB 的锁定特性

1. 锁类型

总的来说,InnoDB 的锁定机制和 Oracle 数据库有不少相似之处。InnoDB 的行级锁定同样分为两种类型

  • 共享锁(S): 又叫读锁,其他事务可以继续加共享锁,但是不能继续加排他锁
  • 排他锁(X): 又叫写锁,一旦加了排他锁之后,其他事务就不能加锁了

2. 意向锁

而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB 也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种

  • 意向共享锁(IS): 表达一个事务想要获取一张表中某几行的共享锁
  • 意向排他锁(IX): 表达一个事务想要获取一张表中某几行的排他锁

这个锁有什么用呢,为什么需要这个锁呢?

首先说一下如果没有这个锁,要给这个表加上表锁,一般的做法是去遍历每一行看看它是否有行锁,这样的话效率太低,而我们有意向锁,只需要判断是否有意向锁即可,不需要再去一行行的去扫描

更深层次是意向锁的作用就是当一个事务在获取所需资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以在锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在

3. 锁类型总结

 

共享锁(S)

排他锁(X)

意向共享锁(IS)

意向排他锁(IX)

共享锁(S)

兼容

冲突

兼容

冲突

排他锁(X)

冲突

冲突

冲突

冲突

意向共享锁(IS)

兼容

冲突

兼容

兼容

意向排他锁(IX)

冲突

冲突

兼容

兼容

如果一个事务请求的锁模式与当前锁兼容,InnoDB 就将请求的锁授予该事务,反之,如果两者不兼容,该事务就要等待锁释放

  1. 意向锁是 InnoDB 自动加的,不需用户干预
  2. 对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会自动给涉及数据集加排他锁(X)
  3. 对于普通 SELECT 语句,InnoDB 不会加任何锁

 

通过以下显示加锁: 

-- 共享锁(S)
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
-- 排他锁(X)
SELECT * FROM table_name WHERE ... FOR UPDATE

用 SELECT ... LOCK IN SHARE MODE 获得共享锁(S),主要用在需要数据依存关系时来确认某行记录是否存在,并确保没有人对这个记录进行 UPDATE 或者 DELETE 操作

但是如果当前事务也需要对该记录进行更新操作,则很有可能造成死锁,对于锁定行记录后需要进行更新操作的应用,应该使用 SELECT... FOR UPDATE 方式获得排他锁(X)

 

四. 行级锁机制

InnoDB 行锁

InnoDB 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁

在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能,下面通过一些实际例子来加以说明

  1. 在不通过索引条件查询的时候,InnoDB 确实使用的是表锁,而不是行锁
  2. 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
  3. 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁
  4. 即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查 SQL 的执行计划,以确认是否真正使用了索引

自增长锁

 自增长锁是一种特殊的表锁机制,如果表中存在自增字段,MySQL 便会自动维护一个自增锁,提升并发插入性能,对于这个锁有几个特点

  • 在 SQL 执行完就释放锁,并不是事务执行完
  • 对于 INSERT...SELECT 大数据量插入会影响插入性能,因为会阻塞另外一个事务执行
  • 自增算法可以配置

查看自增长锁模式: 

show variables like 'innodb_autoinc_lock_mode';

 在 MySQL 中 innodbautoinclock_mode 有 3 种配置模式 0、1、2,分别对应

  • 传统模式(0): 也就是我们最上面的使用表锁
  • 连续模式(1): 对于插入的时候可以确定行数的使用互斥量,对于不能确定行数的使用表锁的模式
  • 交错模式(2): 所有的都使用互斥量,为什么叫交错模式呢,有可能在批量插入时自增值不是连续的,当然一般来说如果不看重自增值连续一般选择这个模式,性能是最好的

什么时候MySQL读会上锁 mysql什么时候会加锁_加锁

InnoDB 存储引擎中自增长的实现和 MyISAM 不同。MyISAM 存储引擎是表锁设计,自增长不用考虑并发插入的问题。在 InnoDB 存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列,如果不是第一个列,则 MySQL 会抛出异常。MyISAM 存储引擎没有这个问题

 

五.InnoDB锁算法

1. 记录锁(Record Lock)

记录锁是锁住记录的,仅锁定一行记录(如共享锁、排他锁),这里要说明的是这里锁住的是索引记录,而不是我们真正的数据记录

  • 如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住
  • 如果没有表上没有索引(包括没有主键),则会使用隐式(隐藏)的主键索引进行加锁
  • 如果要锁的列没有索引,则会进行全表记录加锁
  • 查询条件的列是唯一索引的情况下,临键锁退化为记录锁

2. 间隙锁(GAP Lock)

间隙锁顾名思义锁间隙(GAP Lock,锁定一个范围,但不包含记录本身),不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫 GAP 锁,其不会阻塞其他的 GAP 锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键

关闭间隙锁的 2 种方式

  1. 将事务隔离级别变为 Read Committed(提交读)
  2. 将参数 innodb_locks_unsafe_for_binlog 设置为 1

在上述配置下,除了外键和唯一性检查依然需要间隙锁,其余情况仅适用行锁进行锁定

3. 临键锁(Next-Key Lock)

Next-Key Lock,等于记录锁(Record Lock) + 间隙锁(GAP Lock),锁定一个范围,并且锁定记录本身。主要是阻止多个事务将记录插入到同一个范围内,从而避免幻读

在 RR(可重读) 隔离级别下(InnoDB 默认),InnoDB 对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。

为什么呢,因为唯一索引能确定行数,而其他索引不能确定行数,有可能在其他事务中会再次添加这个索引的数据造成幻读,这里也说明了为什么 MySQL 可以在 RR 级别下解决幻读

4. 插入意向锁

对已有数据行的修改与删除,必须加强互斥锁 X 锁,那对于数据的插入,是否还需要加这么强的锁,来实施互斥呢?插入意向锁,孕育而生

插入意向锁,是间隙锁(Gap Lock)的一种(所以,也是实施在索引上的),它是专门针对 INSERT 操作的

可以看出插入意向锁是在插入的时候产生的,在多个事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待(多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此)

假设有一个记录索引包含键值 4 和 7,不同的事务分别插入 5 和 6,每个事务都会产生一个加在 4-7 之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突

这里要说明的是如果有间隙锁了,插入意向锁会被阻塞

 

六.封锁协议 

在运用X锁和S锁对数据对象加锁的时候,还需要约定一些规则。比如何时申请X锁或S锁、持锁时间、何时释放等。这些规则称为封锁协议

1. 一次封锁协议

因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据

2. 两段锁协议

将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段: 在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得 S 锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得 X 锁(排它锁(只有当前数据无共享锁,无排它锁之后才能获得),其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。事务提交时(commit)和事务回滚时(rollback)会自动的同时释放该事务所加的 INSERT、UPDATE、DELETE 对应的锁

这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的

3. 三级封锁协议

① 一级封锁协议

事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。

可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。

但不能解决读脏数据和不可重复读的问题,因为在一级封锁协议中,仅仅读数据而对其进行修改是不需要进行加锁的

 ② 二级封锁协议

在一级的基础上,要求读取数据 A 时必须加 S 锁, 读取完马上释放 S 锁。

可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。

但不能解决不可重复读问题,因为读完数据后就释放S锁,其他事务可以再加锁进行修改

③ 三级封锁协议

在一级协议的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁

可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。