mysql innodb 写锁_JAVA面试系列 - Mysql InnoDB 锁机制介绍

概述

在mysql中,锁机制看起来很复杂,一堆名词:排他锁、共享锁、表锁、间隙锁、意向锁等等,搞的初学者云里雾里。同时锁的相关知识又跟事务隔离级别、索引等概念有千丝万缕的关系,是面试中的常规问题。

c2d3c445f16e5e2bfbef939546e9492c.png

上面的脑图是对mysql锁相关知识的一个梳理,希望能够帮到大家,让大家能够:

能让我们在特定的场景下派得上用场

更好把控自己写的程序

在跟别人聊数据库技术的时候可以搭上几句话

构建自己的知识库体系!在面试的时候不虚

读、写锁

按锁的应用场景来看,分为读锁和写锁,读锁又可称为S锁和共享锁;写锁又可称为X锁和排他锁。简单来说,读锁 = S锁 = 共享锁,同样,写锁 = X锁 = 排他锁。

正如他们的取名,只要碰到排他锁,那么就会阻塞,具体阻塞情况如下表:

读锁

写锁

读锁

写锁

读读不阻塞:当前用户在读数据,其他的用户也在读数据,不会加锁

读写阻塞:当前用户在读数据,其他的用户不能修改当前用户读的数据,会加锁!

写写阻塞:当前用户在修改数据,其他的用户不能修改当前用户正在修改的数据,会加锁!

读锁和写锁是互斥的,读写操作是串行

行锁

我们使用Mysql一般是使用InnoDB存储引擎的。InnoDB和MyISAM有两个本质的区别:

InnoDB支持行锁

InnoDB支持事务

对于行锁来说,也分2种类型的锁

共享锁(S锁),允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。也叫做读锁,读锁是共享的,多个客户可以同时读取同一个资源,但不允许其他客户修改。

排他锁(X锁),允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。也叫做写锁,写锁是排他的,写锁会阻塞其他的写锁和读锁。

其实事务隔离级别就是通过锁机制来实现的,只不过隐藏了加锁的细节,下面来看看两者的关系。

事务隔离级别

大家都知道,innodb的事务隔离级别有4种

Read uncommitted,会出现脏读、不可重复读、幻读

脏读就是一个事务读取到另一个事务未提交的数据。出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据。

Read committed,会出现不可重复读、幻读

避免脏读的做法很简单:就是把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的。即读写是串行的。

但Read committed会出现不可重复读,即一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改。多次查询数据库的结果都不一样。

Repeatable read,会出现幻读

和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致。可以把不可重复读理解为数据更新,幻读是数据插入。

innodb通过MVCC解决了不可重复读的问题,MVCC的具体原理下面介绍。同时结合间隙锁,避免了幻读。即innodb的Repeatable read其实不会出现幻读的问题,innodb的事务默认级别就是Repeatable read

Serializable,串行,避免以上所有问题

那么MVCC究竟是一种什么机制,能够解决不可重复读的问题?

MVCC

MVCC即多版本并发控制,可以简单认为 是行级锁的一个升级版。前面提到,只有读-读场景是不阻塞的,其他只有要写(排他锁)场景,都是阻塞的,一定程度上影响了读写效率。基于提升并发性能的考虑,MVCC一般读写是不阻塞的,所以说MVCC很多情况下避免了加锁的操作。

InnoDB中的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的删除时间。当然存储的并不是实际的时间值,而是系统版本号。没开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为此事务的版本号,用来和查询到的每行记录的版本号进行比较。

举个select的例子,InnoDB会根据以下两个条件检查每行记录:

InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的

行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除。

简单总结下,多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。

MVCC虽然解决了不可重复读问题,但是无法解决幻读,需要配合间隙锁。

间隙锁(解决幻读)

首先我们看个例子,初始表如下:

id

x

y

创建时间

删除时间

1

30

10

1

undefined

很简单,一个自增id,一列x,一列y,假设有个限制条件:x+y <= 100。然后两个事务同时并发执行:

T2(事务id=2):set y=60,事务隔离级别是不可重复度,所以此事务内,x=30, y=10,更新y后,x+y=30+60 = 90,满足条件

T3(事务id=3):set x=50,事务隔离级别是不可重复度,所以此事务内,x=50, y=10,更新y后,x+y=50+10 = 60,满足条件

T2和T3提交后,x+y=50+60=110 不符合小于100的要求。

Update的本质是 read --> write,MySQL(innodb)为了解决这个问题,强行把 read 分成了 snapshot read(快照读)和 locking read (当前读)。在 UPDATE 或者 SELECT ... FOR UPDATE 的时候,innodb 引擎实际执行的是当前读。

