Mysql——锁(1)

目录

1、概述

2. MySQL并发事务访问相同记录

1、读-读情况

2、写-写情况

3、读写、写读的情况

4 并发问题的解决方案

3、锁的不同角度分类

3.1、从数据操作类型划分:读锁,写锁

1、锁定读

2、写操作

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

1、表锁(Table Lock)


事务的隔离性 是由锁实现的。

1、概述

是计算机协调多个进程或线程 并发访问某一资源 的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何时刻 最多只有一个线程 在访问,保证数据的 完整性一致性。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要。

在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性,需要对 并发操作进行控制,因此产生了。同时 锁机制 也为实现MySQL的各个隔离级别提供了保证。锁冲突 也是影响数据库 并发访问性能 的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。

2. MySQL并发事务访问相同记录

并发事务访问相同记录的情况大致可以划分为3种:

1、读-读情况

读-读 情况,即并发事务相继 读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么 问题,所以允许这种情况的发生。

2、写-写情况

写-写 情况,即并发事务相继对相同的记录做出改动。

在这种情况下会发生 脏写 的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务 相继对一条记录做改动时,需要让它们 排队执行 ,这个排队的过程其实是通过 锁 来实现的。这个所谓 的锁其实是一个 内存中的结构 ,在事务执行前本来是没有锁的,也就是说一开始是没有 锁结构 和记录进 行关联的,如图所示:

当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的 锁结构 ,当没有的时候 就会在内存中生成一个 锁结构 与之关联。比如,事务 T1 要对这条记录做改动,就需要生成一个 锁结构 与之关联:

在 锁结构 里有很多信息,为了简化理解,只把两个比较重要的属性拿了出来:
。trx信息: 代表这个锁结构是哪个事务生成的。
。 is_waiting:代表当前事务是否在等待
当事务T1 改动了这条记录后,就生成了一个 锁结构 与该记录关联,因为之前没有别的事务为这条记录加锁,所以 is_waiting 属性就是 false ,我们把这个场景就称之为 获取锁成功,或者 加锁成功,然后就可以继续执行操作了。

在事务T1 提交之前,另一个事务 T2 也想对该记录做改动,那么先看看有没有 锁结构 与这条记录关联,发现有一个锁结构 与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的 is_wating 属性值为 true,表示当前事务需要等待,我们把这个场景就称之为 获取锁失败,或者 加锁失败,图示:

在事务T1 提交之前,另一个事务 T2 也想对该记录做改动,那么先看看有没有 锁结构 与这条记录关联,发现有一个锁结构 与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的 is_wating 属性值为 true,表示当前事务需要等待,我们把这个场景就称之为 获取锁失败,或者 加锁失败,图示:

小结几种说法:

不加锁 意思就是不需要在内存中生成对应的 锁结构 ,可以直接执行操作。

获取锁成功,或者加锁成功 意思就是在内存中生成了对应的 锁结构 ,而且锁结构的 is_waiting 属性为 false ,也就是事务 可以继续执行操作。

获取锁失败,或者加锁失败,或者没有获取到锁。意思就是在内存中生成了对应的 锁结构 ,不过锁结构的 is_waiting 属性为 true ,也就是事务 需要等待,不可以继续执行操作。

3、读写、写读的情况

读-写 或 写-读 ,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重 复读 、 幻读 的问题。

各个数据库厂商对 SQL标准 的支持都可能不一样。比如MySQL在 REPEATABLE READ 隔离级别上就已经 解决了 幻读 问题。

4 并发问题的解决方案

怎么解决 脏读 、 不可重复读 、 幻读 这些问题呢?其实有两种可选的解决方案:

方案一:读操作利用多版本并发控制( MVCC ,下章讲解),写操作进行 加锁 。

所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本 (历史版本由undo日志构建)。查询语句只能读 到在生成ReadView之前 已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而 写操作肯定针对的是 最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写 操作并不冲突。

普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。

在 READ COMMITTED(读已提交) 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一 个ReadView,ReadView的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就 是避免了脏读现象;

在 REPEATABLE READ (可重复读)隔离级别下,一个事务在执行过程中只有 第一次执行SELECT操作 才会 生成一个ReadView,之后的SELECT操作都 复用 这个ReadView,这样也就避免了不可重复读 和幻读的问题。

方案二:读、写操作都采用 加锁 的方式。

如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去 读取记录的最新版本。比如,在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行 加锁 操作,这样也就意味着 读 操作和 写操作也像 写-写操作那样 排队执行。
脏读 的产生是因为当前事务读取了另一个末提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。
不可重复读 的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录自然也不会发生不可重复读了
幻读 问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)。

小结对比发现:

