MySQL-锁机制
MySQL 锁机制
什么是锁?
为了保证数据并发访问时的一致性和有效性,数据库会提供锁机制。锁机制的优劣直接影响到数据库的并发处理能力和系统性能,所以锁机制也就成为了各种数据库的核心技术之一。
所以,锁机制是为了解决数据库的并发控制问题而产生的一种控制机制。例如:在同一时刻,两个不同事务同时对同一个表做更新数据或查询数据操作,为了保证数据的一致性,必须对并发操作进行控制。同时,锁机制也为实现 MySQL事务的各个隔离级别提供了保证。
锁的缺点
锁是一种消耗资源的机制,锁的各种操作,包括获得锁、检测锁是否已解除、释放锁等 ,都会增加系统的开销。
锁的分类
为了尽可能提高数据库的并发量,每次锁定的数据范围越小越好,但是锁定越小的范围,耗费的系统资源越多,也会系统性能下降。所以,为在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度”的概念,可以将锁粒度理解成锁定范围。
MySQL 按锁的粒度可以细分为:全局锁、表级锁、行级锁、页级锁。
全局锁
通过全局锁,可以锁定整个数据库实例,阻止所有写操作,读操作可正常执行,通常用于全库备份期间,避免数据或数据表的更新,导致备份数据出现误差。
执行全局锁:
flush tables with read lock
执行全局锁命令后:
- 数据库的增删改操作,会被阻塞,例如:insert、delete、update;
- 表结构的更改删除操作,会被阻塞,例如:alert table、drop table;
执行全局锁期间,整个数据库处于只读状态,备份结束后,需要及时释放全局锁。
释放全局锁:
unlock tables
为了避免全局锁在释放前,更新相关的业务(执行增删改操作)处于长时间的阻塞等待,可以在数据库备份时开启一个事务,通过可重复读的事务事务隔离级别,来避免长时间的等待。
表级锁(table lock)
什么是表级锁?
表级锁是一种表级别的锁定机制。它会锁定整张表,可以很好的避免死锁,是 MySQL 中最大颗粒度的锁定机制。表级锁根据操作的不同,分为:表级共享锁(S)、表级排他锁(X)、意向锁。
表级共享锁(S)
表级共享锁(shared lock)也被称为“读锁(read lock)”:事务获取表 S 锁后,仅能对整张表执行读操作(SELECT),不能进行任何修改(INSERT/UPDATE/DELETE)或表结构修改(ALTER TABLE)。
LOCK TABLE 表名 READ; -- 加入共享锁(S)
SELECT 语句; -- 只允许读
UPDATE 语句; -- 不允许写
UNLOCK TABLES; -- 释放共享锁
表级排他锁(X)
排他锁(exclusive lock)也被称为“写锁(write lock)”:事务获取表 X 锁后,拥有对整张表的完全控制权,可执行读写操作及表结构变更。
LOCK TABLE 表名 WRITE; -- 加入排他锁(X)
SELECT 语句; -- 允许读
UPDATE 语句; -- 允许写
UNLOCK TABLES; -- 释放共享锁
S锁和X锁的自动加锁
- MyISAM 存储引擎中,SELECT语句会自动获取表 S 锁,INSERT/UPDATE/DELETE会自动获取表 X 锁,事务结束(隐式提交)时释放。
- InnoDB 通常使用行级锁,但执行ALTER TABLE等 DDL 操作时,会自动获取表 X 锁,操作期间阻塞所有读写。
S锁和X锁的兼容性
- 若事务 A 持有表 S 锁,事务 B 请求表 S 锁:兼容,可同时读表。
- 若事务 A 持有表 S 锁,事务 B 请求表 X 锁:冲突,B 需等待 A 释放表 S 锁。
- 若事务 A 持有表 X 锁,事务 B 请求表 S 锁或表 X 锁:均冲突,B 需等待 A 释放表 X
S锁和X锁的优点和缺点
表级锁的优点和缺点
表级锁是MySQL 中锁定粒度最大的一种锁。
优点:对整张表加锁,资源消耗也比较少,加锁和释放锁的速度很快,不会出现死锁。
缺点:锁粒度大,发生锁冲突概率大,并发性低。
表级锁会显著降低并发性能,尤其是表 X 锁,可能导致长时间阻塞(如大表ALTER TABLE操作)。
MyISAM 的表级锁是导致其不适合高并发写入场景的核心原因(读写互斥)。
S锁和X锁的适用场景
表级 S 锁适用场景:
适用于 “多事务需要批量读取全表数据,且不允许期间有任何修改” 的场景,例如:生成全表数据统计报表时,防止统计过程中数据被修改导致结果不一致。
表级 X 锁适用场景:
适用于 “需对表进行大量修改或结构变更” 的场景,例如批量导入数据(LOAD DATA INFILE)时,通过表 X 锁阻塞其他事务,确保导入数据的完整性。
表级意向锁
意向锁是表级锁,是表级锁与行级锁的桥梁,用于标识 “某事务将要对表中的行加锁",避免表级锁与行级锁的冲突检测开销,分为“意向独占锁”和“意向共享锁”。
- 意向共享锁(IS):事务计划对表中某些行加 S 锁(行级锁)前,需先获取 IS 锁(表级锁)。
- 意向排他锁(IX):事务计划对表中某些行加 X 锁(行级锁)前,需先获取 IX 锁(表级锁)。
由于意向锁与表级共享锁和表级排它锁,都属于表级锁,所以它们之间是互斥的。为了满足表锁和行锁之间的“读读共享”“读写互斥”“写写互斥”,所以在添加“表级排他锁”的时候,就可以通过“意向排它锁”来快速判断表中是否已经加入了“行级排他锁”,避免全表扫描。
行级锁(row lock)
什么是行级锁?
行级锁的锁定颗粒度在 MySQL 中是最小的,只针对操作的当前行进行加锁,所以行级锁发生锁定资源争用的概率也最小。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要处理并发逻辑更多,同时消耗的资源也就更大。此外,行级锁也最容易发生死锁。
所以,行级锁最大程度地支持并发处理的同时,也带来了最大的锁开销。
InnoDB 存储引擎默认使用行级锁。MyISAM存储引擎不支持行级锁。
行级锁的分类
- 记录锁(Record Lock):只锁定一条记录;
- 间隙锁(Gap Lock):锁定一个数据范围,不包含记录本身;
- 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个数据范围,包含记录本身;
记录锁(Record Lock)
记录锁(Record Lock)是对表中具体一行记录施加的锁,仅锁定记录本身。它分为共享记录锁(S 锁)和排他记录锁(X 锁),遵循行级锁的兼容性规则:多个 S 锁可共存,S 锁与 X 锁互斥,X 锁与 X 锁互斥。
- 共享锁(S 锁):又称读锁,多个事务可同时获取同一数据的 S 锁,用于读取数据。持有 S 锁时,不允许其他事务获取 X 锁,但允许其他 S 锁。
- 排他锁(X 锁):又称写锁,事务获取数据的 X 锁后,可修改数据。持有 X 锁时,禁止其他事务获取 S 锁或 X 锁,确保数据修改的独占性。
间隙锁(Gap Lock)
间隙锁是 InnoDB 为解决幻读问题设计的特殊锁类型,它不锁定具体记录,而是锁定记录之间的间隙区域,防止其他事务在该间隙中插入新记录。
间隙锁(Gap Lock)是对索引记录之间的空白区域施加的锁,作用是阻止其他事务在该间隙中插入新记录。间隙锁属于表级逻辑锁,但依赖索引存在,其锁定范围由索引值决定。
临键锁(Next-Key Lock)
临键锁是记录锁与间隙锁的组合,是 InnoDB 在REPEATABLE READ隔离级别下的默认加锁方式,兼顾了记录锁定和间隙锁定的需求。
临键锁(Next-Key Lock)= 记录锁 + 间隙锁,它是 InnoDB 最常用的行级锁类型。锁定范围是索引中的一个 “左开右闭” 区间(锁定具体记录和该记录前面的间隙)。
行级锁的优点和缺点
行级锁是MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发性高,但加锁的开销也最大,加锁慢,会出现死锁。
页级锁(page lock)
页级锁是 MySQL 中比较独特的一种锁定级别。
页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力同样也是介于上面二者之间。另外,页级锁和行级锁一样,会发生死锁。
页级锁主要应用于 BDB 存储引擎。
锁分类总结
乐观锁与悲观锁
悲观锁(Pessimistic Lock)
悲观锁(又名“悲观并发控制”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
所以,悲观锁从名称上顾名思义,就是很悲观的锁定思想,每次去拿数据的时候都认为其它的事务会修改,所以每次在拿数据的时候都会上锁,这样其它事务想要获取这个数据就会被block阻塞,直到当前事务释放锁。关系型数据库中的行锁,表锁,读锁,写锁等,都是在做操作之前先上锁。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
乐观锁(Optimistic Lock)
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于读多写少的应用场景。
比较
两种锁各有优缺点:
- 乐观锁适用于写比较少的情况下,即:并发冲突相对较低的场景。这样可以省去了锁的开销,加大了系统的整个吞吐量。
- 悲观锁适用于写入比较多的情况下,因为如果经常产生数据冲突,使用乐观锁,应用程序会不断的retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
死锁
**数据库的死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。**常见的报错信息为“Deadlock found when trying to get lock…”。
死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁。多数情况下只需要重新执行因死锁回滚的事务即可。
在实际应用中,我们通过下面几种方式避免死锁的方法:
- 如果不同程序会并发存取多个表,或者涉及多行记录时,尽量约定以相同的顺序访问表,这样可以大大降低死锁的发生。
- 业务中要及时提交或者回滚事务,可减少死锁产生的概率。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁粒度,通过表锁定来减少死锁产生的概率(表级锁不会产生死锁)。