MySQL的锁

锁的类型

1、操作数据的类型

1.1、读锁/共享锁(S锁)

对同一份数据,多个事务的读操作可以同时进行而不会互相影响,也不会相互阻塞。多个事务只能读操作不能写操作。

对读取的记录加S锁语法:

select ... lock in share mode;
select ... for share;
1.2、写锁/排他锁(X锁)

对同一份数据,当前事务的写操作没有完成前,它会阻断其他写锁和读锁,保证只有一个事务能进行写入操作,防止其他事务读写同一数据。

加X锁的语法

select ... for update;

在mysql5.7之前,X锁,如果获取不到锁,会进入等待状态,直到innodb_lock_wait_timeout超时。

在8.0版本中,S锁和X锁在语法后添加NOWAIT(如果查询的行已经加锁,那么NOWAIT会立即报错返回)、SKIP LOCKED(如果查询的行已经加锁,也会立即返回,返回的结果不会包含被锁定的行)。

2、操作数据的粒度

2.1 表级锁

开销小,加锁快:因为只需要对整个表进行锁定,不需要维护复杂的锁结构,所以操作快速且资源消耗较少。
不会出现死锁:由于锁定整个表,事务之间的依赖关系简化,降低了死锁发生的可能性。
实现简单:逻辑相对简单,易于理解和实现。
锁定粒度大:会锁定整个表,即使事务只操作表中的一小部分数据,其他事务也无法访问该表的任何部分,导致并发度低。
并发度低:因为一次只能有一个事务访问表,所以在高并发环境下可能会引起严重的性能瓶颈。
资源竞争激烈:如果有多个事务需要访问同一表的不同部分,它们必须等待锁释放,增加了等待时间。

2.1.1 表级别的S锁、X锁
lock tables 表名  read
lock tables 表名  write

InnoDB:
一般情况下,都避免在InnoDB下使用表级别的S锁、X锁,只会降低并发。
MyISAM:
执行读操作前,给需要的所有表加上S锁;有读锁的情况下:自己可读,自己不可写,别人也可读,别人不可写
执行写操作前,给需要的所有表加上X锁;有写锁的情况下:自己可读,自己可写,别人不可读,别人不可写

2.1.2 意向锁

允许行级锁和表级锁共存的多粒度锁。在保证并发性的前提下,实现行级锁和表级锁共存,还满足事务隔离性。
目的:为了协调行锁和表锁的关系,不与行级锁冲突的表级锁,且表示某个事务在那行持有了锁,或者某事务准备持有锁
当给表中某一行加上X锁,数据库会自动给数据页或者数据表加意向锁,告诉其他事务这里有事务上过X锁了
意向共享锁(IS):事务有意向对表中某些行加S锁
意向排他锁(IX):事务有意向对表中某些行加X锁
IS,IX是表级锁,不会和行级的S,X冲突,但是和表级的会。务处理,避免造成服务阻塞。

2.1.3 自增锁

自增锁是在向具有AUTO_INCREMENT(自增)属性的表插入数据时,MySQL(尤其是InnoDB存储引擎)自动获取的一种特殊类型的表级锁。它的主要目的是为了保证自增值的唯一性和顺序性,避免在并发插入时自增值错乱。具体来说,当事务开始插入新行时,会锁定自增列的计数器,直到事务结束才释放。这确保了即使在高并发环境下,每个新插入的行都能获得一个唯一的、递增的自增值。

2.1.4 元数据锁

元数据锁是MySQL引入的一种机制,用于保护数据库的元数据(如表结构定义)免受并发操作的不一致性影响。元数据锁分为读锁和写锁两种:

MDL读锁:当一个查询(如SELECT)或数据操作(如INSERT、UPDATE、DELETE)访问一个表时自动获取。这类锁允许其他事务并发读取同一表,但会阻止其他事务对表结构进行修改(如ALTER TABLE、DROP TABLE),以确保数据读取操作过程中表结构的稳定性。
MDL写锁:当对表结构进行修改操作(如创建索引、修改列定义、重命名表等)时获取。这类锁不仅阻止其他事务对表进行结构修改,同时也阻止任何读取或写入该表数据的操作,直到表结构修改完成。这样做确保了在表结构变更过程中,不会有其他操作影响变更的完整性。

2.2 行级锁

