MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MySQL锁与事务隔离级别的关系

2022-03-117.9k 阅读

MySQL 锁机制概述

锁的基本概念

在数据库系统中,锁是一种用于控制并发访问的机制。它允许数据库在多用户环境下,确保数据的一致性和完整性。当多个事务同时访问和修改相同的数据时,可能会出现数据不一致的问题,如脏读、不可重复读和幻读等。MySQL 通过锁机制来协调这些并发操作,使得每个事务都能在一个相对隔离的环境中执行。

MySQL 中的锁可以根据不同的维度进行分类。从锁的粒度来看,主要有表级锁、行级锁和页级锁。表级锁是对整个表进行锁定,开销小,加锁快,但并发度低,适用于以读为主,写操作较少的场景;行级锁只锁定涉及到的行数据,开销大,加锁慢,但并发度高,适合写操作频繁的场景;页级锁则介于表级锁和行级锁之间,锁定的是数据页。

常见锁类型

  1. 共享锁(S 锁):又称读锁,多个事务可以同时获取共享锁来读取数据,这样可以提高并发读取的效率。例如,事务 A 和事务 B 都可以对某一行数据获取共享锁进行读取操作。当一个事务获取了共享锁后,其他事务只能获取共享锁进行读取,而不能获取排他锁进行写入操作。
  2. 排他锁(X 锁):也叫写锁,一旦一个事务获取了排他锁,其他事务既不能获取共享锁也不能获取排他锁,直到该事务释放排他锁。这保证了在写操作时,不会有其他事务干扰,确保数据的一致性。比如,事务 C 获取了某一行数据的排他锁进行写入操作,此时其他事务就无法再对该行数据进行读写操作。
  3. 意向锁:意向锁是为了在获取表级锁时,能够快速判断是否可以获取而引入的。意向锁分为意向共享锁(IS)和意向排他锁(IX)。当事务要对某一行数据加共享锁时,会先在表上加意向共享锁;要加排他锁时,会先在表上加意向排他锁。这样当其他事务要获取表级锁时,通过检查意向锁就可以快速判断是否冲突。例如,事务 D 要对表中的某一行数据加排他锁,它会先在表上加意向排他锁,此时如果有其他事务想要获取整个表的共享锁,发现表上有意向排他锁,就知道不能获取,避免了对每一行数据的检查。

MySQL 事务隔离级别

事务的特性

在深入了解事务隔离级别之前,先回顾一下事务的四大特性,即 ACID:

  1. 原子性(Atomicity):事务是一个不可分割的工作单位,要么全部执行成功,要么全部失败回滚。例如,在银行转账操作中,从账户 A 向账户 B 转账 100 元,这个操作要么成功完成,使得 A 账户减少 100 元,B 账户增加 100 元;要么由于某种原因失败,A 和 B 账户的余额都不发生变化。
  2. 一致性(Consistency):事务执行前后,数据库的完整性约束不会被破坏。比如在转账事务中,转账前后,整个系统的总金额应该保持不变。
  3. 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,每个事务都好像在独立的环境中执行一样。不同的隔离级别会影响事务之间的隔离程度。
  4. 持久性(Durability):一旦事务提交,它对数据库所做的修改就会永久保存,即使系统崩溃也不会丢失。

事务隔离级别分类

MySQL 提供了四种事务隔离级别,分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

  1. 读未提交(Read Uncommitted):这是最低的隔离级别,在这个级别下,一个事务可以读取到另一个未提交事务修改的数据。这种隔离级别会导致脏读问题,因为可能读取到的数据最终并不会被提交,是无效的数据。例如,事务 A 对某一行数据进行修改但未提交,事务 B 就可以读取到事务 A 修改后的数据,如果事务 A 最终回滚,那么事务 B 读取到的数据就是脏数据。
  2. 读已提交(Read Committed):在这个级别下,一个事务只能读取到其他已提交事务修改的数据,避免了脏读问题。但是,在一个事务内多次读取同一数据时,如果其他事务在这期间提交了对该数据的修改,那么每次读取的结果可能不同,会出现不可重复读问题。例如,事务 A 先读取了某一行数据,然后事务 B 修改并提交了该行数据,事务 A 再次读取时,就会得到不同的结果。
  3. 可重复读(Repeatable Read):该级别保证在一个事务内多次读取同一数据时,读取到的数据是一致的,避免了不可重复读问题。MySQL 默认的隔离级别就是可重复读。在可重复读级别下,MySQL 通过多版本并发控制(MVCC)来实现,事务在读取数据时,会根据版本号获取到事务开始时的数据版本,即使其他事务在这期间修改并提交了数据,当前事务读取到的数据仍然是事务开始时的版本。不过,可重复读级别并不能完全避免幻读问题,当一个事务在范围内插入新数据时,另一个事务再次读取该范围时,会发现多出了新数据,就好像出现了幻觉一样。
  4. 串行化(Serializable):这是最高的隔离级别,它将所有事务串行化执行,避免了脏读、不可重复读和幻读等所有并发问题。但是,这种隔离级别会极大地降低系统的并发性能,因为所有事务必须依次执行,不能并发执行。

