23Mysql事务和锁

3 MySQL锁

事务的 隔离性 由这章讲述的 来实现。

3.1 锁概述

是计算机协调多个进程或者线程并发访问某一个资源的机制。我们就需要保证这个数据在任何时刻最多只有一个线程在访问,保证数据的完整性和一致性。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要。

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

讲一下mysql里有哪些锁?

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

img

  • 全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
  • 表级锁:MySQL 里面表级别的锁有这几种:
    • 表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
    • 元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
    • 意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁
  • 行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
  • 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
  • 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
  • Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

3.2 并发事务带来的问题

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 也叫丢失更新。指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入或者删除了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:

不可重复读的重点是修改,针对的数据是多行。幻读的重点在于新增或者删除,针对数据是多行。

3.3 并发事务的解决方案

解决方案:对事务进行隔离

MySQL的四种隔离级别如下:

  • READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。可以看到,在终端 B 事务执行新增操作时,会发生阻塞,锁超时后会抛出 1205 - Lock wait timeout exceeded; try restarting transaction 错误,避免了幻读。可以通过 select * from performance_schema.data_locks; 查看事务的锁信息,从 supremum pseudo-record 获知,通过添加锁解决幻读问题。
1
SELECT @@global.transaction_isolation ;

3.4 并发事务访问情况说明

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

3.4.1 读-读情况

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

3.4.2 写-写情况

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

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

在锁结构中存在很多的信息,为了简化理解,只把两个比较重要的属性拿出来:

1、trx信息:代表这个锁结构是哪一个事务生成的

2、is_waiting: 代表当前事务是否在线等待

当事务T1改动了这条记录后,就生成了一个锁结构与该条记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功。然后就可以继续进行操作了。

image-20251116111043986

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

当事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看有没有别的事务在等待获取锁,发现了事务T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让他继续执行,此时事务T2就算获取到了锁。

小结几种说法:

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

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

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

3.4.3 读-写情况

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

要想解决这些问题就需要使用到到事务的隔离级别,而事务的隔离性的实现原理有两种:

1、使用MVCC:读操作利用多版本并发控制( MVCC ),写操作进行加锁 。

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

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

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

2、读、写操作都采用 加锁 的方式。

小结对比发现:

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

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

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

3.5 锁的分类

从对数据操作的粒度分 :

1) 表锁(手动加):操作时,会锁定整个表。

2) 行锁(Innodb默认):操作时,会锁定当前操作行。

3) 意向锁(自动加):解决表锁和行锁之间的兼容性冲突,提高并发效率

从对数据操作的类型分:

1) 读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。

2) 写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁。

相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。下表中罗列出了各存储引擎对锁的支持情况:

存储引擎 表级锁 行级锁 页面锁
MyISAM 支持 不支持 不支持
InnoDB 支持 支持 不支持

MySQL这3种锁的特性可大致归纳如下 :

锁类型 特点
表级锁 偏向MyISAM 存储引擎,开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁 偏向InnoDB 存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
意向锁 意向锁是 表级锁,但不会真的阻塞其他行操作 当事务要加行锁时,InnoDB 会先在表上加意向锁,告诉系统我要对某些行加锁了 这样,当其他事务想申请表锁时,就能快速判断表里是否已经有人持有行锁,而不需要一行一行检查。
页面锁 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用。最后意向锁解决表锁和行锁之间的兼容性冲突,提高并发效率

3.6 InnoDB行锁

3.6.1 加锁特点

InnoDB 实现了以下两种类型的行锁。

1、共享锁(S):又称为读锁,简称S锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

select for share

2、排他锁(X):又称为写锁,简称X锁,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据进行读取和修改 select…for update。

对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及到的数据集加排他锁(X);

对于普通SELECT语句,InnoDB不会加任何锁;

3.6.2 案例准备工作3.6.3 行锁基本演示

1
set autocommit=0;

3.6.4 无索引行锁升级为表锁

如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟表锁一样。

3.6.6 意向锁

意向锁是表级锁,共有两种:

  • 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
  • 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。

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

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

IS 锁 IX 锁
IS 锁 兼容 兼容
IX 锁 兼容 兼容

意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。

IS 锁 IX 锁
S 锁 兼容 互斥
X 锁 互斥 互斥

《MySQL 技术内幕 InnoDB 存储引擎》这本书对应的描述应该是笔误了。

img


