【数据库与数据仓库】了解数据库锁

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ARPOSPF/article/details/83513006

数据库锁


何为锁?封闭的器物,以钥匙或暗码开启。在计算机中的锁一般用来管理对共享资源的并发访问,如锁定,同步等。

当然在数据库中也有锁用来控制资源的并发访问,这也是数据库和文件系统的区别之一。

什么事InnoDB的?

MySQL的体系结构

首先了解MySQL的体系结构:

发现MySQL的由连接池组件,管理服务和工具组件,SQL接口组件,查询分析器组件,优化器组件,缓冲组件,插件式存储引擎,物理文件组成。

在MySQL的中存储引擎是以插件的方式提供的,在MySQL的中有多种存储引擎,每个存储引擎都有自己的特点。

查看当前数据库默认的引擎:

InnoDB的存储引擎与MyISAM数据的比较:

事务的隔离性

锁在数据库中的功能之一就是用来实现事务的隔离性,而事务的隔离性其实是用来解决脏读,不可重复读,幻读等几类问题。

脏读

一个事务读取到另一个事务未提交的更新数据。

在事务A,B中,事务甲在时间点2,4分别对用户表中ID = 1的数据进行了查询。

但是事务乙在时间点3进行了修改,导致了事务A在4中的查询出的结果其实是事务乙修改后的。这样就破坏了数据库中的隔离性。

不可重复读

在同一个事务中,多次读取同一数据返回的结果不同,不可重复读和脏读不同的是这里读取的是已经提交过后的数据。

在事务乙中提交的操作在事务甲第二次查询之前,但是依然读到了事务乙的更新结果,也破坏了事务的隔离性。

幻读

一个事务读到另一个事务已提交的插入数据。

在事务阿中查询两次ID大于1的,在第一次ID大于1查询结果中没有数据,但是由于事务乙插入一条ID = 2的数据,导致事务甲第二次查询时能查到事务乙中插入的数据。

事务中的隔离性:

InnoDB的锁类型

MySQL的中常见的锁类型有哪些:

S或X.

在InnoDB的中实现了两个标准的行级锁,可以简单的看为两个读写锁:

小号共享锁:又叫读锁,其他事务可以继续加共享锁,但是不能继续加排它锁。

X排它锁:又叫写锁,一旦加了写锁之后,其他事务就不能加锁了。

兼容性:是指事务A获得一个某行某种锁之后,事务B同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。

纵轴是代表已有的锁,横轴是代表尝试获取的锁。

意向锁

意向锁在InnoDB中是表级锁,和它的名字一样它是用来表达一个事务想要获取什么。

意向锁分为:

  • 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。
  • 意向排他锁:表达一个事务想要获取一张表中某几行的排它锁。

这个锁有什么用呢?为什么需要这个锁呢?

答案是如果没有这个锁,要给这个表加上表锁,一般的做法是去遍历每一行看看它是否有航所,这样的话效率太低。使用意向锁,只需要判断是否有意向锁即可,不需要再去一行一行的去扫描。

在InnoDB中由于支持的是行级的锁,因此InnoDB锁的兼容性可以扩展为:

自增长锁

自增长锁是一种特殊的表锁机制,提高并发插入性能。

这个锁有几个特点:

  • 在SQL执行完就释放锁,并不是事务执行完
  • 对于insert ... select大数据量插入会影响插入性能,因为会阻塞另外一个事务执行。
  • 自增算法可以配置。

在MySQL5.1.2版本之后,有了很多优化,可以根据不同的模式来调整自增加锁的方式。

在MySQL中innodb_autoinc_lock_mode有3中配置模式0、1、2,分别对应:

传统模式:也就是最上面的使用表锁。

连续模式:对于插入的时候可以确定行数的使用互斥量,对于不能确定行数的使用表锁的模式。

交错模式:所有的都使用互斥量。为什么叫交错模式呢?有可能在批量插入的时候自增值不是连续的,当然一般来说如果不看重自增值连续一般选择这个模式,性能是最好的。

InnoDB锁算法

如何使用这些锁,还是靠锁算法。

记录锁(Record-Lock)

记录锁是锁住记录的,这里要说明的是这里锁住的是索引记录,而不是真正的数据记录。

如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住。

如果没有表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。

如果要锁的列没有索引,则会进行全表记录加锁。

间隙锁

顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫gap锁,其不会阻塞其他的gap锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。

next-key锁

这个锁本质是记录锁加上gap锁。在RR隔离级别下(InnoDB默认),InnoDB对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。

为什么呢?因为唯一索引能确定行数,而其他索引不能确定行数,有可能在其他事务中会再次添加这个索引的数据造成幻读。

这也说明了为什么MySQL可以在RR级别下解决幻读。

插入意向锁

插入意向锁MySQL官方对其的解释为:

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

可以看出插入意向锁是在插入的时候产生的,在多个事务同时写入不同数据至同一个索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。

假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排他锁,但是不会被互相锁住,因为数据行并不冲突。

要说明的是如果有间隙锁,插入意向锁会被阻塞。

MVCC

MVCC,多版本并发控制技术。在InnoDB中,在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号。通过版本号和行锁,从而提交数据库系统并发性能。

