MySQL系列之锁机制

2021年10月12日 194点热度 0人点赞 0条评论

开发多用户,数据库驱动的应用时,最大的一个难点是:一方面要最大程度的利用数据库的并发访问,另一方面还要确保每个用户能以一致的方式读取和修改数据,为此就有了锁机制。同时这也是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问,InnoDB存储引擎会在行级别上对表数据上锁。

myisam引擎不支持行锁,只支持表锁,并发情况下的读没有问题,但是并发插入时的性能就要差一些了。

InnoDB存储引擎的实现和Oracle数据库非常类似,提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。

lock和latch

在数据库中,锁可以分为lock和latch两种。

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

lock:lock的对象是事务,用来锁定的是数据库中的对象,如表,页,行等。并且一般lock的对象仅在事务commit或rollback后进行释放,(不同事务隔离级别释放的时间可能不同)。lock锁通过waits-for-graph,time-out机制来进行死锁检测和处理。lock具体可以分为行锁,表锁,意向锁。

lock与latch的比较:

lock latch
对象 事务 线程
保护 数据库内容 内存数据结构
持续时间 整个事务过程 临界资源
模式 行锁、表锁、意向锁 读写锁、互斥量
死锁 通过waits-for-graph,time-out机制来进行死锁检测和处理 无死锁检测与处理机制。仅通过应用程序加锁的顺序保证无死锁的情况发生
存在于 Lock Manager的哈希表中 每个数据结构的对象中

InnoDB存储引擎中的锁

锁的类型

InnoDB存储引擎实现了如下两种标准的行级锁:

  • 共享锁(S Lock),允许事务读一行数据
  • 排他锁(X Lock),允许事务删除或更新一行数据

如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁,这种情况称为锁不兼容。

排他锁和共享锁的兼容性如下表:

X S
X 不兼容 不兼容
S 不兼容 兼容

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

op1=>operation: 数据库
op2=>operation: 表
op3=>operation: 页
op4=>operation: 记录
op1->op2->op3->op4

如上图,数据库分为不同的层级,如果对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。

InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。其支持两种意向锁:

  • 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁;
  • 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁;

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。所以表级意向锁与行级锁的兼容性如下表:

IS IX S X
IS 兼容 兼容 兼容 不兼容
IX 兼容 兼容 不兼容 不兼容
S 兼容 不兼容 兼容 不兼容
X 不兼容 不兼容 不兼容 不兼容

一致性非锁定读

一致性的非锁定读是InnoDB存储引擎通过多版本并发控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或update操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。

之所以称为非锁定读,因为不需要等待访问的行上排他锁的释放。快照数据是指该行的之前的版本,该实现是通过undo段来完成。而undo用来在事务中回滚数据,因此快照数据本身是没有额外开销的。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作(历史不可篡改)。

非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同的事务隔离级别下,读取的方式是不同的,因为并不是每个事务隔离级别下都是采用非锁定的一致性读。此外,就算是都使用非锁定的一致性读,但是对于快照数据的定义也各不相同。

快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为多版本技术。由此带来的并发控制,称之为多版本并发控制。

在读已提交和可重复读隔离级别下,InnoDB存储引擎使用非锁定的一致性读。然而他们对于快照数据的定义是不同的。在读已提交下,对于快照数据,非锁定的一致性读总是读取被锁定行的最新一份快照数据。而在可重复读下,对于快照数据,非锁定的一致性读总是读取事务开始时的行数据版本。

一致性锁定读

在默认配置下,InnoDB存储引擎的事务隔离级别为可重复读,这个时候InnoDB存储引擎的select操作使用一致性非锁定读。但是在某些情况下,用户需要显式地对数据库读取操作进行加锁以保证数据库逻辑的一致性。而这要求数据库支持加锁语句,即使是对于select的只读操作。InnoDB存储引擎对于select语句支持两种一致性的锁定读操作:

  • select....for update
  • select ...lock in share mode

select....for update对读取的行记录加一个X锁,其他事务不能对已锁定的行记录加上任何锁。

select ...lock in share mode对读取的行记录加上一个S锁,其他事务可以向被锁定的行记录再加S锁,但是不能加X锁,会被阻塞。

对于一致性非锁定读,即使读取的行记录上加了显式的锁,比如select....for update,也是可以进行读取的,只不过读取的是行上的一个快照数据。此外select....for updateselect ...lock in share mode必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两个语句时,必须加上beginstart transaction或者set autocommit = 0

自增长与锁

自增长在数据库中是一种非常常见的属性,也是很多开发人员首选的主键方式。在InnoDB存储引擎的内存结构中,对于每一个含有自增长值的表都有一个自增长计数器。当对含有自增长的计数器的表进行插入操作时,插入操作会依据这个自增长的计数器值加一来赋予自增长列。这个实现方式叫auto-inc-locking。这种锁采用的是一种优化后的表锁机制,为了提高插入的性能,锁不是在一个事务完成后才释放的,而是在完成对自增长值插入的SQL语句后立即释放。

这种机制虽然从一定程度上提高了并发性,但是在并发插入上性能依然较差,事务必须等待前一个事务中的SQL语句执行完了才可以插入。在后续的版本,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,这种机制大大提高了自增长值插入的性能。并且提供了一个参数可以自主控制自增长的模式。

外键和锁

