
MySQL 面试之事务和锁篇
MySQL 面试之事务和锁篇
MySQL 事务
扩展阅读:
【基础】什么是事务,什么是 ACID?
要点
事务指的是满足 ACID 特性的一组操作。事务内的 SQL 语句,要么全执行成功,要么全执行失败。可以通过 Commit
提交一个事务,也可以使用 Rollback
进行回滚。通俗来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。

ACID 是数据库事务正确执行的四个基本要素。
- 原子性(Atomicity)
- 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。
- 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
- 一致性(Consistency)
- 数据库在事务执行前后都保持一致性状态。
- 在一致性状态下,所有事务对一个数据的读取结果都是相同的。
- 隔离性(Isolation)
- 一个事务所做的修改在最终提交以前,对其它事务是不可见的。
- 持久性(Durability)
- 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
- 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。
一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。
【中级】事务存在哪些并发一致性问题?
要点
事务中存在的并发一致性问题有:
- 丢失修改
- 脏读
- 不可重复读
- 幻读
“丢失修改”是指一个事务的更新操作被另外一个事务的更新操作替换。
如下图所示,T1 和 T2 两个事务对同一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

“脏读(dirty read)”是指当前事务可以读取其他事务未提交的数据。
如下图所示,T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。

“不可重复读(non-repeatable read)”是指一个事务内多次读取同一数据,过程中,该数据被其他事务所修改,导致当前事务多次读取的数据可能不一致。
如下图所示,T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

“幻读(phantom read)”是指一个事务内多次读取同一范围的数据,过程中,其他事务在该数据范围新增了数据,导致当前事务未发现新增数据。
事务 T1 读取某个范围内的记录时,事务 T2 在该范围内插入了新的记录,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。

