【MySQL锁】基础篇

目录

1.什么是锁

2.锁用来解决什么问题

2.1.脏读(select)

2.2.不可重复读(update)

2.3.幻读(insert)

3.锁的分类

3.1.使用方法

3.1.1.共享锁

3.1.2.排他锁

4.加锁范围

4.1.全局锁

4.1.1.加锁方式

4.1.2.释放锁

4.1.3.使用场景

4.2.表级锁

4.2.1读锁

4.2.2.写锁

4.2.3.MDL元数据锁

4.2.4.意向锁

4.2.5.AUTO-INC锁

4.2.6.锁的兼容性

4.3.行锁

4.3.1.记录锁(Record Lock)

4.3.2.间隙锁(Gap Lock)

4.3.3.临建锁(Next-Key Lock)

4.3.4.插入意向锁(Insert Intention Lock)

4.3.5.临键锁在什么情况下会降级

4.3.5.1.唯一索引等值查询

4.3.5.2.非唯一索引等值查询

5.乐观锁&悲观锁

6.死锁&死锁检测


1.什么是锁

MySQL中为了保证数据访问的一致性与有效性等功能,实现了锁机制,MySQL中的锁是在服务器层或者存储引擎层实现的。

2.锁用来解决什么问题

锁是用来解决并发事务的访问问题,我们已经知道事务并发执行时可能带来的各种问题,最大的一个难点是:一方面要最大程度地利用数据库的并发访问,另外一方面还要确保每个用户能以一致的方式读取和修改数据,尤其是一个事务进行读取操作,另一个同时进行改动操作的情况下。

一个事务进行读取操作,另一个进行改动操作,我们前边说过,这种情况下可能发生脏读、不可重复读、幻读的问题。

2.1.脏读(select)

如果一个事务读取到了另一个未提交事务修改过的数据,我们就称发生了脏读现象。

假设现在有两个事务A、B:

  • 假设现在A的余额是100,事务A正在准备查询Jay的余额
  • 事务B先扣减Jay的余额,扣了10,但是还没提交
  • 最后A读到的余额是90,即扣减后的余额

因为事务A读取到事务B未提交的数据,这就是脏读。

2.2.不可重复读(update)

同一个事务内,前后多次读取,读取到的数据内容不一致

假设现在有两个事务A和B:

  • 事务A先查询Jay的余额,查到结果是100
  • 这时候事务B 对Jay的账户余额进行扣减,扣去10后,提交事务
  • 事务A再去查询Jay的账户余额发现变成了90

事务A被事务B干扰到了!在事务A范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读

2.3.幻读(insert)

如果一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录(如insert、delete、update),就意味着发生了幻读。

假设现在有两个事务A、B:

  • 事务A先查询id大于2的账户记录,得到记录id=2和id=3的两条记录
  • 这时候,事务B开启,插入一条id=4的记录,并且提交了
  • 事务A再去执行相同的查询,却得到了id=2,3,4的3条记录了。

事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入新的数据,并提交事务,然后事务A再次查询相同的范围,两次读取到的结果集却不一样了,这就是幻读。

3.锁的分类

3.1.使用方法

3.1.1.共享锁

共享锁,Share lock,也叫读锁。它是指当对象被锁定时,允许其它事务读取该对象,也允许其它事务从该对象上再次获取共享锁,但不能对该对象进行写入。

加锁方式:

# 方式1
select ... lock in share mode;
# 方式2
select ... for share;

如果事务T1 在某对象持有共享(S)锁,则事务T2 需要再次获取该对象的锁时,会出现下面两种情况:

  • 如果T2 获取该对象的共享(S)锁,则可以立即获取锁;
  • 如果T2 获取该对象的排他(X)锁,则无法获取锁;

3.1.2.排他锁

排它锁,Exclusive Lock,也叫写锁或者独占锁,主要是防止其它事务和当前加锁事务锁定同一对象。同一对象主要有两层含义:

  • 当排他锁加在表上,则其它事务无法对该表进行insert,update,delete,alter,drop等更新操作;
  • 当排他锁加在表的行上,则其它事务无法对该行进行insert,update,delete,alter,drop等更新操作;

加锁方式:

select ... for update;

4.加锁范围

4.1.全局锁

全局锁,顾名思义,就是对整个数据库实例加锁。它是粒度最大的锁。

4.1.1.加锁方式

flush tables with read lock

指令执行完,整个数据库就处于只读状态了,其他线程执行以下操作,都会被阻塞:

  • 数据更新语句被阻塞,包括 insert, update, delete语句;
  • 数据定义语句被阻塞,包括建表 create table,alter table、drop table 语句;
  • 更新操作事务commit语句被阻塞;