在一个支持MVCC的并发系统中, 我们需要支持两种读, 一个是快照读, 一个是当前读。

快照读:简单的select操作,属于快照读,不加锁。

当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁, 读取的是最新数据。

给一个幻读的例子:

事务A种执行修改

update user set col1='new_val' where id=1;

结果:

Query OK, 0 rows affected (0.00 sec)

Rows matched: 0 Changed: 0 Warnings: 0

事务B插入一条数据,并提交

begin;

insert into user values('A');

commit;

事务A种再次执行修改

update user set col1='new_val' where id=1;

结果:

Query OK, 1 rows affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

问题的本质是因为 update语句的查找阶段相当于select ... for update,这会更新事务A的 ReadView,从而可以读到「其他事务已提交的修改」。即出现了幻读

把上面的例子用事务版本号来解释:

事务A(事务版本号=1)种执行修改,此时是空表,所以update的select结果是0

事务B(事务版本号=2)插入一条数据,并提交

id

col1

创建时间

删除时间

1

A

2

undefined

此时如果事务A进行快照读(snapshot select),那么按照刚才mvcc的解释,由于库中id=1的记录的创建时间(事务版本号)为2,大于当前事务的版本号1,所以不会被查找出来,符合Repeatable read。但是如果此时执行的是update操作,执行的读是当前读(select for update)(InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间),成功更新id=1的记录,即幻读

id

col1

创建时间

删除时间

1

A

2

1

1

A

1

undefined

InnoDB 通过加间隙锁的方式,解决幻读。innodb对于键值在条件范围内但并不存在的记录(叫做『间隙』)加锁,这种锁机制就是所谓的间隙锁。 相对的,可以把上面不同的行锁称为记录锁。

间隙锁产生的条件分唯一索引和普通索引:

唯一索引

对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁

对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE id BETWEEN 5 AND 7 FOR UPDATE

普通索引

在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样

在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序。

针对以上幻读的例子,update语句select * from user where id=1 for update,id是唯一索引,但是由于id=1的记录不存在,于是产生了间隙锁(排他),可以阻塞其他事务的insert操作。

MySQL(innodb)的选择是允许在快照读之后执行当前读,并且更新 snapshot 镜像的版本。严格来说,这个结果违反了 repeatable read 隔离级别,,但是 who cares 呢,毕竟官方都说了:“This is not a bug but an intended and documented behavior.”

表锁

表锁相对来说就很简单了,表锁顾名思义,就是锁针对的范围是整张表。表锁开销小,加锁快,不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低。现在考虑这样一个情景:

事务A获取了某一行的排他锁,并未提交:

SELECT * FROM users WHERE id = 6 FOR UPDATE;

事务B想要获取users表的表锁:

LOCK TABLES users READ;

因为共享锁与排他锁互斥,所以事务B得确保:

当前没有其他事务持有 users 表的排他锁。

当前没有其他事务持有 users 表中任意一行的排他锁 。

为了检测是否满足第二个条件,事务 B 必须在确保 users表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。扫描所有行这明显是一个效率很差的做法,于是提出了意向锁。

意向锁

事务在获取行锁(包括读锁和写锁)的同时会获取表的意向锁(包括读锁和写锁)。

意向锁之间是相互兼容的:

意向共享锁

意向排他锁

意向共享锁

兼容

兼容

意向排他锁

兼容

兼容

意向锁和表级锁之间存在互斥情况

意向共享锁

意向排他锁

表级共享锁

兼容

互斥

表级排他锁

互斥

互斥

这里再次强调下:

这里的排他 / 共享锁指的都是表锁!!!意向锁不会与行级的共享 / 排他锁互斥!!!

意向锁不会与行级的共享 / 排他锁互斥!!!

现在再回过头来看刚才的例子:

事务A获取了某一行的排他锁,并未提交:

SELECT * FROM users WHERE id = 6 FOR UPDATE;

此时,事务A也获取了users表的意向排他锁,

事务B想要获取users表的表锁:

LOCK TABLES users READ;

发现此表存在意向排他锁,于是事务B被阻塞,直到意向排他锁被释放。