锁定粒度小:只锁定需要修改的行,允许其他事务并发访问表中的其他行,提高了并发处理能力。
并发度高:多个事务可以同时修改表的不同行,适合高并发场景。
减少锁等待:由于锁的范围小,减少了不必要的阻塞,提高了系统整体吞吐量。
开销大,加锁慢:需要维护复杂的锁结构来跟踪每行的锁状态,消耗更多内存和CPU资源。
易发生死锁:由于存在多行锁,事务间的锁依赖复杂,增加了死锁的可能性。
I/O操作较多:在进行查询时,为了检查锁的状态,可能需要更多的I/O操作,尤其是在对大量数据进行操作时。

2.2.1 记录锁

锁住了某条记录,对周围的数据没有影响,记录锁是有S锁和X锁之分的,称之为S型记录锁 和 X型记录锁 。

当事务获取了记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;

当事务获取了记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。

2.2.2 间隙锁

为了防止插入幻影记录而提出的,

间隙锁会锁定某个开区间的数据,这个区间指的是两个已存在记录之间的虚拟间隔,或者是索引中第一个记录之前或最后一个记录之后的区间。一旦某个事务对某个区间加了间隙锁,其他事务对该区间内的任何位置尝试插入新记录的操作都会被阻塞,从而防止了幻读现象的发生。但是,需要注意的是,间隙锁并不阻止其他事务读取该区间内的数据(如果数据已经存在的话),只是阻止插入和更新操作。此外,如果其他事务请求的是行锁,并且请求的行不在已锁定的间隙内,那么这个读或写操作仍然可以进行,前提是不违反其他锁的约束

根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B),如果有区间没有记录,会锁定该页最大记录(A,该页最大记录)

2.2.3 临键锁

就是一个`记录锁`和一个`gap锁`的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的`间隙`

2.2.4 插入意向锁

目的:插入意向锁表明一个事务打算在某个间隙内插入一行记录,而不是锁定实际的数据行或整个间隙。它是一种较弱的锁,旨在减少插入操作之间的冲突。

当一个事务尝试插入一行数据到某个索引区间,InnoDB首先会请求一个插入意向锁。这不会直接锁定整个间隙,而是表明有插入意图,减少不必要的锁竞争。如果该间隙已经有其他事务持有了间隙锁或临键锁,那么这个插入意向锁会等待,直到持有锁的事务完成并释放锁。

插入意向锁与其他事务的间隙锁或临键锁兼容,意味着它们之间不会相互阻塞,除非实际要插入的位置已经被其他事务的具体行锁锁定。这样设计是为了尽可能减少锁之间的冲突,提升并发插入的效率。

2.3 页锁

处于表锁和行锁之间的。

3、锁的态度

只是一种设计思想,类似悲观者和乐天派

3.1 乐观锁

认为同一资源并发操作是小概率事件,读 > 写 ,不会去上锁,在更新的时候会判断一下在此期间别人有没有去更新这个数据。一般采用版本号机制或者CAS机制。时间戳机制与版本号机制类似

版本号机制:
原理:在数据表中增加一个额外的字段,通常称为version(版本号)。每次数据被读取时,该版本号会随着数据一起被读出。当事务尝试更新数据时,除了正常的更新条件外,还需要检查当前数据库中该记录的版本号是否与事务开始时读取到的版本号相等。如果相等,则允许更新,并将版本号加一;如果不等,说明数据已被其他事务修改过,当前事务放弃更新,可以选择重试或报错。
优点:减少了锁的使用,提高了系统的并发性能。因为大多数情况下,数据不会发生并发冲突,乐观锁的失败重试策略能够处理冲突,使得系统的吞吐量得到提升。
缺点:在高并发且冲突频繁的场景下,频繁的重试可能会增加事务的执行时间,降低用户体验。

CAS机制:
原理:Compare and Swap是一种硬件指令,后来也被应用于软件算法中,特别是在无锁编程领域。其基本思想是,原子性地比较内存中的某个值与期望值是否相等,如果相等,则更新为新的值;如果不等,则不做任何操作。这个过程作为一个不可分割的整体执行,确保了操作的原子性。
应用:在Java中,AtomicInteger、AtomicLong等原子类就是利用了CAS机制来实现线程安全的更新操作。在分布式系统中,如Redis的WATCH-MULTI-EXEC模式也体现了类似的乐观锁思想。
优点:无锁设计,减少了线程上下文切换和锁带来的开销,提高了程序的执行效率。
缺点:在高并发场景下,如果更新失败(即期望值与当前值不一致)的概率较高,可能会导致大量的自旋重试,消耗CPU资源,即所谓的“ABA问题”和“活锁”问题。
乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,但是它阻止不了除了程序以外的数据库操作。

3.2 悲观锁

