Mysql之锁的介绍

本文阅读 24 分钟
首页 代码,Java 正文

数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。对于任何一种数据库来说都需要有相应的锁定机制,所以MySQL自然也不能例外。 MySQL数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎所针对的应用场景特点都不太一样,为了满足各自特定应用场景的需求,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计的,所以各存储引擎的锁定机制也有较大区别。MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。

表锁

表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。 优点:开销小,加锁快;不会出现死锁   缺点:锁定粒度大,发生锁冲突的概率最高,并发度最低

页锁

页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。 优点:开销和加锁时间界于表锁和行锁之间;锁定粒度界于表锁和行锁之间,并发度一般 缺点:会出现死锁

行锁

行级锁定最大的特点就是锁定对象的颗粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。 优点:锁定粒度最小,发生锁冲突的概率最低,并发度也最高 缺点:开销大,加锁慢;会产生死锁

在数据库中,lock和latch都可以被称为“锁”。但是两者有着截然不同的含义,不过这里我们主要关注的是lock。

latch

latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测

lock

lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同隔离级别释放的时间可能不同)。此外,lock是有死锁检测机制的。

MyISAM是一种非事务类型的只支持表锁的存储引擎。使用的锁定机制完全是由MySQL提供的表级锁定实现。   MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性:   1.对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;   2.对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;   MyISAM加锁方式:   MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁。   在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因 此,用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。

InnoDB存储引擎是目前使用最广泛的,因此这里我们重点讲InnoDB存储引擎中的锁机制。 Innodb存储引擎实现了如下两种标准的行级锁:

  • 共享锁(S lock):允许事务读一行数据。
  • 排它锁(X lock):允许事务删除和更新一行数据。 需要特别注意的是,InnoDB中行锁是加在索引上,而不是加在行记录上的

我们知道,InnoDB存储引擎即支持表锁,又支持行锁,也就是支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。也就是说,如果需要对页上的记录上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对行记录上X锁。若其中任何一部分导致等待,那么该操作需要等待粗粒度锁的完成。 InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

  • 意向共享锁(IS lock):事务想要获得一张表中某几行的共享锁。
  • 意向排它锁(IX lock):事务想要获得一张表中某几行的排它锁。

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。 从InnoDB1.0开始,在INFORMATION_SCHEMA架构下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS,可以通过它们监控当前事务并分析可能存在的锁问题,有关这些表的内容大家可以自行学习,这里不再详述。

在查看上述表时,需要注意一个情况,用户运行一个范围查找时,如果当前资源被锁住了,若锁住的页因为INnoDB存储引擎缓冲池的容量,导致该页从缓冲池中被刷出,则查看INNODB_LOCKS表时,该值同样会显示null,即InnoDB存储引擎不会从磁盘进行再一次的查找。

锁的算法

InnoDB存储引擎有3中行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
  • Next-Key Lok:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

Next-key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。

采用Next-key Lock的锁定技术称为Next-key Locking。其设计的目的是为了解绝“幻读问题”,我们会在稍后介绍。而利用这种锁定技术,锁定的不是单个值,而是一个范围。除了next-key locking,还有previous-key locking技术。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:

(负无穷,10]
(10,11]
(11,13]
(13,20]
(20,正无穷]

若采用previous-key locking技术,那么可锁定的区间为:

(负无穷,10)
[10,11)
[11,13)
[13,20)
[20,正无穷)

然而,当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。也就是说,Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,这情况完全不同。如果查询的列(注意:这里指的是查询的列而不是条件列)是辅助索引,则仍然使用传统的Next-Key Lock技术加锁。如果查询列中有两个索引,其需要分别进行锁定。对于聚簇索引,使用Record Lock进行加锁;对于非聚簇索引,则使用Next-Key Lock技术加锁。

特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上Gap lock。例如:

create table t(a int,b int ,primary key(a),key(b));
insert into t select 1,1;
insert into t select 3,1;
insert into t select 5,3;
insert into t select 7,6;
insert into t select 10,8;

当我们执行select * from t where b=3 for update时,InnoDB存储引擎不但会用Next-Key Lock锁定(1,3)范围,还会对辅助索引范围为(3,6)加锁。因此,若在新会话中运行以下sql,都会被阻塞:

select * from t where a=5 lock in share mode;
insert into t select 4,2;
insert into t select 6,5;

从上例中可以看出,Gap Lock的作用是为了阻止多个事务将记录插入到同一范围内,进而避免幻读问题的出现。 当然,我们也可以通过如下方式显式的关闭Gap Lock:

  • 将事务的隔离级别设置为Read Committed
  • 将参数innodb_locks_unsafe_for_binlog设置为1 在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。但需要记住,上述设置破坏了事务的隔离性,并且可能会导致主从复制不一致。此外,从性能上来说,Read Committed也不会优于Mysql默认的事务隔离级别。

在InnoDB存储引擎中,对于Insert操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许插入。对于上面的例子,一个会话中锁定了表t中b=3的记录,即已经锁定了(1,3)的范围,这时若在其他会话中进行如下插入操作同样会被阻塞:

insert into t select 2,2;

最后需要再次提醒的是,对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引有多列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定

死锁