在MVCC中,对于读操作可以分为两种读:

  • 快照读:读取的历史数据,简单的select语句,不加锁,MVCC实现可重复读,使用的是MVCC机制读取undo中的已经提交的数据。所以它的读取是非阻塞的。
  • 当前读:需要加锁的语句,update、insert、delete、select...for update等等都是当前读。

在RR隔离级别下的快照读,不是以begin事务开始的时间点作为snapshot建立时间点,而是以第一条select语句的时间点作为snapshot建立的时间点。以后的select都会读取当前时间点的快照值。

在RC隔离级别下每次快照均会创建新的快照。

具体原理是通过每行会有两个隐藏的字段一个是用来记录当前事务,一个是用来记录回滚的指向Undolog。利用Undolog就可以读取到之前的快照,不需要单独开辟空间记录。

加锁分析

实验:自己创建一个表

在插入第三条数据的时候,数据库中已经有了主键为20的数据,所以冲突了,产生了错误!

数据库事务隔离选择了RR。

实验1

开启两个事务,进行实验1,如下图所示:

开启了两个事务并输入了上面的语句,发现事务B居然出现了超时。明明是对name=555这一行进行的加锁,为什么插入name=556被阻塞了?

输入以下命令:

mysql> select * from information_schema.INNODB_LOCKS;

发现事务A中给555加了next-key锁,事务B插入的时候会首先进行插入意向锁的插入。

于是得出下面的结果:

可以看到事务B由于间隙锁和插入意向锁的冲突导致了阻塞。

实验2

上面的查询条件用的是普通的非唯一索引,这里试一下主键索引:

发现事务B居然没有发生阻塞。如果按照实验1的套路应该会被阻塞的,因为25-30之间会有间隙锁。

使用命令行工具,发现只加了X记录锁。原来是应为唯一索引会降级记录锁。

这么做的理由是:非唯一索引加next-key锁由于不能确定明确的行数有可能其他事务在你查询的过程中,再次添加这个索引的数据,导致隔离性遭到破坏,也就是幻读。

唯一索引由于明确了唯一的数据行,所以不需要添加间隙锁解决幻读。

实验3

上面测试了主键索引、非唯一索引,这里还有个字段是没有索引的,如果对其加锁会出现什么呢?

发现不管是用实验1非间隙锁范围的数据,还是用间隙锁里面的数据都不行,难道是加了表锁?

确实这样,如果用没有索引的数据,其会对所有聚簇索引上都加上next-key锁。

所以在平常开发的时候如果对查询条件没有索引的,一定进行一致性读,也就是加锁读,会导致全表加上索引,会导致其他事务全部被阻塞,数据库基本会处于不可用状态。


死锁

死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种相互等待的现象。说明有等待才会有死锁,解决死锁可以通过去掉等待,比如回滚事务。

解决死锁的两个办法:

  • 等待超时:当某一个事务等待超时之后回滚该事务,另一个事务就可以执行了。但是这种做法效率较低,会出现等待时间,另一个问题是如果这个事务所占的权重较大,已经更新了很多数据,但是被回滚了,就会导致资源浪费。
  • 等待图(wait-for-graph):等待图用来描述事务之间的等待关系,当这个图如果出现回路(如下),事务就出现回滚,通常来说InnoDB会选择回滚权重较小的事务,也就是undo较小的事务。

复现问题

可以看见事务A出现被回滚了,而事务B成功执行。具体每个时间点发生了什么?

时间点2:事务A删除name='777'的数据,需要对777这个索引加上next-key锁,但是其不存在。所以只对555-999之间加间隙锁,同理,事务B也对555-999之间加间隙锁。间隙锁之间是兼容的。

时间点3:事务A,执行insert操作,首先插入意向锁,但是555-999之间有间隙锁。由于插入意向锁和间隙锁冲突,事务A被阻塞,等待事务B释放间隙锁。同理,事务B等待事务A释放间隙锁,于是出现了A->B,B->A回路等待。

时间点4:事务管理器选择回滚事务A,事务乙插入操作执行成功。

修复的Bug

这个过程中的问题是由于间隙锁,现在需要解决这个问题:

  • 方案1:隔离级别降级为RC,在RC级别下不会加入间隙锁,所以就不会出现问题了,但是在RC级别下会出现幻读,可提交读都破坏隔离性的问题,所以不可行。
  • 方案2:隔离级别升级为可序列化,但是在可序列化级别下,性能会较低,会出现较多的锁等待,同样的也不可行。
  • 方案3:修改代码逻辑,不要直接删,改成每个数据由业务逻辑去判断哪些是更新,哪些是删除,哪些是添加,工作量稍大,所以暂不考虑
  • 方案4:在删除之间,可以通过快照查询(不加锁),如果查询没有结果,则直接插入;如果有,通过主键进行删除,在之前的实验2中,通过唯一索引会降级为记录锁,所以不存在间隙锁。故该方案可行。

如何防止死锁

总结如下几点:

  • 以固定的顺序访问表和行。交叉访问更容易造成事务等待回路。
  • 尽量避免大事务,占有的资源锁越多,越容易出现死锁。建议拆成小事务。
  • 降低隔离级别,如果业务允许,将隔离级别调低也是可行的,比如将隔离级别从RR调整为RC,可以避免很多因为间隙锁造成的死锁。
  • 为表添加合理的索引。防止没有索引出现表锁,出现死锁的概率会突增。

参考资料:http://database.51cto.com/art/201809/584037.htm#topx

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/83513006
今日推荐