外键主要用于引用完整性的约束检查,在InnoDB存储引擎中,对于一个外键列,如果没有显示地为外键列加索引,InnoDB存储引擎会自动地为外键列加一个索引,因为这样可以避免死锁。

对于外键值的插入或更新,首先需要查询父表的记录,即 select 父表。但是对于父表的 select 操作,因为这时使用的是select lock in share mode方式,即主动对父表加了一个 S 锁。如果这时父表已经加了 X 锁,子表上的操作会被阻塞的。

设想一下如果访问父表时使用的是一致性非锁定读,那么会读到快照数据,如果满足插入条件,那么就直接插入了啊!但是如果另一个会话把这个记录删掉了,并且提交了,则父表中就并不存在这个数据了,造成了数据在子表,父表不一致的情况。

锁的算法

行锁的三种算法

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

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但是不包括记录本身
  • Next-Key Lock:锁定一个范围,包括行本身

Record Lock 总会去锁住索引记录,如果InnoDB存储引擎在创建表的时候没有设置任何一个索引,InnoDB存储引擎会使用隐式的主键来锁定。

Next-Key Lock 算法是结合了 Gap Lock 和 Record Lock 的一种锁定算法,InnoDB存储引擎对于行的查询都是使用这种锁定算法。

Next-Key Lock 锁定算法的设计目的是为了解决幻读问题。

注意:

当查询的索引含有唯一属性的时候,也就是定义的时候指定了唯一约束,InnoDB存储引擎会对 Next-Key Lock 降级为 Record Lock。锁降级的前提仅是被查询的列是唯一索引

InnoDB存储引擎会对辅助索引的下一个键值加上 Gap Lock。

Gap Lock 的设计目的是为了避免幻读。它的作用是防止不同事务将记录插入到同一个范围之内,这会导致幻读的产生。

用户可以通过设置隔离级别为RC来显示地关闭 Gap Lock 或者通过参数来设置。

幻读问题怎么解决

在默认的可重复读隔离级别下,InnoDB存储引擎采用 Next-Key Lock 算法来避免幻读的产生。

幻读是指,在同一事务中,连续执行两次相同的SQL语句可能读到不同的结果,第二次执行的SQL语句可能会返回之前不存在的行。

例子:
表中有记录 1,2,4
事务A执行以下操作:
select * from t where a > 2 for update 
应该能查到记录4
这时事务A没有提交

事务B执行以下操作:
begin;
insert into t select 5;
commit;

这时事务A继续执行那条SQL语句,就会发生幻读现象。读取到了记录5
这种情况在InnoDB存储引擎的RR隔离级别下不会发生,因为使用了next key lock算法,锁住了2到正无穷的范围

锁问题

通过锁定机制可以实现事务的隔离性要求,使得事务可以并发地工作,锁提高了并发,但是会带来潜在的问题——主要是脏读、不可重复读和幻读以及逻辑上的更新丢失问题。

脏读

理解脏读之前,先理解脏数据,脏数据和之前学习的脏页是完全不同的。脏页是指缓冲池中已经被修改的页,但是还没有刷新落盘,即数据库实例中内存与磁盘中的数据是不一致的,但是在刷新落盘之前,日志都已经被记录到了重做日志中。最后当脏页都刷回到磁盘的时候,两者会达到一致性。而脏数据指的是事务对数据进行了修改,但是还没有提交。

对于脏页的读取,是非常正常的,脏页是由于内存和磁盘的异步造成的,这部影响数据的最终一致性。并且脏页的刷新还是异步的,不影响数据的可用性,所以能带来性能的提高。

脏数据截然不同,脏数据是其他事务未提交的数据,如果读到了脏数据,这显然违反了事务的隔离性要求,即一个事务读取到了另一个事务未提交的数据。

不可重复读

不可重复读指的是一个事务多次读取同一数据集合,但是查询的结果不一致。原因是在第一个事务中的两次读数据之间,其他事务对数据做了更新。

注意脏读与不可重复读之间的区别主要是脏读是读到了未提交的事务,而不可重复读是读到了其他事务已经提交的数据。但是违反了数据库事务的一致性要求。一般来说,不可重复读的问题是可以接受的,毕竟是其他事务已经提交的数据。本身不会带来很大的问题。

在RR级别下,通过Next-Key Lock(Record Lock 锁住本身,避免修改这一条记录,造成不可重复读)算法,不仅锁住了索引,还锁住了索引覆盖的范围(Gap Lock 锁住范围,避免添加记录后的范围查询多了几条前面没有的记录,造成幻读),因此在这个范围内对于数据的插入都是不允许的。避免了不可重复读,也避免了幻读。

丢失更新问题

丢失更新是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。

在这种问题中,一定要注意修改之前先查看记录,并对记录加排它锁。这样其他事务对数据更新不会覆盖你的更新。

锁升级

锁升级就是将当前的锁的粒度降低。比如说数据库会把一张表的1000条行锁升级为一个页锁,或者将页锁升级为表锁。数据库的设计者认为锁是一种稀缺资源,想要避免锁的开销,那数据库中会频繁出现锁升级的现象。

InnoDB存储引擎不存在锁升级的现象,因为其不是根据每个记录来产生锁的,他是根据每个事务访问的每个页对锁进行一个管理,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销都是一致的。不存在锁升级!!!

agedcat_xuanzai

这个人很懒,什么都没留下

文章评论