采用 MVCC 方式的话, 读-写 操作彼此并不冲突, 性能更高 。

采用 加锁 方式的话, 读-写 操作彼此需要 排队执行 ,影响性能。

一般情况下我们当然愿意采用 MVCC 来解决 读-写 操作并发执行的问题,但是业务在某些特殊情况 下,要求必须采用 加锁 的方式执行。下面就讲解下MySQL中不同类别的锁。

3、锁的不同角度分类

3.1、从数据操作类型划分:读锁,写锁

对于数据库中并发事务的 读-读 情况并不会引起什么问题。对于 写-写、读-写或 写-读 这些情况可能会引起一些问题,需要使用 MVCC 或者 加锁 的方式来解决它们。在使用 加锁 的方式解决问题时,由于既要允许 读-读 情况不受影响,又要使写-写读-写写-读 情况中的操作 相互阻塞,所以MSQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为“共享锁 (Shared Lock,s Lock)和 排他锁 (Excusive Lock,XLock) ,也叫读锁 (readlock) 和写锁(write lock)

读锁 :也称为 共享锁 、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会 互相影响,相互不阻塞的。

写锁 :也称为 排他锁 、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样 就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

需要注意的是对于 InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。

举例:(行级读写锁)

 如果一个事务 T1 已经获得了某个行的读锁,那么此时另外的一个事务 T2 是可以去获得这个行r的读锁的,因为读取操作并没有改变行r的数据;但是,如果某个事务 T3 想获得行r的写锁,则它必须等待事务 T1、T2 释放掉行r上的读锁才行。

总结: 这里的兼容是指对同一张表或记录的锁的兼容性情况

1、锁定读

在采用 加锁 方式解决 脏读、不可重复读、幻读 这些问题时,读取一条记录时需要获取该记录的 S锁,其实是不严谨的,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此MySOL提出了两种比较特殊的SELECT 语句格式:

  • 对读取的记录加 S锁 : 
SELECT  ... LOCK IN SHARE MODE;

#或

SELECT ... FOR SHARE;#(8.0新增)