著作权归JavaGuide(javaguide.cn)所有 基于MIT协议 原文链接:https://javaguide.cn/database/mysql/mysql-questions-01.html

总结

行锁自动带意向锁:任何行锁操作都会在表上自动生成对应的意向锁(IX / IS)。

意向锁的作用:加速判断“能否加表级锁”。

  • 如果表上有 IX,那么后续再申请表级 X 锁,就会直接被阻塞。
  • 这样 MySQL 不用去扫描整张表的每一行,看是不是有人加了行锁。

演示效果

  • Session 1、2 在不同的行上 FOR UPDATE 可以并发成功(因为意向锁不互斥)。
  • 但只要有人持有行锁(带 IX),别人就无法直接加表级写锁。

3.6.6 间隙锁危害

当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据进行加锁; 对于键值在条件范围内但并不存在的记录,叫做 “间隙(GAP)” , InnoDB也会对这个 “间隙” 加锁,这种锁机制就是所谓的 间隙锁(Next-Key锁) 。

[自增锁有了解吗?]

不太重要的一个知识点,简单了解即可。

关系型数据库设计表的时候,通常会有一列作为自增主键。InnoDB 中的自增主键会涉及一种比较特殊的表级锁— 自增锁(AUTO-INC Locks)

1
2
3
4
5
6
CREATE TABLE `sequence_id` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
`stub` CHAR(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

更准确点来说,不仅仅是自增主键,AUTO_INCREMENT的列都会涉及到自增锁,毕竟非主键也可以设置自增长。

如果一个事务正在插入数据到有自增列的表时,会先获取自增锁,拿不到就可能会被阻塞住。这里的阻塞行为只是自增锁行为的其中一种,可以理解为自增锁就是一个接口,其具体的实现有多种。具体的配置项为 innodb_autoinc_lock_mode (MySQL 5.1.22 引入),可以选择的值如下:

innodb_autoinc_lock_mode 介绍
0 传统模式
1 连续模式(MySQL 8.0 之前默认)
2 交错模式(MySQL 8.0 之后默认)

交错模式下,所有的“INSERT-LIKE”语句(所有的插入语句,包括:INSERTREPLACEINSERT…SELECTREPLACE…SELECTLOAD DATA等)都不使用表级锁,使用的是轻量级互斥锁实现,多条插入语句可以并发执行,速度更快,扩展性也更好。

不过,如果你的 MySQL 数据库有主从同步需求并且 Binlog 存储格式为 Statement 的话,不要将 InnoDB 自增锁模式设置为交叉模式,不然会有数据不一致性问题。这是因为并发情况下插入语句的执行顺序就无法得到保障。

如果 MySQL 采用的格式为 Statement ,那么 MySQL 的主从同步实际上同步的就是一条一条的 SQL 语句。

3.6.7 总结

InnoDB 存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面带来了一定的性能损耗,可能比表锁开销更高,但整体并发处理能力远高于 MyISAM 的表锁。当系统并发量较高时,InnoDB 的整体性能表现通常会更好。

不过,InnoDB 的行级锁也有脆弱的一面,如果使用不当,性能不仅不会优于 MyISAM,甚至可能更差。

另外需要注意的是,InnoDB 在行锁和表锁之间引入了 意向锁。意向锁是一种表级锁,用来标识事务即将在表中某些行上加行锁,从而避免在加行锁时与其他事务的表级锁产生冲突。它并不会真正阻塞具体的行访问,但能显著提升锁的检测效率。

优化建议:

  1. 尽可能让所有数据检索都能通过索引来完成,避免无索引导致行锁退化为表锁。
  2. 合理设计索引,尽量缩小锁的范围。
  3. 尽可能减少索引条件范围,避免过多间隙锁的产生。
  4. 尽量控制事务大小,减少锁定资源数量和时间长度。
  5. 在业务允许的前提下,使用较低级别的事务隔离,减少锁冲突。
  6. 合理利用 意向锁 的机制,理解它只是用于快速判定是否存在行锁,避免在表级与行级锁之间产生不必要的冲突。

4 MySQL中的MVCC

4.1 MVCC概述

全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的读写并发性能。

同一行数据平时发生读写请求时,会上锁阻塞住。但mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

4.2 MVCC三要素

4.2.1 隐藏字段

image-20251116104621382

回滚日志,在insert update delete的时候产生的便于数据回滚的日志

当insert的时候,产生的undolog日志只在回滚时需要,一旦事务提交,会被立即删除

但update和delete的时候,产生的undolog日志不仅在回滚的时候需要,mvcc版本访问也需要,不会立即被删除

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表的尾部是最早的旧记录

4.2.3 ReadView读视图

读视图是快照读SQL执行时MVCC提取数据的依据, 记录并维护系统当前活跃事务(未提交的事务)id

4.2.3.1 MVCC读

方式 概念 实现方式
当前读 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对当前记录进行加锁,是阻塞的 select…for update
快照读 我们平常写的select都是快照读,读取的是数据的可见版本,有可能是历史数据,不加锁,是非阻塞的 RC隔离级别: 每次select 都生成一个快照读 RR隔离级别: 开启事务后,同一条select语句多次查询共用RW

4.2.3.2 RW组成

字段 含义
m_ids 当前活跃事务的ID集合 案例中的3,4,5 事务2已提交 不属于活跃事务
min_trx_id 最小活跃事务事务ID 案例中事务3为最小活跃事务id
max_trx_id 预分配事务ID,当前最大事务ID+1(ID是自增的) 案例中事务5+1 也就是事务6
creator_trx_id RW创建者的事务ID 案例中的事务5

RR下的幻读问题

(10 条消息) MySQL 可重复读隔离级别,彻底解决幻读了吗? - 知乎

补充

Mysql 设置了可重读隔离级后,怎么保证不发生幻读?

尽量在开启事务之后,马上执行 select … for update 这类锁定读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录,就避免了幻读的问题。

串行化隔离级别是通过什么实现的?

是通过行级锁来实现的,序列化隔离级别下,普通的 select 查询是会对记录加 S 型的 next-key 锁,其他事务就没没办法对这些已经加锁的记录进行增删改操作了,从而避免了脏读、不可重复读和幻读现象。

一条update是不是原子性的?为什么?

是原子性,主要通过锁+undolog 日志保证原子性的

  • 执行 update 的时候,会加行级别锁,保证了一个事务更新一条记录的时候,不会被其他事务干扰。
  • 事务执行过程中,会生成 undolog,如果事务执行失败,就可以通过 undolog 日志进行回滚。

滥用事务,或者一个事务里有特别多sql的弊端?

事务的资源在事务提交之后才会释放的,比如存储资源、锁。

如果一个事务特别多 sql,那么会带来这些问题:

  • 如果一个事务特别多 sql,锁定的数据太多,容易造成大量的死锁和锁超时。
  • 回滚记录会占用大量存储空间,事务回滚时间长。在[MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值,sql 越多,所需要保存的回滚数据就越多。
  • 执行时间长,容易造成主从延迟,主库上必须等事务执行完成才会写入binlog,再传给备库。所以,如果一个主库上的语句执行10分钟,那这个事务很可能就会导致从库延迟10分钟

MySQL两个线程的update语句同时处理一条数据,会不会有阻塞?

如果是两个事务同时更新了 id = 1,比如 update … where id = 1,那么是会阻塞的。因为 InnoDB 存储引擎实现了行级锁。

当A事务对 id =1 这行记录进行更新时,会对主键 id 为 1 的记录加X类型的记录锁,这样第二事务对 id = 1 进行更新时,发现已经有记录锁了,就会陷入阻塞状态。

#两条update语句处理一张表的不同的主键范围的记录,一个<10,一个>15,会不会遇到阻塞?底层是为什么的?

不会,因为锁住的范围不一样,不会形成冲突。

  • 第一条 update sql 的话( id<10),锁住的范围是(-♾️,10)
  • 第二条 update sql 的话(id >15),锁住的范围是(15,+♾️)

如果2个范围不是主键或索引-还会阻塞吗

如果2个范围查询的字段不是索引的话,那就代表 update 没有用到索引,这时候触发了全表扫描,全部索引都会加行级锁,这时候第二条 update 执行的时候,就会阻塞了。

因为如果 update 没有用到索引,在扫描过程中会对索引加锁,所以全表扫描的场景下,所有记录都会被加锁,相当于锁住了全表。

背书

mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undolog日志,第三个是readView读视图,隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段roll_pointer(回滚指针),指向上一个版本的事务版本记录地址

undolog主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行查找某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表

readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用


23Mysql事务和锁
http://example.com/2025/11/16/23Mysql事务和锁/
作者
無鎏雲
发布于
2025年11月16日
许可协议