参考文档

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
[root@QAQ ~]# sudo tail -n 50 /var/log/mysql/error.log 2023-07-14T02:45:21.370949Z 0 [Note] Shutting down plugin 'partition' 2023-07-14T02:45:21.370952Z 0 [Note] Shutting down plugin 'BLACKHOLE' 2023-07-14T02:45:21.370954Z 0 [Note] Shutting down plugin 'ARCHIVE' 2023-07-14T02:45:21.370956Z 0 [Note] Shutting down plugin 'PERFORMANCE_SCHEMA' 2023-07-14T02:45:21.370993Z 0 [Note] Shutting down plugin 'MRG_MYISAM' 2023-07-14T02:45:21.370995Z 0 [Note] Shutting down plugin 'MyISAM' 2023-07-14T02:45:21.371003Z 0 [Note] Shutting down plugin 'INNODB_SYS_VIRTUAL' 2023-07-14T02:45:21.371011Z 0 [Note] Shutting down plugin 'INNODB_SYS_DATAFILES' 2023-07-14T02:45:21.371013Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESPACES' 2023-07-14T02:45:21.371015Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN_COLS' 2023-07-14T02:45:21.371017Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN' 2023-07-14T02:45:21.371018Z 0 [Note] Shutting down plugin 'INNODB_SYS_FIELDS' 2023-07-14T02:45:21.371020Z 0 [Note] Shutting down plugin 'INNODB_SYS_COLUMNS' 2023-07-14T02:45:21.371022Z 0 [Note] Shutting down plugin 'INNODB_SYS_INDEXES' 2023-07-14T02:45:21.371024Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESTATS' 2023-07-14T02:45:21.371026Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLES' 2023-07-14T02:45:21.371028Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_TABLE' 2023-07-14T02:45:21.371030Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_CACHE' 2023-07-14T02:45:21.371032Z 0 [Note] Shutting down plugin 'INNODB_FT_CONFIG' 2023-07-14T02:45:21.371033Z 0 [Note] Shutting down plugin 'INNODB_FT_BEING_DELETED' 2023-07-14T02:45:21.371035Z 0 [Note] Shutting down plugin 'INNODB_FT_DELETED' 2023-07-14T02:45:21.371037Z 0 [Note] Shutting down plugin 'INNODB_FT_DEFAULT_STOPWORD' 2023-07-14T02:45:21.371039Z 0 [Note] Shutting down plugin 'INNODB_METRICS' 2023-07-14T02:45:21.371041Z 0 [Note] Shutting down plugin 'INNODB_TEMP_TABLE_INFO' 2023-07-14T02:45:21.371043Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_POOL_STATS' 2023-07-14T02:45:21.371045Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE_LRU' 2023-07-14T02:45:21.371047Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE' 2023-07-14T02:45:21.371049Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX_RESET' 2023-07-14T02:45:21.371050Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX' 2023-07-14T02:45:21.371052Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM_RESET' 2023-07-14T02:45:21.371054Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM' 2023-07-14T02:45:21.371056Z 0 [Note] Shutting down plugin 'INNODB_CMP_RESET' 2023-07-14T02:45:21.371058Z 0 [Note] Shutting down plugin 'INNODB_CMP' 2023-07-14T02:45:21.371060Z 0 [Note] Shutting down plugin 'INNODB_LOCK_WAITS' 2023-07-14T02:45:21.371062Z 0 [Note] Shutting down plugin 'INNODB_LOCKS' 2023-07-14T02:45:21.371064Z 0 [Note] Shutting down plugin 'INNODB_TRX' 2023-07-14T02:45:21.371066Z 0 [Note] Shutting down plugin 'InnoDB' 2023-07-14T02:45:21.371100Z 0 [Note] InnoDB: FTS optimize thread exiting. 2023-07-14T02:45:21.371135Z 0 [Note] InnoDB: Starting shutdown... 2023-07-14T02:45:21.471280Z 0 [Note] InnoDB: Dumping buffer pool(s) to /www/server/data/ib_buffer_pool 2023-07-14T02:45:21.471421Z 0 [Note] InnoDB: Buffer pool(s) dump completed at 230714 10:45:21 2023-07-14T02:45:22.992635Z 0 [Note] InnoDB: Shutdown completed; log sequence number 2767468 2023-07-14T02:45:22.993964Z 0 [Note] InnoDB: Removed temporary tablespace data file: "ibtmp1" 2023-07-14T02:45:22.993980Z 0 [Note] Shutting down plugin 'MEMORY' 2023-07-14T02:45:22.993985Z 0 [Note] Shutting down plugin 'CSV' 2023-07-14T02:45:22.993989Z 0 [Note] Shutting down plugin 'sha256_password' 2023-07-14T02:45:22.993991Z 0 [Note] Shutting down plugin 'mysql_native_password' 2023-07-14T02:45:22.994103Z 0 [Note] Shutting down plugin 'binlog' 2023-07-14T02:45:22.994915Z 0 [Note] /www/server/mysql/bin/mysqld: Shutdown complete
07-20

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值