一、事务隔离性概述
事务隔离性(isolation) 是指多个并发事务之间相互隔离,一个事务的执行不应该影响其他事务的执行。
mysql 支持四种事务隔离级别,默认隔离级别:可重复读(repeatable read)。

锁的行为与事务的隔离级别紧密相关:
读未提交:通常不加锁,通过直接读最新数据实现,存在脏读、幻读等问题。
读已提交:每次读取的都是已提交的最新数据(快照)。 对于 update、delete,只对实际修改的行加锁(记录锁),不使用间隙锁,所无法避免幻读。
可重复读(innodb 的默认级别):在事务开始时创建一致性视图,整个事务期间都读取这个视图。使用临键锁作为默认的行锁算法,通过锁住记录和间隙来防止幻读。
串行化:最高的隔离级别,通过强制事务串行执行来避免所有并发问题。 在这个级别下,innodb 可能会隐式地将普通的 select 语句转换为 select ... forshare,从而加共享锁,导致读读也可能阻塞。
二、mysql 保证隔离性的机制
mysql 的锁机制是为了在并发访问下,保证数据的一致性、完整性而设计的。它可以大致分为以下三个维度来理解:
- 锁的粒度:锁定的范围大小
- 锁的模式:锁的兼容性,即多个锁之间能否共存
- 锁的类型:从程序员视角看,锁的共享与排他特性
按锁的粒度划分
1 全局锁,作用范围:整个数据库实例。
flush tables with read lock; set read_only = on
效果:使数据库处于只读状态,所有数据修改操作(dml)和数据结构变更操作 (ddl)都会被阻塞。
使用场景:全库逻辑备份。但请注意,在 innodb 中,由于有 mvcc,通常使 用 mysqldump --single-transaction 来进行不锁表的热备份,这比全局锁更好。
2 表级锁,作用范围:整张表
lock tables table_name read/write; 和 unlock tables;
myisam 引擎默认使用表锁。
元数据锁:不需要显式使用,在执行 dml(如 select, insert, update, delete)时自动加,防止表结构被修改。读锁之间不互斥,但写锁是排他的。
意向锁:innodb 特有的表级锁,用于快速判断表内是否有行锁冲突。
效果:锁定整张表,粒度大,并发性能差。
3 行级锁, 作用范围:单行或多行记录
支持引擎:innodb。
效果:粒度最小,只锁定需要操作的行,极大提高了并发性能。但管理开销最大,容易 导致死锁。
实现方式:innodb 通过给索引项加锁来实现行锁。这意味着:如果查询条件没有用到索引,innodb 会退化为表锁,行锁实际上是加在索引记录上的。
innodb 的行锁模式(锁的类型)
1 共享锁,简称:s 锁。
特性:又称为读锁。一个事务获取了某行的共享锁后,其他事务也可以获取同一行的共享锁(读读兼容),但不能获取该行的排他锁(读写不兼容)。
加锁方式:
select ... lock in share mode; -- 在旧版本中 select ... for share; -- 在 mysql 8.0+ 中推荐使用
2 排他锁,简称:x 锁。
特性:又称为写锁。一个事务获取了某行的排他锁后,其他事务既不能获取该行的共享锁,也不能获取该行的排他锁(写写、读写都不兼容)。
加锁方式:
select ... for update;
dml 语句(update, delete, insert)会自动给涉及的行加上排他锁。
兼容性矩阵:

三、行锁的算法(锁定了哪些数据)
innodb 的行锁是通过对索引项加锁实现的,根据查询条件和索引使用情况,锁定的范围有所不同。
3.1 记录锁
锁定:单个索引记录。
场景:当语句精确匹配到某一条记录,且使用了唯一索引(包括主键)时。
示例:
select * from users where id = 10 for update; 如果 id 是主键,这条语句会在 id=10 这条索引记录上加 x 锁。
3.2 间隙锁
锁定:一个索引区间,但不包括记录本身。例如锁住 (5, 10) 这个开区间。
目的:防止其他事务在区间内插入新的记录,从而解决幻读问题。
场景:使用范围查询或者查询不存在的记录时。
示例:
select * from users where id between 5 and 10 for update; 这条语句会锁住 id 在 (5, 10) 区间内的所有“间隙”,防止插入 id=6,7,8,9 的新记录。
3.3 临键锁
锁定:一个索引记录 + 该记录之前的间隙。它是 记录锁 + 间隙锁 的组合。
目的:innodb 默认的行锁算法。它既锁住了记录本身,也锁住了它之前的间隙,从在 “可重复读”隔离级别下有效地防止了幻读。
示例:如果表中有记录 id=5, 10, 15。
select * from users where id > 10 and id <= 15 for update; 这条语句会锁定 (10, 15] 这个区间。它锁住了 id=15 这条记录(记录锁), 以及 (10,15) 这个间隙(间隙锁)。
四、 死锁
当两个或多个事务相互等待对方释放锁时,就会发生死锁。
示例:
事务 a:update table set ... where id = 1; (持有 id=1 的 x 锁)
事务 b:update table set ... where id = 2; (持有 id=2 的 x 锁)
事务 a:update table set ... where id = 2; (等待事务 b 释放 id=2 的锁)
事务 b:update table set ... where id = 1; (等待事务 a 释放 id=1 的锁)
-> 死锁发生!
innodb 的处理方式:
innodb 有一个内置的死锁检测机制,会主动选择一个回滚成本较小的事务(通常就是影响行数较少的事务)进行回滚,并报出 1213 错误(deadlock found when trying to get lock)。
另一个事务则可以继续执行。
如何避免死锁:
尽量让事务以相同的顺序访问表和行。
在事务中,尽量一次锁定所有需要的资源,减少事务大小。
使用较低的隔离级别(如 read committed)可以减少间隙锁的使用,从而降低死锁概率。
为表添加合理的索引,避免全表扫描导致锁表。
到此这篇关于mysql 事务隔离性及锁的文章就介绍到这了,更多相关mysql内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论