4.1.2.释放锁

在MySQL中释放锁有2种方式:

1.执行指令

unlock tables

2.加锁的会话断开,全局锁也会被自动释放

4.1.3.使用场景

全局锁的典型使用场景是做全库逻辑备份,在备份过程中整个库完全处于只读状态。如下图:

  • 假如在主库上备份,备份期间,业务服务器不能对数据库执行更新操作,因此涉及到更新操作的业务就瘫痪了;
  • 假如在从库上备份,备份期间,从库不能执行主库同步过来的 binlog,会导致主从延迟越来越大,如果做了读写分离,那么从库上获取数据就会出现延时,影响业务;

从上述分析可以看出,使用全局锁进行数据备份,不管是在主库还是在从库上进行备份操作,对业务总是不太友好。那不加锁行不行?我们可以通过下面还钱转账的例子,看看不加锁会不会出现问题:

  • 备份前:账户A 有1000,账户B 有500
  • 此时,发起逻辑备份
  • 假如数据备份时不加锁,此时,客户端A 发起一个还钱转账的操作:账户A 往账户B 转200
  • 当账户A 转出200完成,账户B 转入200 还未完成时,整个数据备份完成
  • 如果用该备份数据做恢复,会发现账户A 转出了200,账户B 却没有对应的转入记录,这样就会产生纠纷:A 说我账户少了 200, B 说我没有收到,最后,A,B谁都不干。

既然不加锁会产生错误,加全局锁又会影响业务,那么有没有两全其美的方式呢?

有,MySQL官方自带的逻辑备份工具 mysqldump,具体指令如下:

mysqldump –single-transaction

执行该指令,在备份数据之前会先启动一个事务,来确保拿到一致性视图, 加上 MVCC 的支持,保证备份过程中数据是可以正常更新。但是,single-transaction方法只适用于库中所有表都使用了事务引擎,如果有表使用了不支持事务的引擎,备份就只能用 FTWRL 方法。

4.2.表级锁

表锁就是对整张表加锁,包含读锁和写锁,由MySQL Server实现,表锁需要显示加锁或释放锁,具体指令如下:

# 给表加写锁
lock tables tablename write;

# 给表加读锁
lock tables tablename read;

# 释放锁
unlock tables;

4.2.1读锁

代表当前表为只读状态,读锁是一种共享锁。需要注意的是,读锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:

  • 加锁线程只能对当前表进行读操作,不能对当前表进行更新操作,不能对其它表进行所有操作;
  • 其它线程只能对当前表进行读操作,不能对当前表进行更新操作,可以对其它表进行所有操作;

4.2.2.写锁

写锁是一种独占锁,需要注意的是,写锁除了会限制其它线程的操作外,也会限制加锁线程的行为,具体限制如下:

  • 加锁线程对当前表能进行所有操作,不能对其它表进行任何操作;
  • 其它线程不能对当前表进行任何操作,可以对其它表进行任何操作;

4.2.3.MDL元数据锁

DDL的写锁请求优先级高于DML

元数据锁:metadata lock,简称MDL,它是在MySQL 5.5版本引进的。元数据锁不用像表锁那样显式的加锁和释放锁,而是在访问表时被自动加上,以保证读写的正确性。

加锁和释放锁规则如下:

  • MDL读锁之间不互斥,也就是说,允许多个线程同时对加了 MDL读锁的表进行CRUD(增删改查)操作;
  • MDL写锁,它和读锁、写锁都是互斥的,目的是用来保证变更表结构操作的安全性。也就是说,当对表结构进行变更时,会被默认加 MDL写锁,因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
  • MDL读写锁是在事务commit之后才会被释放;

当一个事务开始执行SELECT或UPDATE语句时,MySQL会自动获取相关表的共享元数据锁;而当一个事务开始执行CREATE TABLE或DROP TABLE语句时,则会自动获取相应表的独占元数据锁。在事务提交或回滚后,相应的元数据锁也会自动释放。

4.2.4.意向锁

当事务A有行锁时,MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。

意向锁为什么是表锁而不是行锁?

意向锁时表锁的话在遍历的时候只需要遍历一遍就可以,但是若时行锁则会需要一行一行遍历

意向锁,Intention lock,它是一种表锁,用来标识事务打算在表中的行上获取什么类型的锁。 不同的事务可以在同一张表上获取不同种类的意向锁,但是第一个获取表上意向排他(IX) 锁的事务会阻止其它事务获取该表上的任何 S锁 或 X 锁。反之,第一个获得表上意向共享锁(IS) 的事务可防止其它事务获取该表上的任何 X 锁。