在普通的SELECT语句后边加LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的 S锁 (比方说别的事务也使用 SELECT ... LOCK IN SHAREMODE 语句来读取这些记录),但是不能获取这些记录的X锁 (比如使用 SELECT ... FOR UPDATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的 S锁 释放掉。

  • 对读取的记录 加x锁
SELECT ... FOR UPDATE;

在普通的SELECT语句后边加FOR UPDATE,如果当前事务执行了该语句,那么它会为读取到的记录加X锁这样既不允许别的事务获取这些记录的 S锁 (比方说别的事务使用 SELECT ... LOCK IN SHARE MODE 语来读取这些记录),也不允许获取这些记录的X锁 (比如使用 SELECT ... FOR UPDATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的 S锁 或者 X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的 X锁 释放掉。

Mysql8.0 新特性

在5.7及之前的版本,SELECT ... FOR UPDAE,如果获取不到锁,会一直等待,直到innodb_lock_wat_timeout 超时。在8.0版本中,SELECT ... FOR UPDATE,SELECT ... FOR SHARE添加NOWAITSKIP LOCKED 语法,跳过锁等待,或者跳过锁定
。通过添加NOWAIT、SKIP LOCKED语法,能够立即返回。如果查询的行已经加锁
。那么NOWAIT会立即报错返回
。而SKIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行

2、写操作

平常所用到的 写操作 无非是 DELETE 、 UPDATE、INSERT 这三种:

  • DELETE :

对一条记录做DELETE操作的过程其实是先在 B+ 树中定位到这条记录的位置,然后获取这条记录的 X锁,再执行 delete mark 操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁 的锁定读。

  • UPDATE:

在对一条记录做UPDATE操作时分为三种情况:

情况1: 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。则先在 B+ 树中定位到这条记录的位置,然后再获取一下记录的 X锁,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取锁的 锁定读。


情况2: 未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。则先在 B+ 树中定位到这条记录的位置,然后获取一下记录的 X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取X锁的 锁定读,新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。

情况3: 修改了该记录的键值,则相当于在原记录上做DELETE 操作之后再来一次 INSERT 操作,加锁操作就需要按照DELETE 和 INSERT 的规则进行了。

  • INSERT :

般情况下,新插入一条记录的操作并不加锁,通过一种称之为 隐式锁 的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

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


为了尽可能提高数据库的并度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很 耗资源 的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应和系统性能 两方面进行平衡,这样就产生了“锁粒度(Lock granularity)”的概念
对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细,其实一个事务也可以在 表级别进行加锁,自然就被称之为 表级锁 或者 表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁、页级锁和行锁。

1、表锁(Table Lock)

该锁会锁定整张表,它是 MySQL 中最基本的锁策略,并 不依赖于存储引擎 (不管你是 MySQL 的什么存储引擎对于表锁的策略都是一样的),并且表锁是 开销最小 的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的 避免死锁 问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致 并发率大打折扣。

(一)表级别的s锁、x锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级 别的 S锁 或者 X锁 的。在对某个表执行一些诸如 ALTER TABLE 、 DROP TABLE 这类的 DDL 语句时,其 他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务 中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行 DDL 语句也会 发生阻塞。这个过程其实是通过在 server层 使用一种称之为 元数据锁 (英文名: Metadata Locks , 简称 MDL )结构来实现的。

一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下,比方说 崩 溃恢复 过程中用到。比如,在系统变量 autocommit=0,innodb_table_locks = 1  时, 手动 获取 InnoDB存储引擎提供的表t 的 S锁 或者 X锁 可以这么写:

LOCK TABLES t READ :InnoDB存储引擎会对表 t 加表级别的 S锁 。

LOCK TABLES t WRITE :InnoDB存储引擎会对表 t 加表级别的 X锁 。

 unlock tables; 解锁

不过尽量避免在使用InnoDB存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句,它们并不会提供 什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的 行锁 ,关于 InnoDB表级别的 S锁 和 X锁 大家了解一下就可以了

测试:

1、创建表并添加数据:

CREATE TABLE mylock(
id INT NOT NULL PRIMARY KEY auto_increment;
NAME VARCHAR(20);
)ENGINE myisam; 存储引擎使用InnoDB也可以,但是 不建议

#插入一条数据
INSERT INTO mylock(NAME) values('a');



#查询表中所有数据
SELECT * from mylock;

2、查看表中加过的锁

SHOW OPEN TABLES;  # 主要关注In_use 字段的值

或者

SHOW OPEN TABLES WHERE In_use>0;

3、手动加表锁命令

LOCK TABLES t READ; #存储引擎会对表t加标记别的共享锁,共享锁也叫读锁,S锁(Share LOCK)


LOCK TABLES t WRITE; #存储引擎会对表t加标记别的独占锁,独占锁也叫排他锁,写锁或 X锁(eXclusive LOCK)

4、解锁

Unlock tables;

5、加读锁

我们为mylock表添加read锁(读阻塞写),观察阻塞情况,流程如下:

6、加写锁

为mylock 表加write锁,观察阻塞情况: 

 

总结:

MyISAM在执行语句(SELECT)时,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁,InnoDB存储引擎不会为这个表添加 标记别的读锁和 写锁。

MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)

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

(二)、意向锁(intention lock)

InnoDB 支持 多粒度锁(multiple granularity locking) ,它允许 行级锁 与 表级锁 共存,而意向 锁就是其中的一种 表锁 。

意向锁分为两种:

  • 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)
-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;

 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)

-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;

即:意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前, InooDB 会先获取该数据行 所在数据表的对应意向锁 。 

1.意向锁要解决的问题
现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁,如果存在意向锁,那么此时就会受到由T1控制的 表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁。

在数据表的场景中,如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。

  • 如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上 添加意向共享锁
  • 如果事务根要获得数据表中某些记录的排他锁,就需要在数据表上 添加意向排他锁

这时,意向锁会告诉其他事务已经有人锁定了表中的某些记录。

我们看一下意向锁之间的兼容和互斥性:

即意向锁之间是兼容的,但是对其他排他锁是不兼容的

从上面的案例可以得到如下结论:

1. InnoDB 支持 多粒度锁 ,特定场景下,行级锁可以与表级锁共存。

2. 意向锁之间互不排斥,但除了 IS 与 S 兼容外, 意向锁会与 共享锁 / 排他锁 互斥 。

3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。

4. 意向锁在保证并发性的前提下,实现了 行锁和表锁共存 且 满足事务隔离性 的要求。

(三)、自增锁 (AUTO-INC锁)

在设计表主键 自增的情况下,会用到此锁,且此锁的使用情况,随版本都有所变化,此处不做详细描述。

(四)元素据锁(MDL锁)

MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL 的作用是,保证读写的正确性。比 如,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更 ,增加了一 列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写 锁。

读锁之间不互斥(此处的读锁不互斥,指的是 MDL读锁不互斥,而不涉及其他锁),因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。 不需要显式使用,在访问一个表的时候会被自动加上。

测试:

一个事务 进行 DML操作 自动加上 MDL 读锁 

另一个事务 进行 DDL 操作,系统默认去加MDL 写锁,因为读写互斥,此时此事务正在被阻塞。

我们查看此指令

发返现 MDL 确实被加了 。

下一节,我们看看 行锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值