数据库复习笔记8——MySQL锁机制

tech2022-07-11  195

MySQL锁机制

锁的概念

锁是计算机协调多个进程或线程并发访问某一资源的机制。锁保证数据并发访问的一致性、有效性;锁冲突也是影响数据库并发访问性能的一个重要因素。锁是Mysql在服务器层和存储引擎层的的并发控制。

加锁是消耗资源的,锁的各种操作,包括获得锁、检测锁是否是否已解除、释放锁等。

Latch和Lock

有两种锁,一种是Latch闩锁(轻量级的锁),它要求锁定的时间非常短,若持续时间长,则其应用性能会很差(有点类似于OS中的自旋锁,会忙等)。在InnoDB引擎中,Latch分为rwlock(读写锁)和mutex(互斥锁),其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

还有一种是Lock,它的对象是事务,用来锁定数据库中的对象,如表、页、行。并且lock的对象commit或rollback后进行释放(不同事物隔离级别释放的时间可能不同)。

在MySQL中的引擎中主要是Lock。

按照粒度划分锁

Lock锁根据粒度主要分为表锁、行锁、页锁,不同的存储引擎拥有的锁粒度都不同。

表锁:表级锁就是一次会将整个表进行锁定,是个存储引擎中最大颗粒度的锁机制;它的特点是实现逻辑简单,资源消耗较少,获取锁和释放锁的速度很快,很好的解决了死锁问题。使用表锁的主要是MyISAM(只支持表锁)、MEMORY,CSV等一些非事务性存储引擎。但是,它发生锁冲突的概率最高,并发度最低。

 表级锁更适合于以查询为主,并发用户少,只有少量按索引条件更新数据的应用,如Web 应用,因为查询的时候,锁是共享锁。

行锁:MySQL中锁定粒度最小的锁机制,特点是发生锁定资源争用的概率小,能给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能,但资源消耗较大,且可能会产生死锁。使用行锁的主要是InnoDB引擎。

行级锁更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统.

页锁:它的锁定颗粒度介于表锁和行锁之间,它的并发能力与资源开销也是介于两者之间,另外,它与行锁一样,会发生死锁。使用页锁的主要是BerkeleyDB存储引擎。

默认情况下,表锁和行锁都是自动获得的, 不需要额外的命令。

但是在有的情况下, 用户需要明确地进行锁表或者进行事务的控制, 以便确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成。

按照共享性划分锁

从类别上锁分为:共享锁(S Lock)和排它锁(X Lock)。

共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。加了共享锁的数据对象可以被其他事务读取,但不能修改。(对于MyISAM存储引擎,执行select语句,自动加上读锁;对于InnoDB,select语句不加锁)

排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。加上排他锁的数据,既不能被其他事务读取,也不能被其他事务修改。(执行UPDATE、INSERT、DELETE语句,自动加上排他锁)

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:

意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。

InnoDB加锁机制

隐式加锁:

首先,意向锁是 InnoDB 在行锁之前自动加的, 不需用户干预。对于 UPDATE、 DELETE 和 INSERT 语句(写操作), InnoDB会自动给涉及数据集加排他锁(X)对于普通 SELECT 语句,InnoDB 不会加任何锁

显式加锁:

事务可以通过以下语句显式给记录集加共享锁或排他锁:

性能影响:

select for update 语句,相当于一个 update 语句。在业务繁忙的情况下,如果事务没有及时的commit或者rollback 可能会造成其他事务长时间的等待,从而影响数据库的并发使用效率。select lock in share mode 语句是一个给查找的数据上一个共享锁(S 锁)的功能,它允许其他的事务也对该数据上S锁,但是不能够允许对该数据进行修改。如果不及时的commit 或者rollback 也可能会造成大量的事务等待。

InnoDB行锁的实现

虽然InnoDB默认使用行锁,但是InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!

如果上述id列不是索引键,那么InnoDB只能创建表锁,并发性将大打折扣。

MySQL InnoDB支持三种行锁定方式:

行锁(Record Lock):锁直接加在索引记录上面,锁住的是key。间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变。间隙锁是针对事务隔离级别为可重复读以上级别而言的。目的:gap lock的机制主要是解决可重复读模式下的幻读问题Next-Key Lock :行锁和间隙锁组合起来就叫Next-Key Lock;InnoDB对于行的查询都是采用这种锁定算法,既锁定间隙和锁定行。

两阶段锁协议

InnoDB在事务执行过程中,使用两阶段锁协议:

随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

两阶段锁协议将一个事务分为两阶段。第一阶段为加锁阶段,只可以申请锁,当释放一个锁后进入解锁阶段,只可以释放锁。

死锁

指多个事务在执行过程中因为争夺锁资源而产生的相互等待现象。

死锁的原因:

真正的数据冲突;存储引擎的实现方式。

检测死锁:数据库系统实现了各种死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。

死锁恢复:死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。

外部锁的死锁检测:发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决(外部死锁不通过死锁检测,而是死锁超时机制控制)

死锁影响性能:死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖innodb_lock_wait_timeout设置进行事务回滚。(等待时间超过限制,就进行事务回滚,实现简单,开销小。

优化锁的性能

尽量使用较低的隔离级别;精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会选择合理的事务大小,小事务发生锁冲突的几率也更小给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响不要申请超过实际需要的锁级别除非必须,查询时不要显式加锁。 MySQL的MVCC(多版本并发控制机制)可以实现事务中的查询不用加锁,优化事务性能;MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能。

乐观锁与悲观锁

1、乐观锁(不加锁,直到提交时再检查是否有“写-写”冲突)

乐观锁(Optimistic Lock):从名字上看,就是很乐观,每次去拿数据的时候认为别人不会改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新数据,乐观也严谨。乐观锁适用于读多写少的场景,这样可以提高吞吐量。乐观锁总是假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性(覆盖写)。

乐观锁通过使用数据版本(Version)记录机制(最常用)和时间戳(timestamp)来实现。确保事务提交之前,所读取的数据没有被更新。

数据版本机制:是通过为数据库表增加一个数字类型“version”字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,version值加1.当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

时间戳机制:时间戳的方式与前者实现方式差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

2、悲观锁(操作之前先加锁,避免一切冲突)

悲观锁(Pessimistic Lock):就是比较悲观,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会对数据上锁,这样别人想拿这个数据的时候就会先block,然后获得锁之后再对数据进行操作。总的来说就是悲观锁总是假定会发生并发冲突,然后屏蔽一切可能违反数据完整性的操作。

悲观锁大多数情况下依靠数据库的锁机制实现,例如使用Select … for update语句,以保证操作最大程度的独占性。但如果采用悲观锁,则整个操作过程中,数据库始终处于加锁状态(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),而面对成百上千万的并发,就会导致灾难性的后果,因此采用悲观锁进行控制时要考虑清楚。

MVCC机制(无锁并发控制,基于快照)

多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照(快照读)。 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读。

MVCC机制只能应用在RC和RR两种事务隔离级别下,由于使用了快照读机制,读取的内容始终是事务开始前的数据库快照,所以其它事务的修改和回滚对它没有影响,解决了脏读和不可重复读

关于MVCC的详细解释参见:https://www.cnblogs.com/moershiwei/p/9766916.html。

参考资料:

1、https://hillzhang1999.gitee.io/2020/05/29/shu-ju-ku-fu-xi-ji-yu-mysql/#toc-heading-104​​​​​​​

最新回复(0)