【中级】长事务可能会导致哪些问题?
要点
长事务可能会导致以下问题:
- 锁竞争与资源阻塞
- 长事务长时间持有锁,导致其他事务阻塞,增加系统等待时间,降低并发性能。
- 业务线程因数据库请求等待而阻塞,可能引发连锁反应(如服务雪崩),造成严重线上事故。
- 死锁风险增加 - 多个长事务互相等待对方释放锁,容易形成死锁,导致系统无法正常执行。
- 主从延迟问题 - 主库执行时间长,从库同步及重放耗时增加,导致主从数据长时间不一致。
- 回滚效率低下 - 长事务执行中途失败时,回滚操作会浪费已执行的资源与时间,影响系统效率。
【高级】事务的二阶段提交是什么?
要点
事务的二阶段提交确保 redo log(重做日志) 和 binlog(二进制日志) 的一致性,防止崩溃恢复时出现数据丢失或不一致。
两阶段流程
- Prepare 阶段(准备阶段) - InnoDB 写入 redo log,并标记为 prepare 状态(事务预提交,但未最终提交)。
- Commit 阶段(提交阶段) - MySQL Server 写入 binlog(记录 DML 操作)。binlog 写入成功后,InnoDB 将 redo log 状态改为 commit,完成事务提交。
细节
binlog 和 redo log 的区别
特性 | redo log | binlog |
---|---|---|
所属层级 | InnoDB 引擎层 | MySQL Server 层 |
日志类型 | 记录数据页的物理日志 | 记录 DML/DDL 操作的逻辑日志 |
存储方式 | 固定大小,环形写入 | 追加写入,可无限增长 |
主要用途 | 崩溃恢复(保证数据持久性) | 主从复制、数据恢复、备份 |
为什么需要二阶段提交?
无论是单独先写 redo log 或先写 binlog,都可能导致数据不一致:
- 先写 redo log,后写 binlog(宕机时 binlog 未写入) - redo log 恢复数据,但 binlog 缺失该事务 → 主从数据不一致。
- 先写 binlog,后写 redo log(宕机时 redo log 未写入) - binlog 有记录,但 redo log 未提交 → 数据库实际数据丢失,与 binlog 不一致。
为了解决以上问题,所以需要事务二阶段提交(repare
→ commit
),以确保写入两日志的原子性。
二阶段提交如何保证一致性?
MySQL 崩溃恢复时,检查两日志状态:
- redo log prepare,binlog 未写入 - 直接回滚(两日志均无有效记录)。
- redo log prepare,binlog 已写入 - 对比两日志数据:
- 一致:提交事务(redo log commit)。
- 不一致:回滚事务(保证数据一致)。
【中级】有哪些事务隔离级别,分别解决了什么问题?
要点
为了解决以上提到的并发一致性问题,SQL 标准提出了四种“事务隔离级别”来应对这些问题。事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。
事务隔离级别从低到高分别是:
- “读未提交(read uncommitted)” - 是指,事务中的修改,即使没有提交,对其它事务也是可见的。
- **“读已提交(read committed)” ** - 是指,事务提交后,其他事务才能看到它的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
- 读已提交解决了脏读的问题。
- 读已提交是大多数数据库的默认事务隔离级别,如 Oracle。
- “可重复读(repeatable read)” - 是指:保证在同一个事务中多次读取同样数据的结果是一样的。
- 可重复读解决了不可重复读问题。
- 可重复读是 InnoDB 存储引擎的默认事务隔离级别。
- “串行化(serializable )” - 是指,强制事务串行执行,对于同一行记录,加读写锁,一旦出现锁冲突,必须等前面的事务释放锁。
- 串行化解决了幻读问题。由于强制事务串行执行,自然避免了所有的并发问题。
- 串行化策略会在读取的每一行数据上都加锁,这可能导致大量的超时和锁竞争。这对于高并发应用基本上是不可接受的,所以一般不会采用这个级别。
事务隔离级别对并发一致性问题的解决情况:
隔离级别 | 丢失修改 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | ✔️️️ | ❌ | ❌ | ❌ |
读已提交 | ✔️️️ | ✔️️️ | ❌ | ❌ |
可重复读 | ✔️️️ | ✔️️️ | ✔️️️ | ❌ |
可串行化 | ✔️️️ | ✔️️️ | ✔️️️ | ✔️️️ |
【中级】MySQL 的默认事务隔离级别是什么?为什么?
要点
事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。
MySQL 中的事务功能是在存储引擎层实现的,并非所有存储引擎都支持事务功能。比如 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
大部分数据库的默认隔离级别是“读已提交”。然而,InnoDB 的默认隔离级别是“可重复读”。这是为了兼容早期 binlog 的 statement 格式问题。如果使用可重复读以下的隔离级别,使用了 statement 格式的 binlog 会产生主从数据不一致的问题。
此外,在 InnoDB 中,可重复读隔离级别虽然不能解决幻读,但是可以很大程度的避免幻读的发生。根据不同的查询方式,分别提出了避免幻读的方案:
- 针对快照读(普通
select
语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 - 针对当前读(
select ... for update
等语句),通过 Next-Key Lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 Next-Key Lock,如果有其他事务在 Next-Key Lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
很大程度的避免幻读,不代表完全解决幻读问题,下面是两个示例:
- 对于快照读,MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
- 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
【高级】MySQL 是如何实现事务的?
要点
MySQL 主要是通过 锁、Redo Log 、Undo Log、MVCC 来实现事务。
- MySQL 利用**锁(行锁、间隙锁等等)**机制,控制数据的并发修改,满足事务的隔离性。
- Redo Log(重做日志),它会记录事务对数据库的所有修改,当 MySQL 发生宕机或崩溃时,通过重放 redo log 就可以恢复数据,用来满足事务的持久性。
- Undo Log(回滚日志),它会记录事务的反向操作,简单地说就是保存数据的历史版本,用于事务的回滚,使得事务执行失败之后可以恢复之前的样子。实现原子性和隔离性
- MVCC(多版本并发控制),满足了非锁定读的需求,提高了并发度,实现了读已提交和可重复读两种隔离级别,实现了事务的隔离性。
事务实现了原子性、隔离性和持久性特性后,本身就达到了一致性的目的。
【高级】各事务隔离级别是如何实现的?
要点
四种隔离级别具体是如何实现的呢?
以 InnoDB 的事务实现来说明:
- 对于“读未提交”隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于“串行化”隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于“读提交”和“可重复读”隔离级别的事务来说,它们都是通过 ReadView 来实现的,区别仅在于创建 ReadView 的时机不同。ReadView 可以理解为一个数据快照。
- “读提交”隔离级别是在“每个语句执行前”都会重新生成一个 ReadView
- “可重复读”隔离级别是在“启动事务时”生成一个 ReadView,然后整个事务期间都在用这个 ReadView。
【中级】什么是 MVCC?
要点
MVCC 是 Multi Version Concurrency Control 的缩写,即“多版本并发控制”。MVCC 的设计目标是提高数据库的并发性,采用非阻塞的方式去处理读/写并发冲突,可以将其看成一种乐观锁。
不仅是 MySQL,包括 Oracle、PostgreSQL 等其他关系型数据库都实现了各自的 MVCC,实现机制没有统一标准。MVCC 是 InnoDB 存储引擎实现事务隔离级别的一种具体方式。其主要用于实现读已提交和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
【高级】MVCC 的实现原理是什么?
要点
MVCC 的实现原理,主要基于隐式字段、UndoLog、ReadView 来实现。
隐式字段
InnoDB 存储引擎中,数据表的每行记录,除了用户显示定义的字段以外,还有几个数据库隐式定义的字段:
row_id
- 隐藏的自增 ID。如果数据表没有指定主键,InnoDB 会自动基于row_id
产生一个聚簇索引。trx_id
- 最近修改的事务 ID。事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;roll_pointer
- 回滚指针,指向这条记录的上一个版本。
UndoLog
MVCC 的多版本指的是多个版本的快照,快照存储在 UndoLog 中。该日志通过回滚指针 roll_pointer
把一个数据行的所有快照链接起来,构成一个版本链。

ReadView
ReadView 就是事务进行快照读时产生的读视图(快照)。
ReadView 有四个重要的字段:
m_ids
- 指的是在创建 ReadView 时,当前数据库中“活跃事务”的事务 ID 列表。注意:这是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。min_trx_id
- 指的是在创建 ReadView 时,当前数据库中“活跃事务”中事务 id 最小的事务,也就是m_ids
的最小值。max_trx_id
- 这个并不是 m_ids 的最大值,而是指创建 ReadView 时当前数据库中应该给下一个事务分配的 ID 值,也就是全局事务中最大的事务 ID 值 + 1;creator_trx_id
- 指的是创建该 ReadView 的事务的事务 ID。
在创建 ReadView 后,我们可以将记录中的 trx_id 划分为三种情况:
- 已提交事务
- 已启动但未提交的事务
- 未启动的事务

ReadView 如何判断版本链中哪个版本可见?
一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:
trx_id == creator_trx_id
- 表示trx_id
版本记录由 ReadView 所代表的当前事务产生,当然可以访问。trx_id < min_trx_id
- 表示trx_id
版本记录是在创建 ReadView 之前已提交的事务生成的,当前事务可以访问。trx_id >= max_trx_id
- 表示trx_id
版本记录是在创建 ReadView 之后才启动的事务生成的,当前事务不可以访问。min_trx_id <= trx_id < max_trx_id
- 需要判断trx_id
是否在m_ids
列表中- 如果
trx_id
在m_ids
列表中,表示生成trx_id
版本记录的事务依然活跃(未提交事务),当前事务不可以访问。 - 如果
trx_id
不在m_ids
列表中,表示生成trx_id
版本记录的事务已提交,当前事务可以访问。
- 如果
这种通过“版本链”来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
【高级】MVCC 实现了哪些隔离级别,如何实现的?
要点
对于“读已提交”和“可重复读”隔离级别的事务来说,它们都是通过 MVCC 的 ReadView 机制来实现的,区别仅在于创建 ReadView 的时机不同。ReadView 可以理解为一个数据快照。
MVCC 如何实现可重复读隔离级别
可重复读隔离级别只有在启动事务时才会创建 ReadView,然后整个事务期间都使用这个 ReadView。这样就保证了在事务期间读到的数据都是事务启动前的记录。
举个例子,假设有两个事务依次执行以下操作:
- 初始,表中 id = 1 的 value 列值为 100。
- 事务 2 读取数据,value 为 100;
- 事务 1 将 value 设为 200;
- 事务 2 读取数据,value 为 100;
- 事务 1 提交事务;
- 事务 2 读取数据,value 依旧为 100;
以上操作,如下图所示。T2 事务在事务过程中,是否可以看到 T1 事务的修改,可以根据 ReadView 中描述的规则去判断。

从图中不难看出:
- 对于
trx_id = 100
的版本记录,比对 T2 事务 ReadView ,trx_id < min_trx_id
,因此在 T2 事务中的任意时刻都可见; - 对于
trx_id = 101
的版本记录,比对 T2 事务 ReadView ,可以看出min_trx_id <= trx_id < max_trx_id
,且trx_id
在m_ids
中,因此 T2 事务中不可见。
综上所述,在 T2 事务中,自始至终只能看到 trx_id = 100
的版本记录。
MVCC 如何实现读已提交隔离级别
读已提交隔离级别每次读取数据时都会创建一个 ReadView。这意味着,事务期间的多次读取同一条数据,前后读取的数据可能会出现不一致——因为,这期间可能有另外一个事务修改了该记录,并提交了事务。
举个例子,假设有两个事务依次执行以下操作:
- 初始,表中 id = 1 的 value 列值为 100。
- 事务 2 读取数据(创建 ReadView),value 为 0;
- 事务 1 将 value 设为 100;
- 事务 2 读取数据(创建 ReadView),value 为 0;
- 事务 1 提交事务;
- 事务 2 读取数据(创建 ReadView),value 为 100;
以上操作,如下图所示,T2 事务在事务过程中,是否可以看到其他事务的修改,可以根据 ReadView 中描述的规则去判断。

从图中不难看出:
- 对于
trx_id = 100
的版本记录,比对 T2 事务 ReadView ,trx_id < min_trx_id
,因此在 T2 事务中的任意时刻都可见; - 对于
trx_id = 101
的版本记录,比对 T2 事务 ReadView ,可以看出第二次查询时(T1 更新未提交),min_trx_id <= trx_id < max_trx_id
,且trx_id
在m_ids
中,因此 T2 事务中不可见;而第三次查询时(T1 更新已提交),trx_id < min_trx_id
,因此在 T2 事务中可见;
综上所述,在 T2 事务中,当 T1 事务提交前,可读取到的是 trx_id = 100
的版本记录;当 T1 事务提交后,可读取到的是 trx_id = 101
的版本记录。
MVCC + Next-Key Lock 如何解决幻读
MySQL InnoDB 引擎的默认隔离级别虽然是“可重复读”,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
- 针对快照读(普通
SELECT
语句),通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 - 针对当前读(
SELECT ... FOR UPDATE
等语句),通过 Next-Key Lock(记录锁+间隙锁)方式解决了幻读,因为当执行SELECT ... FOR UPDATE
语句的时候,会加上 Next-Key Lock,如果有其他事务在 Next-Key Lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好的避免了幻读问题。
MySQL 锁
【中级】MySQL 中有哪些锁?
要点
为了解决并发一致性问题,MySQL 支持了很多种锁来实现不同程度的隔离性,以保证数据的安全性。
- 独享锁和共享锁
- 悲观锁和乐观锁
- 全局锁 - 锁定整个数据库。典型应用是全库逻辑备份。
- 表级锁
- 表锁 - 表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。
- 元数据锁(Metadata Lock, MDL) - 用于保护表结构变更,MDL 不需要显式使用,在访问一个表的时候会被自动加上。
- 增删改查,加读锁
- 结构变更,加写锁
- 意向锁(Intention Lock) - 表明事务打算在表中的行上获取什么类型的锁。
- 意向共享锁 (IS)
- 意向排他锁 (IX)
- 自增锁(Auto Increment Lock)
- 行级锁
- 记录锁(Record Lock) - 锁定索引中的单条记录。
- 间隙锁(Gap Lock) - 锁定索引记录之间的间隙。
- 临键锁(Next-Key Lock) - 记录锁+间隙锁的组合。
- 插入意向锁(Insert Intention Lock) - INSERT 操作设置的间隙锁。
【中级】独享锁 vs. 共享锁?
要点
InnoDB 实现标准行级锁定,根据是否独享资源,可以把锁分为两类:
- 独享锁(Exclusive),简写为 X 锁,又称为“写锁”、“排它锁”。
- 独享锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。
- 使用方式:
SELECT ... FOR UPDATE;
- 共享锁(Shared),简写为 S 锁,又称为“读锁”。
- 共享锁锁定的资源可以被其他用户读取,但不能修改。在进行
SELECT
的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。 - 使用方式:
SELECT ... LOCK IN SHARE MODE;
- 共享锁锁定的资源可以被其他用户读取,但不能修改。在进行
为什么要引入读写锁机制?
实际上,读写锁是一种通用的锁机制,并非 MySQL 的专利。在很多软件领域,都存在读写锁机制。
因为读操作本身是线程安全的,而一般业务往往又是读多写少的情况。因此,如果对读操作进行互斥,是不必要的,并且会大大降低并发访问效率。正式为了应对这种问题,产生了读写锁机制。
读写锁的特点是:读读不互斥、读写互斥、写写互斥。简言之:只要存在写锁,其他事务就不能做任何操作。
注:InnoDB 下的行锁、间隙锁、next-key 锁统统属于独享锁。
【中级】悲观锁 vs. 乐观锁?
要点
基于加锁方式分类,MySQL 可以分为悲观锁和乐观锁。
- 悲观锁 - 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
- 在查询完数据的时候就把事务锁起来,直到提交事务(
COMMIT
) - 实现方式:使用数据库中的锁机制。
- 在查询完数据的时候就把事务锁起来,直到提交事务(
- 乐观锁 - 假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。只在更新的时候,判断一下在此期间是否有其他线程更新该数据。
- 实现方式:更新数据时,先使用版本号机制或 CAS 算法检查数据是否被修改。
为什么要引入乐观锁?
乐观锁也是一种通用的锁机制,在很多软件领域,都存在乐观锁机制。
锁,意味着互斥,意味着阻塞。在高并发场景下,锁越多,阻塞越多,势必会拉低并发性能。那么,为了提高并发度,能不能尽量不加锁呢?
乐观锁,顾名思义,就是假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。虽然不加锁,但不意味着什么都不做,而是在更新的时候,判断一下在此期间是否有其他线程更新该数据。乐观锁最常见的实现方式,是使用版本号机制或 CAS 算法(Compare And Swap)去实现。
【示例】MySQL 乐观锁示例
假设,order 表中有一个字段 status,表示订单状态:status 为 1 代表订单未支付;status 为 2 代表订单已支付。现在,要将 id 为 1 的订单状态置为已支付,则操作如下:
select status, version from order where id=#{id}
update order
set status=2, version=version+1
where id=#{id} and version=#{version};
乐观锁的优点是:减少锁竞争,提高并发度。
乐观锁的缺点是:
- 存在 ABA 问题。所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了
- 如果乐观锁所检查的数据存在大量锁竞争,会由于不断循环重试,产生大量的 CPU 开销。
【中级】全局锁 vs. 表级锁 vs. 行级锁?
要点
前文提到了,锁,意味着互斥,意味着阻塞。在高并发场景下,锁越多,阻塞越多,势必会拉低并发性能。在不得不加锁的情况下,显然,加锁的范围越小,锁竞争的发生频率就越小,系统的并发程度就越高。但是,加锁也需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销,锁粒度越小,系统的锁操作开销就越大。因此,在选择锁粒度时,也需要在锁开销和并发程度之间做一个权衡。
根据加锁的范围,MySQL 的锁大致可以分为:
- 全局锁 - “全局锁”会锁定整个数据库。
- 表级锁(table lock) - “表级锁”锁定整张表。用户对表进行写操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获得读锁,读锁之间不会相互阻塞。表级锁有:
- 表锁 - 表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。
- 元数据锁(MDL) - MDL 不需要显式使用,在访问一个表的时候会被自动加上。
- 增删改查,加读锁
- 结构变更,加写锁
- 意向锁(Intention Lock)
- 自增锁(AUTO-INC)
- 行级锁(row lock) - “行级锁”锁定指定的行记录。这样其它线程还是可以对同一个表中的其它行记录进行操作。行级锁有:
- 记录锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock)
- 插入意向锁
以上各种加锁粒度,在不同存储引擎中的支持情况并不相同。如:InnoDB 支持全局锁、表级锁、行级锁;而 MyISAM 只支持全局锁、表级锁。
每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如 InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
【中级】死锁是如何产生的?
要点
“死锁”是指两个或多个事务竞争同一资源,并请求锁定对方占用的资源,从而导致恶性循环的现象。
产生死锁的场景:
- 多个事务以不同的顺序锁定资源,可能会产生死锁。
- 多个事务同时锁定同一个资源时,也会产生死锁。
【高级】如何避免死锁?
要点
死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。由此可知,要想避免死锁,就要从这几个必要条件上去着手:
- 更新表时,尽量使用主键更新,减少冲突;
- 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
- 设置合理的锁等待超时参数,我们可以通过
innodb_lock_wait_timeout
设置合理的等待超时阈值,特别是在一些高并发的业务中,我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。 - 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
- 在允许幻读和不可重复读的情况下,尽量使用读已提交事务隔离级别,可以避免 Gap Lock 导致的死锁问题;
- 还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。
【高级】如何解决死锁?
要点
当出现死锁以后,有两种策略:
- 设置事务等待锁的超时时间。这个超时时间可以通过参数
innodb_lock_wait_timeout
来设置。 - 开启死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数
innodb_deadlock_detect
设置为on
,表示开启这个逻辑。
在 InnoDB 中,innodb_lock_wait_timeout
的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,若直接把这个时间设置成一个很小的值,比如 1s,也是不可取的。当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect
的默认值本身就是 on。为了解决死锁问题,不同数据库实现了各自的死锁检测和超时机制。InnoDB 的处理策略是:将持有最少行级排它锁的事务进行回滚。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的:每当事务被锁时,就要查看它所依赖的线程是否被其他事务锁住,如此循环,来判断是否出现了循环等待,也就是死锁。因此,死锁检测可能会耗费大量的 CPU。