锁与事务隔离级别的关系

读未提交隔离级别与锁

在读未提交隔离级别下,几乎不需要锁来保证数据的读取。因为该级别允许事务读取未提交的数据,所以不存在因为数据被锁定而无法读取的情况。但是,这种情况下数据的一致性无法得到保障,容易出现脏读问题。例如,以下代码演示了读未提交隔离级别下的脏读情况:

-- 创建测试表
CREATE TABLE test_table (id INT PRIMARY KEY, value VARCHAR(50));

-- 开启事务1
START TRANSACTION;
INSERT INTO test_table (id, value) VALUES (1, 'initial_value');

-- 开启事务2
START TRANSACTION;
SELECT value FROM test_table WHERE id = 1; -- 此时事务1未提交,事务2能读到插入的数据,出现脏读
COMMIT;

-- 事务1回滚
ROLLBACK;

在上述代码中,事务 2 在事务 1 未提交时就可以读取到事务 1 插入的数据,当事务 1 回滚后,事务 2 读取到的数据就是无效的,这就是脏读。由于不需要等待数据提交就可以读取,所以锁的使用非常少,并发性能相对较高,但数据的准确性和一致性较差。

读已提交隔离级别与锁

读已提交隔离级别通过锁机制来保证事务只能读取已提交的数据。在读取数据时,MySQL 使用共享锁来防止其他事务在读取期间对数据进行修改。当一个事务读取数据时,会获取共享锁,其他事务如果要对该数据进行修改,必须先获取排他锁,但由于共享锁的存在,排他锁无法获取,直到读取事务释放共享锁。

例如,在以下代码中:

-- 开启事务1
START TRANSACTION;
SELECT value FROM test_table WHERE id = 1 FOR SHARE; -- 获取共享锁
-- 此时其他事务不能对id = 1的数据加排他锁进行修改

-- 开启事务2
START TRANSACTION;
UPDATE test_table SET value = 'new_value' WHERE id = 1; -- 由于事务1持有共享锁,此操作会等待事务1释放锁
COMMIT;

-- 事务1提交
COMMIT;

在事务 1 执行 SELECT... FOR SHARE 语句获取共享锁后,事务 2 的 UPDATE 操作需要获取排他锁,由于共享锁的存在,事务 2 会等待事务 1 释放共享锁。这种机制避免了脏读问题,但在事务内多次读取同一数据时,如果其他事务在这期间提交了对该数据的修改,就会出现不可重复读问题。

可重复读隔离级别与锁

可重复读隔离级别除了使用 MVCC 来实现数据的一致性读取外,也会使用锁机制来处理一些特殊情况。在可重复读级别下,普通的 SELECT 操作使用 MVCC 来读取数据,不需要加锁,这样可以提高并发读取的性能。但是,当进行一些可能会改变数据的操作,如 UPDATEDELETE 等,MySQL 会使用行级排他锁来锁定相关的数据行。

例如,以下代码展示了可重复读隔离级别下的情况:

-- 开启事务1
START TRANSACTION;
SELECT value FROM test_table WHERE id = 1; -- 使用MVCC读取数据,不加锁

-- 开启事务2
START TRANSACTION;
UPDATE test_table SET value = 'new_value' WHERE id = 1; -- 获取行级排他锁
COMMIT;

-- 事务1再次读取
SELECT value FROM test_table WHERE id = 1; -- 由于MVCC,读取到的仍然是事务1开始时的数据版本,避免了不可重复读

-- 事务1提交
COMMIT;

在事务 1 第一次读取时,使用 MVCC 读取数据,不加锁。事务 2 进行 UPDATE 操作时,获取行级排他锁。当事务 1 再次读取时,由于 MVCC 的存在,它读取到的仍然是事务开始时的数据版本,避免了不可重复读问题。然而,在可重复读级别下,虽然 MVCC 可以处理大部分读取一致性问题,但对于一些特殊情况,如范围查询和插入操作,还是可能会出现幻读问题。例如,当事务 1 进行范围查询时,事务 2 在该范围内插入新数据,事务 1 再次进行相同范围查询时,就会发现多出了新数据,这就是幻读。为了避免幻读,MySQL 在可重复读级别下,对于范围查询会使用间隙锁(Gap Lock)和临键锁(Next-Key Lock)。

间隙锁是对两个数据之间的间隙进行锁定,防止其他事务在该间隙插入数据。临键锁则是间隙锁和行锁的组合,既锁定数据行,又锁定数据行前面的间隙。例如,在以下代码中:

-- 开启事务1
START TRANSACTION;
SELECT * FROM test_table WHERE id BETWEEN 1 AND 10 FOR UPDATE; -- 使用临键锁,锁定id在1到10之间的行以及它们之间的间隙