意向锁通常有两种类型:

  • 意向共享锁(IS),表示事务打算在表中的各个行上设置共享锁。
  • 意向排他锁(IX),表示事务打算对表中的各个行设置排他锁。

意向锁是InnoDB自动加上的,加锁时遵从下面两个协议:

  • 事务在获取表中行的共享锁之前,必须先获取表上的IS锁或更强的锁。
  • 事务在获取表中行的排他锁之前,必须先获取表上的IX锁。

4.2.5.AUTO-INC锁

AUTO-INC锁是一种特殊的表级锁,当表中有AUTO_INCREMENT的列时,如果向这张表插入数据时,InnoDB会先获取这张表的AUTO-INC锁,等插入语句执行完成后,AUTO-INC锁会被释放。

AUTO-INC锁可以使用innodb_autoinc_lock_mode变量来配置自增锁的算法,

innodb_autoinc_lock_mode变量可以选择三种值如下表:

4.2.6.锁的兼容性

4.3.行锁

行锁只用在事务中才能使用,若是事务提交行锁也会自动解锁,行锁是加在索引上面的

行锁是针对数据表中行记录的锁。MySQL 的行锁是在引擎层实现的,并不是所有的引擎都支持行锁,比如,InnoDB引擎支持行锁而 MyISAM引擎不支持。

InnoDB 引擎的行锁主要有三类:

  • Record Lock: 记录锁,是在索引记录上加锁;
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录;
  • Next-key Lock:Gap Lock + Record Lock,锁定一个范围(Gap Lock实现),并且锁定记录本身(Record Lock实现)

4.3.1.记录锁(Record Lock)

Record Lock:记录锁,是针对索引记录的锁,锁定的总是索引记录,而不是真正的数据记录

例如,select id from user where id = 1 for update; for update 就显式在索引id上加行锁(排他锁),防止其它任何事务 update或delete id=1 的行,但是对user表的insert、alter、drop操作还是可以正常执行。

4.3.2.间隙锁(Gap Lock)

锁住的是一个区间,而不仅仅是这个区间中的每一条数据是在可重复读的隔离级别下 为了解决幻读引入的锁机制

Gap Lock:间隙锁,锁住两个索引记录之间的间隙上,由InnoDB隐式添加。比如(1,3) 表示锁住记录1和记录3之间的间隙,这样记录2就无法插入,间隙可能跨越单个索引值、多个索引值,甚至是空。

4.3.3.临建锁(Next-Key Lock)

可以解决幻读的问题

注意:临建锁只与非唯一索引列有关,在唯一索引列上不存在临建锁

Next-Key锁,称为临键锁,它是Record Lock + Gap Lock的组合,用来锁定一个范围,并且锁定记录本身锁,它是一种左开右闭的范围,可以用符号表示为:(a,b]。

4.3.4.插入意向锁(Insert Intention Lock)

插入意向锁,它是一种特殊的间隙锁,特指插入操作产生的间隙锁。

4.3.5.临键锁在什么情况下会降级

4.3.5.1.唯一索引等值查询
  1. 当查询的记录是存在的,next-key lock 会退化成【记录锁】
  2. 当查询的记录是不存在的,next-key lock 会退化成【间隙锁】
4.3.5.2.非唯一索引等值查询
  1. 当查询的记录存在时,除了会加 next-key lock 外,还额外加间隙锁,也就是会加两把锁。
  2. 当查询的记录不存在时,只会加 next-key lock,然后会退化为间隙锁,也就是只会加一把锁。

5.乐观锁&悲观锁

在MySQL中,无论是悲观锁还是乐观锁,都是人们对概念的一种思想抽象,它们本身还是利用 MySQL提供的锁机制来实现的。其实,除了在MySQL数据,像 Java语言里面也有乐观锁和悲观锁的概念。

  • 悲观锁,可以理解成:在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking),采用的是先获取锁再操作数据的策略,可能会产生死锁;
  • 乐观锁,是相对悲观锁而言,一般不会利用数据库的锁机制,而是采用类似版本号比较之类的操作,因此乐观锁不会产生死锁的问题;

6.死锁&死锁检测

当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁。可以通过下面的指令查看死锁

show engine innodb status\G

当出现死锁以后,有两种策略:

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置,InnoDB 中 innodb_lock_wait_timeout 的默认值是 50s。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其它事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启死锁检测。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值