死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种相互等待的现象。 解决死锁问题最简单的一种方式是超时,即当两个事务互相等待时看,当一个等待时间超时设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续进行了。在InnoDB中,参数innodb_lock_wait_timeout用来设置超时的时间。 超时机制虽然简单,但是其仅通过超时后对事务进行回滚的方式来处理,或者说其根据FIFO的顺序选择回滚对象。但若超时的事务所占权重比较大,如事务操作更新了很多行,占用了较多的undo log,这时采用FIFO的方式,就显得不合适了,因为回滚这个事务的时间相对另一个事务所占用的时间可能会更多。 因此,当前数据库还都普遍采用wait-for graph(等待图)的方式来进行死锁检测。InnoDB存储引擎也采用的这种方式。wait-for graph要求数据库保存以下两种信息:

  • 锁的信息链表
  • 事务等待链表 通过上述链表可以构造出一张图,若图中存在回路,就表示存在死锁。wait-for graph是一种较为主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,通过来说InnoDB存储引擎选择回滚undo量最小的事务

锁升级

锁升级是指将当前锁的粒度降低。 有些数据库设计时认为锁是一种稀有的资源,在适合的时候会自动地将行、键或分页锁升级为更粗粒度的表级锁。这种升级保护了系统资源,防止系统使用太多的内存来维护锁,在一定程度上提高了效率。

InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生锁的,相反,其根据每个事务访问的每个页对锁进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一致的。 假设一个表有三百万个数据页,每个页大约有100条记录,那么总共有三亿条记录。若一个事务执行全表更新的语句,则需要对所有的记录加X锁。若根据每行记录产生锁对象进行加锁,并且每个锁占用10个字节,则仅对锁管理就需要差不多3GB的内存。而InnoDB存储引擎根据页进行加锁,并采用位图方式,假设每个页存储的锁信息占用30个字节,则锁对象仅需90MB的内存。由此可见两者对于锁资源开销的差距之大。

一致性非锁定读

一个行记录可能有不止一个快照数据,一般称这种技术为多版本技术。由此带来的并发控制,称之为多版本并发控制(MVCC)

一致性非锁定读是指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。所谓快照数据是指该行的之前版本的数据,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。

非锁定读机制极大地提高了数据库的并发性。但是在不同事务隔离级别下,读取的方式不同,也并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

对于InnoDB存储引擎来说,在事务隔离级别READ COMMITTED和REPEATABLE READ(默认的隔离级别)下,Innodb存储引擎都使用一致性非锁定读。然而,对快照的定义却不相同。在READ COMMITTED事务隔离级别下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,一致性非锁定读总是读取事务开始时的行数据版本。这也就解释了为什么READ COMMITTED事务隔离级别下会出现“不可重复读”问题。

一致性锁定读

在InnoDB存储引擎中默认的隔离级别REPEATABLE READ下,select操作使用一致性非锁定度。但是在某些情况下,我们需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于SELECT的只读操作。Mysql5.6中select语句支持两种一致性的锁定读:

  • select …for update:对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁。
  • select… lock in share mode:对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。 才外select …for update,select… lock in share mode必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句select锁定语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0。

自增长与锁

自增长在数据库中是非常常见的一种属性,一般我们在建表时都对主键设置自增长属性。在InnoDB存储引擎中,对每个含有自增长值的表都有一个自增长计数器。当对含有自增长的计数器的表进行插入操作时,这个计数器被初始化。插入操作会依据这个自增长额计数器值加1赋予自增长列。这个实现方式称为AUTO-INC Locking。这中锁其实是采用了一种特殊的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的SQL语句后立即释放,这也是Mysql5.1.22版本之前的实现方式。但是AUTO-INC Locking还是存在一定的性能问题。首先,对于有自增长值的列的并发插入性能较差,事务必须等前一个插入的完成(虽然不用等待事务的完成)。其次对于insert…select大大数量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。

从Mysql5.1.22版本开始,InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,这种机制可以大大提高自增长值插入的性能。并且InnoDB提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,该参数的默认值为1。其总共有三个有效值:

  • 0 此值是为了兼容Mysql5.1.22之前的自增长实现方式,即通过表锁的AUTO-INC Locking方式。但是有了新的增长方式,此值不应该是新版用户的首选项。
  • 1 该值是默认值。对于“simple inserts”,该值会用互斥量去对内存中的计数器进行累加的操作。对于“bulk inserts”,还是使用传统表锁的AUTO-INC Locking方式。这种配置下,如果不考虑回滚操作,对于自增值列的增长还是连续的。
  • 2 在这个模式下,对于所有“INSERT-like”自增长值的产生都是通过互斥量,而不是AUTO-INC Locking的方式。显然,这是性能最高的方式。然而,这会带来一定的问题。因为并发插入的存在,在每次插入时,自增长的值可能不是连续的。此外,使用这个模式,任何时候都应该使用基于行的复制,这样才能保证最大的并发性以及主从数据的一致。

此外,还需要特别注意的是InnoDB存储引擎中自增长的实现和MyISAM不同,MyISAM存储引擎是表锁设计,自增长不用考虑并发插入的问题。 另外,在InnoDB存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一列,这Mysql数据库会抛异常,而MyISAM没有这个问题。

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://blog.csdn.net/qq_38571892/article/details/119650748
-- 展开阅读全文 --
Web安全—逻辑越权漏洞(BAC)
« 上一篇 03-13
Redis底层数据结构--简单动态字符串
下一篇 » 04-10

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复