-- 开启事务2
START TRANSACTION;
INSERT INTO test_table (id, value) VALUES (5, 'new_row'); -- 由于事务1的临键锁,此插入操作会等待事务1释放锁
COMMIT;

-- 事务1提交
COMMIT;

在事务 1 执行 SELECT... FOR UPDATE 语句时,会使用临键锁锁定 id 在 1 到 10 之间的行以及它们之间的间隙。事务 2 尝试在这个范围内插入新数据时,由于临键锁的存在,会等待事务 1 释放锁,从而避免了幻读问题。

串行化隔离级别与锁

串行化隔离级别是通过锁机制来强制事务串行执行的。在串行化级别下,所有的事务都以串行的方式执行,就像排队一样,一个事务执行完后,下一个事务才能开始执行。在这种隔离级别下,读操作会获取共享锁,写操作会获取排他锁,并且所有的锁都会一直持有到事务结束。

例如,以下代码展示了串行化隔离级别下的操作:

-- 设置事务隔离级别为串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 开启事务1
START TRANSACTION;
SELECT value FROM test_table WHERE id = 1; -- 获取共享锁,直到事务结束才释放
-- 其他事务不能对id = 1的数据进行写操作

-- 开启事务2
START TRANSACTION;
UPDATE test_table SET value = 'new_value' WHERE id = 1; -- 由于事务1持有共享锁,此操作会等待事务1结束
COMMIT;

-- 事务1提交
COMMIT;

在事务 1 执行 SELECT 操作获取共享锁后,事务 2 的 UPDATE 操作需要获取排他锁,但由于事务 1 持有共享锁,事务 2 会一直等待事务 1 提交并释放锁。这种方式虽然避免了所有的并发问题,包括脏读、不可重复读和幻读,但由于所有事务都是串行执行,并发性能极低,在实际应用中,除非对数据一致性要求极高且并发量较小的场景,否则很少使用串行化隔离级别。

如何根据业务场景选择合适的事务隔离级别和锁策略

高并发读场景

如果业务场景是以读操作为主,并发量较高,对数据一致性要求不是特别严格,可以选择读已提交或可重复读隔离级别。读已提交隔离级别下,读操作使用共享锁,能保证读取到已提交的数据,并发性能相对较高,但可能会出现不可重复读问题。可重复读隔离级别通过 MVCC 避免了不可重复读问题,普通读操作不加锁,进一步提高了并发读取性能,同时对于写操作会使用行级排他锁和间隙锁等保证数据一致性,适用于对数据一致性有一定要求的高并发读场景。

在锁策略上,可以尽量减少锁的持有时间,对于读操作,可以使用 SELECT... FOR SHARE 语句获取共享锁,但在读取完成后尽快释放锁,以提高并发度。例如,在电商商品展示页面,多个用户同时查看商品信息,对数据一致性要求不是特别高,只要能读取到已提交的商品信息即可,可以选择读已提交隔离级别。

读写混合场景

对于读写混合的场景,需要在保证数据一致性的前提下,尽量提高并发性能。如果业务对数据一致性要求较高,且不希望出现幻读问题,可重复读隔离级别是一个较好的选择。在这个级别下,通过 MVCC 保证读操作的并发性能,通过行级排他锁和间隙锁等处理写操作,避免数据冲突。

在锁策略方面,要合理安排锁的粒度和持有时间。对于涉及范围查询和修改的操作,要注意间隙锁和临键锁的使用,避免锁争用过于严重。例如,在银行转账系统中,既要保证账户余额查询的准确性(避免不可重复读和幻读),又要处理转账等写操作,可重复读隔离级别比较适合。在进行转账操作时,获取相关账户数据的行级排他锁,确保数据一致性。

高并发写场景

在高并发写场景下,对数据一致性的要求非常高,同时需要尽量减少锁争用。行级锁在这种场景下更为适用,因为它的粒度最小,能最大程度地提高并发度。可重复读隔离级别结合行级锁可以满足大部分高并发写场景的需求。

例如,在库存管理系统中,多个订单同时对库存进行扣减操作。每个订单在进行库存扣减时,获取库存数据的行级排他锁,在可重复读隔离级别下,通过 MVCC 保证其他事务在读取库存数据时不会受到干扰,同时行级锁确保了库存扣减操作的原子性和一致性。

数据一致性要求极高场景

如果业务对数据一致性要求极高,不允许出现任何并发问题,如银行核心账务系统等,串行化隔离级别是唯一的选择。虽然这种隔离级别会严重降低并发性能,但能保证数据的绝对一致性。在这种场景下,锁的粒度可以根据具体业务需求选择表级锁或行级锁,关键是要确保所有事务按照顺序执行,避免并发带来的任何数据不一致问题。

综上所述,在选择事务隔离级别和锁策略时,需要综合考虑业务场景对并发性能和数据一致性的要求,权衡利弊,选择最适合的方案。同时,在实际应用中,还需要通过性能测试和调优来进一步优化系统的性能,确保系统在满足业务需求的前提下,能够高效稳定地运行。