认为同一资源并发操作是大概率事件,读 < 写 ,会去上锁,从而保证数据操作的排它性。共享资源每次只给一个线程使用,其它线程阻塞, 用完后再把资源转让给其它线程。

悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和 写 - 写的冲突。

4、加锁的方式

4.1显式锁

显式锁是指程序员在代码中明确声明和管理的锁。开发者需要手动获取锁、使用锁保护共享资源的访问,并在完成后显式释放锁。

通常在需要更细粒度控制并发访问的场景下使用。

4.2 隐式锁

本次事务提交前不希望被别的事务访问到。

InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+树中。
在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将隐式锁转换为显示锁。
检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到写数据
等待加锁成功,被唤醒,或者超时。
写数据,并将自己的trx_id写入trx_id字段

聚簇索引:

新插入记录标识:每当事务在聚簇索引中插入一条新记录时,该记录会包含一个隐藏列trx_id,用于标记执行此插入操作的事务ID。
锁检测与创建:如果另一个事务尝试对这条新记录加锁(S锁或X锁)时,它会检查记录的trx_id是否属于当前活跃的事务。如果是,则帮助原事务创建一个X锁(表示独占访问,is_waiting设为false,表明锁已被持有),而自己则进入等待状态(创建锁结构,is_waiting设为true)。

二级索引:

二级索引标识:二级索引记录没有直接的trx_id隐藏列,但所在页面的PAGE_MAX_TRX_ID属性记录了对该页面改动过的最大事务ID。
判断与回表:如果PAGE_MAX_TRX_ID小于当前系统中最活跃事务的ID,说明页面上的更改已全部提交,可以直接进行下一步操作。否则,需要进一步定位到具体的二级索引记录,然后通过该记录找到其对应的聚簇索引记录。
重复聚簇索引处理流程:找到对应的聚簇索引记录后,处理方式同情景一,即检查聚簇索引记录

优点:简化了并发控制的实现,降低了因不当管理锁而导致的错误,如死锁。

缺点:灵活性较低,可能无法满足特定的并发控制需求,且开发者难以对锁的使用进行精细控制。

5、全局锁

全局锁对整个数据库实例加锁,是一种重量级的同步手段,主要应用于需要确保数据库处于完全静默、只读状态的特殊场景。
作用范围:影响整个数据库实例,涵盖所有数据库和表。
启用后,数据更新语句、数据定义语句和更新类事务的提交语句会被阻塞 。
典型使用场景: 全库逻辑备份:
备份一致性:在进行全库逻辑备份时,使用全局锁可以确保备份过程中数据的一致性,避免因备份期间的数据变动导致备份文件不完整或数据不一致。
性能与风险考量:尽管全局锁能确保数据一致性,但它显著影响数据库的并发写入能力,因此应谨慎使用,并尽量安排在低峰时段执行,以减少对业务的影响。

Flush tables with read lock

6、死锁

(MySQL)两个事务都持有对方需要的锁,并且在等待对方释放,并且双方都不会释放自己的锁

必要条件:
互斥条件:至少有一个锁资源必须被一个事务独占使用。
请求和保持条件:一个事务已经至少持有一个锁资源,但又请求额外的锁资源,而这些锁资源又被其他事务持有。
不剥夺条件:事务已经获得的锁资源在事务结束之前不能被其他事务强行夺走。
循环等待条件:存在一种锁资源的循环等待链,其中每个事务都持有下一个事务所需的锁资源,并等待其他事务释放锁资源。

怎么处理死锁:
方式1:等待,直到超时
方式2:使用死锁检测处理死锁程序
主动的死锁检测机制,要求数据库保存锁的信息链表和事务等待链表两部分信息,一旦检测到回路、有死锁,这时候InnoDB存储引擎会选择回滚undo量最小的事务,让其他事务继续执行
缺点:每个新的被阻塞的线程,都要判断是不是由于自己的加入导致了死锁。
方式1:关闭死锁检测,但意味着可能会出现大量的超时,会导致业务有损。
方式2:控制并发访问的数量。
方式3:将一行改成逻辑上的多行来减少锁冲突

怎么避免死锁:
合理设计索引,让业务sql通过索引选择更少的数据,减少锁竞争
调整Sql执行顺序,避免update/delete长时间持有锁
避免大事务,将大事务拆成小事务处理,因为小事务锁定资源时间短,发生锁冲突几率也小
在并发高的情况下,不要显示加锁,特别是事务里显示加锁
降低事务的隔离级别,如从可重复读调整为读已提交,可以减少锁的竞争。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值